【问题标题】:FluentValidation - Validate interfaced child propertiesFluentValidation - 验证接口的子属性
【发布时间】:2020-08-15 15:00:07
【问题描述】:

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

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

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 中。

【问题讨论】:

    标签: fluentvalidation


    【解决方案1】:

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

    选项 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

    【讨论】:

    • 如果您选择第二个选项,我认为您需要稍微调整解决方案,以便 Add 方法采用回调函数,否则您将在实例化时陷入无限循环BinaryElementValidator。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-03
    • 1970-01-01
    • 2021-06-21
    • 2018-11-25
    • 2019-09-17
    相关资源
    最近更新 更多