Shoe Diamente
Shoe Diamente

Reputation: 794

FluentValidation implicit child validation using manual validation

Reading the documentation it appears that in .NET Core you can do implicit child properties validation with:

services.AddMvc().AddFluentValidation(fv => {
    fv.ImplicitlyValidateChildProperties = true;
});

Unfortunately I'm not working with MVC directly so I have to call Validate myself (IValidator<T> instances are registered by myself with Scrutor). Is there a similar setting for manually validating nested fields as well as top-level fields (using .NET Core DI)?

Upvotes: 3

Views: 1528

Answers (2)

Bouke Versteegh
Bouke Versteegh

Reputation: 4677

Another approach would be to define a base class for your validators, which itself handles the recursive validation.

I needed this specifically so that the validators run in a nested way, and the errors follow the structure of the object (e.g. request.account.name: 'Name' must not be empty).

This is the implementation:

using System.Linq.Expressions;
using System.Reflection;
using FluentValidation;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;

namespace Example.Validation;


public class RecursiveValidator<T> : AbstractValidator<T>
{
    private readonly IServiceProvider serviceProvider;

    public RecursiveValidator(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }

    public void ValidateMembers(Type allowedType)
    {
        foreach (var propertyInfo in typeof(T).GetProperties()) {
            if (allowedType.IsAssignableFrom(propertyInfo.PropertyType)) {
                ValidateMember(propertyInfo.PropertyType, propertyInfo.Name, allowedType);
            }
        }
    }

    private void ValidateMember(Type fieldType, string propertyName, Type allowedType)
    {
        var lambdaParameter = Expression.Parameter(typeof(T), "request");
        var lambdaBody = Expression.Property(lambdaParameter, propertyName);
        var propertyExpression = Expression.Lambda(lambdaBody, lambdaParameter);

        typeof(RecursionHelper)
            .GetMethod(nameof(RecursionHelper.ValidateMember), BindingFlags.Public | BindingFlags.Static)!
            .MakeGenericMethod(typeof(T), fieldType)
            .Invoke(obj: null, new object[] { this, serviceProvider, propertyExpression, allowedType });
    }

    private static class RecursionHelper
    {
        [UsedImplicitly]
        public static void ValidateMember<TMessage, TProperty>(
            AbstractValidator<TMessage> validator,
            IServiceProvider serviceProvider,
            Expression<Func<TMessage, TProperty>> expression,
            Type allowedType
        )
        {
            // Discovers and adds registered validators for the given type
            var fieldValidators = serviceProvider.GetServices<IValidator<TProperty>>().ToList();
            var rule = validator.RuleFor(expression);
            foreach (var fieldValidator in fieldValidators) {
                rule.SetValidator(fieldValidator);
            }

            // If no validators are found, create one and let it recurse
            if (fieldValidators.Count == 0) {
                var recursiveValidator = new RecursiveValidator<TProperty>(serviceProvider);
                recursiveValidator.ValidateMembers(allowedType);
                rule.SetValidator(recursiveValidator);
            }
        }
    }
}

To use it, inherit from RecursiveValidator<T> instead of ValidateMembers<T>, and .

Then call ValidateMembers(Type allowedType) when you want it to validate all members.


class UserValidator : RecursiveValidator<User> {
    public UserValidator(IServiceProvider services) : base(services) {
        // Recurse on specific types
        ValidateMembers(typeof(Address));
        ValidateMembers(typeof(PhoneNumber));

        // Recurse on all members of a baseclass, e.g. IMessage
        ValidateMembers(typeof(IMessage));

        RuleFor(user => user.Name).NotEmpty();
    }
}

Note, recursion will go only 1 level. All other validators should call ValidateMembers(typeof(SomeType)) manually.

If you want all validators to recurse automatically, then create another base class that calls all required ValidateMembers in the constructor.

public class DefaultValidator<T>: RecursiveValidator<T> {
    public DefaultValidator<T>(IServiceProvider services): base(services) {
       ValidateMembers(typeof(User));
       ValidateMembers(typeof(Address));
       ValidateMembers(typeof(Project));
       ValidateMembers(typeof(PhoneNumber));

       // Or just a single base class
       ValidateMembers(typeof(BaseEntity));
    }
}

Note: This implementation assumes you may have multiple validators for each type, and you want to run all of them. This is why the IServiceProvider is required. The standard IValidatorFactory only returns a single validator.

Upvotes: 0

If you check the FluentValidation source code you will notice that the ImplicitlyValidateChildProperties option uses the Asp.Net mechanism of validating nested properties so it won't work for manual validation.

You can write a helper or extension method that uses reflection and the IValidatorFactory service to do nested validation:

public static async Task<ValidationResult> ValidateImplicitAsync<TReq>(this TReq request, IValidatorFactory factory)
            where TReq: class
{
    var result = new ValidationResult();
    await ValidateRecursively(request, factory, result);
    return result;
}

static async Task ValidateRecursively(
            object obj, 
            IValidatorFactory factory, 
            ValidationResult result)
{
    if(obj == null) { return; }
    Type t = obj.GetType();
    IValidator validator = factory.GetValidator(t);
    if(validator == null) { return; }
    ValidationResult r = await validator.ValidateAsync(new ValidationContext<object>(obj));
    foreach(var error in r.Errors)
    {
        result.Errors.Add(error);
    }
    foreach(var prop in t.GetProperties())
    {
        object childValue = prop.GetValue(obj, null);
        await ValidateRecursively(childValue, factory, result);
    }
}

Then you could call it like this:

public class MyService {
    readonly IValidatorFactory factory;
    
    public MyService(IValidatorFactory factory){
       this.factory = factory;
    }

    public async Task Handle(MyModel model){
       ValidationResult validationResult = await model.ValidateImplicitAsync(factory);
       // ... etc...
    }
}

Upvotes: 2

Related Questions