FluentValidation 使用手动验证的隐式子验证

FluentValidation implicit child validation using manual validation

Reading the documentation 看来在 .NET Core 中您可以使用以下方法进行隐式子属性验证:

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

不幸的是,我没有直接使用 MVC,所以我必须自己调用 ValidateIValidator<T> 实例是我自己在 Scrutor 中注册的)。是否有类似的设置用于手动验证嵌套字段以及顶级字段(使用 .NET Core DI)?

如果您查看 FluentValidation 源代码,您会注意到 ImplicitlyValidateChildProperties 选项使用 Asp.Net 机制来验证嵌套属性,因此它不适用于手动验证。

您可以编写使用反射和 IValidatorFactory 服务进行嵌套验证的帮助程序或扩展方法:

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);
    }
}

那么你可以这样称呼它:

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...
    }
}

另一种方法是为您的验证器定义一个基 class,它本身处理递归验证。

我特别需要这个,以便验证器 运行 以嵌套方式出现,并且错误遵循对象的结构(例如 request.account.name: 'Name' must not be empty)。

这是实现:

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);
            }
        }
    }
}

要使用它,继承自 RecursiveValidator<T> 而不是 ValidateMembers<T>,并且 .

然后在您希望它验证所有成员时调用 ValidateMembers(Type allowedType)


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.

如果你希望所有验证器自动递归,那么创建另一个基础class,在构造函数中调用所有必需的ValidateMembers

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.