FluentValidation - 验证接口子属性

FluentValidation - Validate interfaced child properties

我正在尝试使用访问者模式在树结构上应用 FluentValidation (v 9.1.1)。它的特别之处在于,几个不同的树元素都实现了一个接口,并且元素的子属性都是这种接口类型。换句话说,子属性不是强类型的。简化模型见下文。每个验证器都进行了具体的实现,但我不明白如何为 interface children.

附加子验证器

这是一个演示模型(工作代码):

public interface IElement
{
    Type ResultType { get; }
    TResult Accept<TResult>(IElementVisitor<TResult> visitor);
}

public class ConstElement : IElement
{
    public object Value { get; set; }
    public Type ResultType => Value?.GetType();
    public TResult Accept<TResult>(IElementVisitor<TResult> visitor)
    {
        return visitor.VisitElement(this);
    }
}

public class BinaryElement : IElement
{
    // Child properties are not strongly typed.
    public IElement Left { get; set; }
    public IElement Right { get; set; }
    public Operand Operand { get; set; }
    public Type ResultType => Operand switch
    {
        Operand.Equal => typeof(bool),
        Operand.GreaterThan => typeof(bool),
        Operand.Plus => Left.GetType(),
        Operand.Multiply => Left.GetType(),
        _ => throw new NotImplementedException(),
    };
    public TResult Accept<TResult>(IElementVisitor<TResult> visitor)
    {
        return visitor.VisitElement(this);
    }
}

public enum Operand { Equal, GreaterThan, Plus, Multiply }

public class ConstElementValidator : AbstractValidator<ConstElement>
{
    public ConstElementValidator()
    {
        RuleFor(ele => ele.Value).NotNull().Must(value => (value is double) || (value is TimeSpan));
    }
}

public class BinaryElementValidator : AbstractValidator<BinaryElement>
{
    public BinaryElementValidator()
    {
        // Rules for the element itself
        RuleFor(ele => ele.Left).NotNull();
        RuleFor(ele => ele.Right).NotNull();
        RuleFor(ele => ele).Must(ele => IsValidResultTypeCombination(ele.Left.ResultType, ele.Right.ResultType, ele.Operand));
        // Add rules for child elements here? How?
    }
    private bool IsValidResultTypeCombination(Type left, Type right, Operand operand)
    {
        if (left == typeof(bool) && right != typeof(bool))
            return false;
        // other result type validations...
        return true;
    }
}

public interface IElementVisitor<TResult>
{
    TResult VisitElement(ConstElement element);
    TResult VisitElement(BinaryElement element);
}

public class ValidationVisitor : IElementVisitor<ValidationResult>
{
    public ValidationResult VisitElement(ConstElement element)
    {
        return new ConstElementValidator().Validate(element);
    }

    public ValidationResult VisitElement(BinaryElement element)
    {
        // How to add validation of element.Left and element.Right, 
        // taking into account, that their type is IElement, while Validators are bound to the implementation type?
        var result = new BinaryElementValidator().Validate(element);
        var leftResult = element.Left.Accept(this);
        var rightResult = element.Right.Accept(this);
        // merge leftResult and rightResult with result
        return result;
    }
}

一般来说,有两种添加子验证的方法。要么直接在验证器中调用子验证器,这会使 ValidationVisitor 过时,要么让验证器专注于它们自己的逻辑并在 ValidationVisitor 中添加子验证,如代码所示。

我现在能够继续的唯一方法是使用访问者并合并元素及其子元素的验证结果。

在这种情况下,有没有办法将子验证器添加到 BinaryElement 中?在访问者中或直接在 BinaryElementValidator 中。

有几种不同的方法可以做到这一点。您可以为每个接口实现者定义多个规则,也可以使用自定义 属性 验证器对类型进行运行时检查。这类似于 .

选项 1:使用类型过滤器的多个规则定义

使用此选项,您可以为接口的每个潜在实现者创建一个特定的规则定义:

// Inside your BinaryElementValidator use a safe cast inside the RuleFor definition. 
// If it isn't the right type, the child validator won't be executed 
// as child validators aren't run for null properties.

RuleFor(x => x.Left as BinaryElement).SetValidator(new BinaryElementValidator());
RuleFor(x => x.Left as ConstElement).SetValidator(new ConstElementValidator());

RuleFor(x => x.Right as BinaryElement).SetValidator(new BinaryElementValidator());
RuleFor(x => x.Right as ConstElement).SetValidator(new ConstElementValidator());


这是最简单的方法,但是通过在对 RuleFor 的调用中使用更复杂的表达式,您将绕过 FluentValidation 的表达式缓存,如果您多次实例化验证器,这将影响性能.我会留给您来决定这是否会成为您申请中的问题。

您可能还需要为每个规则调用 OverridePropertyName,因为 FluentValidation 无法使用此方法推断 属性 的名称。

选项 2:自定义 属性 验证器

一个稍微复杂的解决方案,但意味着您可以在 RuleFor 中坚持使用简单的 属性 表达式,这意味着您不会绕过缓存。这利用了一个名为 PolymorphicValidator 的自定义验证器,它将在运行时检查 属性 的类型。

RuleFor(x => x.Left).SetValidator(new PolymorphicValidator<BinaryElement, IElement>()
  .Add<BinaryElement>(new BinaryElementValidator())
  .Add<ConstElement>(new ConstElementValidator())
);

RuleFor(x => x.Right).SetValidator(new PolymorphicValidator<BinaryElement, IElement>()
  .Add<BinaryElement>(new BinaryElementValidator())
  .Add<ConstElement(new ConstElementValidator())
);

这里是 PolymorphicValidator 的代码:

public class PolymorphicValidator<T, TInterface> : ChildValidatorAdaptor<T, TInterface> {
    readonly Dictionary<Type, IValidator> _derivedValidators = new Dictionary<Type, IValidator>();

    // Need the base constructor call, even though we're just passing null.
    public PolymorphicValidator() : base((IValidator<TInterface>)null, typeof(IValidator<TInterface>))  {
    }

    public PolymorphicValidator<T, TInterface> Add<TDerived>(IValidator<TDerived> derivedValidator) where TDerived : TInterface {
        _derivedValidators[typeof(TDerived)] = derivedValidator;
        return this;
    }

    public override IValidator<TInterface> GetValidator(PropertyValidatorContext context) {
        // bail out if the current item is null
        if (context.PropertyValue == null) return null;

        if (_derivedValidators.TryGetValue(context.PropertyValue.GetType(), out var derivedValidator)) {
            return new ValidatorWrapper(derivedValidator);
        }

        return null;
    }

    private class ValidatorWrapper : AbstractValidator<TInterface> {

        private IValidator _innerValidator;
        public ValidatorWrapper(IValidator innerValidator) {
            _innerValidator = innerValidator;
        }

        public override ValidationResult Validate(ValidationContext<TInterface> context) {
            return _innerValidator.Validate(context);
        }

        public override Task<ValidationResult> ValidateAsync(ValidationContext<TInterface> context, CancellationToken cancellation = new CancellationToken()) {
            return _innerValidator.ValidateAsync(context, cancellation);
        }

        public override IValidatorDescriptor CreateDescriptor() {
            return _innerValidator.CreateDescriptor();
        }
    }
}

这种方法实际上会在未来的版本中添加到库中 - 如果您有兴趣,可以在这里跟踪它的发展:https://github.com/FluentValidation/FluentValidation/issues/1237