Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RuleForEach.SetValidator does not support AbstractValidator<T> #1080

Closed
mortware opened this issue Apr 5, 2019 · 6 comments
Closed

RuleForEach.SetValidator does not support AbstractValidator<T> #1080

mortware opened this issue Apr 5, 2019 · 6 comments

Comments

@mortware
Copy link

mortware commented Apr 5, 2019

System Details

  • FluentValidation version: 8.2.0
  • Web Framework version: ASP.NET Core 2.1

Issue Description

I need to validate a collection of complex child objects on my object. I have created an AbstractValidator for each child, expecting to be able to use it in the Rule chain.

RuleForEach.SetValidator does not support AbstractValidator. The documentation leads me to believe it should. Presently it will only support PropertyValidator.

RuleForEach(x => x.Assets).SetValidator(new MyAssetValidator(myService));

Am I missing something?

@JeremySkinner
Copy link
Member

JeremySkinner commented Apr 5, 2019

Hi, RuleForEach.SetValidator definitely does support AbstractValidator (and has done since RuleForEach was introduced)

There are 3 overloads of SetValidator:

  • One takes an IPropertyValidator
  • One takes an IValidator<T> (this is the overload that accepts an AbstractValidator<T> instance, where T is the collection element type)
  • One takes a Func<TParent, IValidator<T>> (where TParent is the parent model, and T is the collection element type)

What error are you seeing?

@mortware
Copy link
Author

mortware commented Apr 5, 2019

My bad - It was a problem with generics. It seems you can't run a validator on an interface. Is there some way to achieve what I'm trying to do?

The error is
"cannot convert from 'ContactValidator' to 'FluentValidation.Validators.IPropertyValidator' but I suspect that is the error from the first overload.

public class Person
{
    public Address Address { get; set; }
    public IList<IContact> Contacts { get; set; }
}

public interface IContact
{
    string Value { get; }
}

public class Address {}

public class Contact : IContact
{
    public string Value { get; set; }
}

public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(p => p.Address)
            .SetValidator(new AddressValidator());

        RuleForEach(p => p.Contacts)
            .SetValidator(new ContactValidator());
    }
}

public class AddressValidator : AbstractValidator<Address> { }

public class ContactValidator : AbstractValidator<Contact> { }

@JeremySkinner
Copy link
Member

That's correct - the compiler won't allow that as the types don't match. Either your proprety would need to be defined as IList<Contact>, or your validator would need to be defined as AbstractValidator<IContact>.

(Just a tip, github uses 3 backticks to format code blocks: https://help.github.com/en/articles/creating-and-highlighting-code-blocks)

@mortware
Copy link
Author

mortware commented Apr 5, 2019

That's a shame... as I need to validate against different types of IContact (Email, Phone, Skype etc.)

If I make the AbstractValidator<IContact> I will need to re-cast it to the implementation. I'll be doing this to determine the correct validator as well.

EDIT

Ignore the above - how can I validate my implementations when the properties don't exist on the interface? i.e.

    public class Person
    {
        public IList<IContact> Contacts { get; set; }
    }

    public interface IContact
    {
        string Value { get; }
    }

    public class Email : IContact
    {
        public string DisplayName { get; set; }
        public string Value { get; set; }
    }

    public class PersonValidator : AbstractValidator<Person>
    {
        public PersonValidator()
        {
            RuleForEach(p => p.Contacts)
                .Where(c => c is Email)
                .SetValidator(new EmailValidator()); // <---- COMPILATION ERROR
        }
    }
    
    public class EmailValidator : AbstractValidator<Email>
    {
        public EmailValidator()
        {
            RuleFor(c => c.DisplayName)
                .NotEmpty();
        }
    }

I'm not saying this "should" work. I'd like to know if it's possible to cast the IContact into a concrete class for the validator.

(You saw me struggling with the code formatting - thanks for the tip!)

@JeremySkinner
Copy link
Member

JeremySkinner commented Apr 5, 2019

Just to explain further, validators are contravariant in T (the interface is defined a IValidator<in T>), so you if your property is defined as IList<Contact> and your validator is defined as AbstractValidator<IContact> then this would work (even though the types aren't exact) because the variance allows a validator for the interface to be used against a concrete type that implements that intreface (which is perfectly safe), but what you're trying to do is the opposite, which the compiler (correctly) won't allow, as its possible that not all items in the collection will be of type concrete Contact type.

So as this can't be handled at a compiler level it must be done at runtime. You need to be making a decision at runtime to select which validator you want to use based on the type of each element in the collection. There's a few ways to do this:

Use OfType in your rule definition

RuleForEach(p => p.Contacts.OfType<Contact>())
	.SetValidator(new ContactValidator());

RuleForEach(p => p.Contacts.OfType<Email>())
	.SetValidator(new EmailValidator());

This is simple, but is expensive as method calls inside RuleFor/RuleForEach can't be cached, so it makes validator instantiation much slower. This may or may not be an issue for you.

Implement a Polymorphic child validator

Edit: This is now available in FluentValidation 9.2. Please see here: https://docs.fluentvalidation.net/en/latest/inheritance.html

You can create a custom property validator that inspects the type of the current item being validated and picks the appropriate validator to use. The property validator would be defined like this:

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

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

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

	public override IValidator 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 derivedValidator;
		}

		return null;
	}
}

You'd then use it like this:

RuleForEach(p => p.Contacts).SetValidator(new PolymorphicValidator<IContact>()
	.Add<Contact>(new ContactValidator())
	.Add<Email>(new EmailValidator())
);

(This is untested, but hopefully illustrates the point!)

Edit 8th July 2020: For FluentValidation 9.x, the code will need to be updated slightly as ChildValidatorAdaptor now requires generic type parameters, see #1433 for details.

Edit 26th August 2020 This is now available in FluentValidation 9.2. Please see here: https://docs.fluentvalidation.net/en/latest/inheritance.html

@mortware
Copy link
Author

mortware commented Apr 5, 2019

Perfect. You've helped fill a gap in my knowledge and fix my issue.

Thank you so much for your feedback

@mortware mortware closed this as completed Apr 5, 2019
@lock lock bot locked and limited conversation to collaborators Apr 19, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants