Validation of Complex Objects in Multi-Lingual Environment Using DynamicVNET With ASP.NET Core

In this article, we will see why we created our own validation library and how it was needed.

Everything started in 2018 when we needed a flexible and easy-to-use validation tool for a commercial project. The goal was to create a library that could validate POCO (Plain Old CLR Objects) while following the Single Responsibility Principle (SRP). This is how DynamicVNET was born, a rule-based validation library that has now been downloaded over 13,000 times on NuGet.

The main aim of DynamicVNET was to give developers a solution they could easily add to their projects, so they would not have to repeat the same validation code. With a simple API, developers can create validation rules that work at runtime, making it perfect for projects that need flexible validation options.

Let’s start to demonstrate our library with an ASP.net core example.

ASP.NET Core Environment Preparation

In our example, we have asp.net core web api which has the phone validation controller and validate action that accepts input argument phone number object. So, in the requested action, we need to validate the input argument with rules defined by languages.

To implement this scenario we have to integrate the validation library into the request lifecycle. So, in our case, we will use an action filter to achieve this.

First of all, we need to install the DynamicVNET Nuget package. Also, keep that we are using the stable version of it.

Install-Package DynamicVNET

Let’s start using it.

To bind validation for each input parameter we have to define the BindValidator attribute.

[System.AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
public class BindValidator : Attribute { }

So simple to understand, the next step is to declare an action filter which provides validation.

public class LangValidatorFilter : IActionFilter
{
    private readonly ProfileValidator _validator;
   
    public LangValidatorFilter(ProfileValidator validator)
    {
        this._validator = validator;
    }

    public void OnActionExecuted(ActionExecutedContext context) { }
    
    public void OnActionExecuting(ActionExecutingContext context)
    {
        if (context.ActionArguments != null && context.ActionArguments.Count >= 1)
        {
            var validatableArguments = context.ActionDescriptor.Parameters
                                                .Where(ar => context.ActionArguments.ContainsKey(ar.Name))
                                                .Select(s => s as ControllerParameterDescriptor)
                                                .Where(a => a != null && a.ParameterInfo.CustomAttributes.Any(at => at.AttributeType == typeof(BindValidator)));

            // That part can be replaced by any globalization technique.
            string langName = context.HttpContext.Request.Query["culture"].ToString() ?? "en";

            foreach (var argument in validatableArguments)
            {
                try
                {
                    var validationResults = this._validator.Validate(langName, context.ActionArguments[argument.Name]);
                    
                    if (validationResults != null && validationResults.Any())
                    {
                        foreach (var validationResultItem in validationResults)
                        {
                            context.ModelState.AddModelError(validationResultItem.MemberName,
                                                                validationResultItem.ErrorMessage);
                        }
                    }
                }
                catch (InvalidProfileNameException)
                {
                    context.Result = new StatusCodeResult(404);
                    break;
                }
            }
        }
    }
}

As you can see code above, in the executing step we are getting arguments marked by BindValidator attributes. After that, we got the language key and escaped the globalization stuff. Instead, we used query param for language detection (just for simulation). In the last step, we looped all arguments with ProfileValidator (I should add that ProfileValidator is simple, it provides validation rules by string name) validated them, and got the results. We then filled in the errors using modelState.

After all these steps we should implement a factory for action filter.

[System.AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public class VNETFilterFactory : Attribute, IFilterFactory
{
    public bool IsReusable => false;

    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
    {
        DynamicVNET.Lib.ProfileValidator instance = serviceProvider.GetService(typeof(DynamicVNET.Lib.ProfileValidator)) as DynamicVNET.Lib.ProfileValidator;
        return new LangValidatorFilter(instance);
    }
}

The last part is the defined configuration from ConfigureServices. To apply best practice we are using the extension method.

public static class DIExtensions
{
    public static IServiceCollection AddProfileValidation(this IServiceCollection services,
                                                                    Action<ProfileValidator> setup)
    {
        if (services == null)
            throw new ArgumentNullException(nameof(services));
        if (setup == null)
            throw new ArgumentNullException(nameof(ProfileValidator));
        ProfileValidator validator = new ProfileValidator();
        setup(validator);       
        services.AddSingleton(validator);
        return services;
    }
}

Everything is ready to go.

ASP.NET Core Web API Example

As we explain above we are planning to validate the phone number through the API controller.

First of all, we need to declare request and response models.

public class PhoneNumberRequest
{
    public byte AttempsCount { get; set; }
    public string Name { get; set; }
    public string NumberPrefix { get; set; }
    public string Number { get; set; }
}
public sealed class PhoneNumberValidationException(string member, string message)
{
    public string Member { get; } = member;
    public string Message { get; } = message;
}
public class PhoneNumberResponse
{
    public bool IsSuccess => this.Errors == null || !this.Errors.Any();
    public IEnumerable<PhoneNumberValidationException> Errors { get; }
    public PhoneNumberResponse(IEnumerable<PhoneNumberValidationException> errors)
    {
        this.Errors = errors;
    }
    public PhoneNumberResponse() : this(null) { }
}

Everything is clear, so the next step is to declare api controller.

[ApiController]
[Route("[controller]")]
public class PhoneNumberValidatorController : ControllerBase
{
    [HttpGet]
    [VNETFilterFactory]
    public IActionResult Validate([BindValidator][FromBody] PhoneNumberRequest model)
    {
        if (!ModelState.IsValid)
        {
            var errors = ModelState.Select(x => new PhoneNumberValidationException(x.Key, x.Value.Errors.FirstOrDefault()?.ErrorMessage));
            return Ok(new PhoneNumberResponse(errors));
        }

        return Ok(new PhoneNumberResponse());                
    }
}

The controller contains the Validate action method which provides validation of all types of numbers. So, as you can see BindValidator declared with argument. In case of an invalid modelState, the action returns errors otherwise returns an empty response with a success message.

Our sample is almost ready, only validation methods are missing. Let’s continue with defining validation through DynamicVNET. Also according to readability, we will use method-based validation instead of strongly typed one.

builder.Services.AddProfileValidation(validator =>
{
    validator.AddProfile<PhoneNumberRequest>("az", marker =>
    {
        marker
        .Required(x => x.Name)
        .Required(x => x.Number)
        .Range(x => x.AttempsCount, 1, 10)
        .StringLen(x => x.Number, 12)
        .Required(x => x.NumberPrefix)
        .Predicate(x => x.NumberPrefix.StartsWith("+994"), "Invalid prefix, it should start with +994!");
    });
    validator.AddProfile<PhoneNumberRequest>("en", marker =>
    {
        marker
        .Required(x => x.Name)
        .Required(x => x.Number)
        .Range(x => x.AttempsCount, 1, 5)
        .StringLen(x => x.Number, 10);
    });
    validator.AddProfile<PhoneNumberRequest>("fr", marker =>
    {
        marker
        .Required(x => x.Name)
        .Required(x => x.Number)
        .Range(x => x.AttempsCount, 1, 3)
        .StringLen(x => x.Number, 9)
        .Required(x => x.NumberPrefix)
        .Predicate(x => x.NumberPrefix.StartsWith("+33"), "Invalid prefix, it should start with +33!");
    });
});

As you can see in ConfigureServices we just declared AddProfileValidation and setup needed validations by culture. But the main point is that DynamicVNET provides a declarative approach for achieving flexible validations.

Let’s see the result through the postman.

Postman

When inputs are not valid we are getting error which in our case is invalid number prefix. Also we can apply to another language.

Localhost

As you can see how it works in a very simple and straightforward way.

As a result of our example easily shows how we can work flexibly and validate very complex structures.

Conclusion

DynamicVNET is a lightweight, flexible, and easy-to-use validation library designed for .NET applications. It provides a dynamic, rule-based validation system that can be integrated seamlessly into your projects, offering robust validation logic without the need for hard-coded rules. It also includes various methods and extension points, making it adaptable to all types of validations.

One of the strengths of DynamicVNET is how it integrates effortlessly with ASP.NET Core. By using DynamicVNET in your ASP.NET Core applications, you can easily validate incoming request models (e.g., in API controllers) without tightly coupling your validation logic to specific models or methods. This ensures clean separation of concerns, allowing you to maintain a well-structured, SRP-compliant application. Its flexibility enables developers to adapt validation rules dynamically, which is especially useful in applications with frequently changing business requirements.

Whether you are building an API, a web app or a service-based architecture, DynamicVNET can help you keep your validation logic clean, reusable, and maintainable. Also you can check it out here.

Stay tuned!