如何使用FluentValidation验证集合中的不同类型?

问题描述

我有一个带有需要验证的集合的类。集合上的泛型带有一个接口,可以将不同的类型添加到集合中。

创建支持多态的FluentValidation验证器的最干净的方法是什么?

public interface IWizardStep {}

public class WizardOne : IWizardStep
{
    public string Model { get; set; }
}

public class WizardTwo : IWizardStep
{
    public string FirstName { get; set; }
}

public class Wizard
{
    public Wizard()
    {
        var w1 = new WizardOne();
        var w2 = new WizardTwo();

        Steps = new List<IWizardStep>
                    {
                        w1,w2
                    };
    }

    public IList<IWizardStep> Steps { get; set; }
}

public class WizardValidator : AbstractValidator<Wizard>
{
    public WizardValidator()
    {
        RuleFor(x => x.Steps)

        // Steps First where is WizardOne
        // Model.NotEmpty()

        // Steps First where is WizardTwo
        // FirstName.NotEmpty()
    }

解决方法

FluentValidation不支持此类子集合的多态性,但是您可以使用自定义属性验证器或在规则定义中使用OfType来添加此行为。

I've written about both approaches before here

第1步:为每个实现者创建一个验证器

首先为WizardOne和WizardTwo创建验证器:

public class WizardOneValidator : AbstractValidator<WizardOne> {
  public WizardOneValidator() {
    RuleFor(x => x.Model).NotEmpty();
  }
}

public class WizardTwoValidator : AbstractValidator<WizardTwo> {
  public WizardTwoValidator() {
    RuleFor(x => x.FirstName).NotEmpty();
  }
}

步骤2:创建父验证器

您有两个用于定义父验证器的选项。最简单的方法是使用OfType,但是性能较低。更为复杂的选项是使用自定义属性验证器。

选项1:使用OfType

public WizardValidator : AbstractValidator<Wizard> {
  public WizardValidator() {
    RuleForEach(x => x.Steps.OfType<WizardOne>()).SetValidator(new WizardOneValidator());
    RuleForEach(x => x.Steps.OfType<WizardTwo>()).SetValidator(new WizardTwoValidator());
  }
}

这是最简单的方法,但是在调用OfType中调用RuleFor最终将绕过FluentValidation的表达式缓存,这可能会打击性能。它还会迭代多个集合。对您而言,这可能是问题,也可能不是问题-您需要确定这是否会对您的应用程序产生现实影响。

选项2:使用自定义的PropertyValidator。

这使用了一个自定义的自定义验证器,该验证器可以在运行时区分基础类型:

public WizardValidator : AbstractValidator<Wizard> {
  public WizardValidator() {
    RuleForEach(x => x.Steps).SetValidator(new PolymorphicValidator<Wizard,IWizardStep>()
      .Add<WizardOne>(new WizardOneValidator())
      .Add<WizardTwo>(new WizardTwoValidator())
    );
  }
}

从语法上讲,这不是很好,但是不会绕过表达式缓存并且不会多次迭代集合。这是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();
        }
    }
}

将来可能会在库中将其作为一流功能实现-you can track its development here if you're interested