Chase Florell
Chase Florell

Reputation: 47387

How can I validate different types within a collection using FluentValidation?

I have a class with a collection that needs validation. The generic on the collection takes an interface and different types can be added to the collection.

What is the cleanest path forward to creating a FluentValidation validator that supports polymorphism?

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

Upvotes: 4

Views: 2021

Answers (1)

Jeremy Skinner
Jeremy Skinner

Reputation: 1461

FluentValidation doesn't support polymorphism for child collections like this out of the box, but you can add this behaviour by using a custom property validator, or by using OfType in your rule definitions.

I've written about both approaches before here:

Step 1: Create a validator for each implementor

Start by creating a validator for WizardOne and 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();
  }
}

Step 2: Create the parent validator

You have two options for defining the parent validator. The simplest approach is to use OfType, but this is less performant. The more complex option is to use a custom property validator.

Option 1: Using 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());
  }
}

This is the simplest approach, but calling OfType inside the call RuleFor will end up bypassing FluentValidation's expression cache, which is a potential performance hit. It also iterates the collection multiple. This may or may not be an issue for you - you'll need to decide if this has any real-world impact on your application.

Option 2: Using a custom PropertyValidator.

This uses a custom custom validator which can differentiate the underlying type at runtime:

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

Syntactically, this isn't quite as nice, but doesn't bypass the expression cache and doesn't iterate the collection multiple times. This is the code for the 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();
        }
    }
}

This will probably be implemented in the library as a first class feature at some point in the future - you can track its development here if you're interested.

Upvotes: 7

Related Questions