Handling inheritance

May 6, 2011 at 3:43 AM

Hi there;

Today I came across an interesting scenario and end up lost, I couldn't figure this out.
I hope that you can help me with that.
This pseudo code will help you understand it:

 

    public class Branch
    {
        public List<Person> People { get; set; }
    }

    public abstract class Person
    {
        public int id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

    public class Employee : Person
    {
        public int DepartmentId { get; set; }
        public double Salary { get; set; }
    }

    public class Member : Person
    {
        public int AccessType { get; set; }
        public string ExpiryMonth { get; set; }
        public string ExpiryYear { get; set; }
    }

 

 

So, I created a PersonValidator and a BranchValidator.
In the BranchValidator I have:

RuleFor(e => e.People).SetValidator(new PersonValidator())

The problem is that PersonValidator wont validate properties that are specific for Employees or Members, which are derived from Person.
How is the best or recommended way to handle this kind of situation?

Thanks in advance.

Coordinator
May 6, 2011 at 9:11 AM
Edited May 6, 2011 at 11:16 AM

Hi

So there isn't anything built in that supports this kind of polymorphic validation, but it should possible to build something that does this. First, you'll need appropriate validators for each subclass (a MemberValidator, EmployeeValidator, PersonValidator) and these all inherit from a base PersonValidator<TPerson> where TPerson : Person, like this:

 

public class PersonBaseValidator<TPerson> : AbstractValidator<TPerson>  where TPerson : Person {
  public PersonBaseValidator() {
    RuleFor(x => x.Id).NotEqual(0);
  }
}

public class PersonValidator : PersonBaseValidator<Person> {
  public PersonValidator() {
    // unknown person instances - just inherit the rules from the base. 
  }
}

public class EmployeeValidator : PersonBaseValidator<Employee> {
  public EmployeeValidator() {
    RuleFor(x => x.Salary).GreaterThan(0);
  }
}

public class MemberValidator : PersonBaseValidator<Member> {
  public MemberValidator() {
    RuleFor(x => x.ExpiryMonth).NotNull();
  }
}

 

So all of the rules for the base Person class are derived in the base PersonBaseValidator, so all of the subclass validators automatically inherit the rules for the base class. The 'PersonValidator' is for any unknown subclasses that don't have validators explicitly defined. 

Now, we need some way to tell FluentValidation in which situation we should use which validator. We can do this by creating a custom property validator for the collection that discriminates based on type. Usage would be like this:

 

RuleFor(x => x.People).SetValidator(
	new PolymorphicCollectionValidator<Person>(new PersonValidator()) //the base validator in the ctor
				.Add<Employee>(new EmployeeValidator())   //then we add a validator per subclass
				.Add<Member>(new MemberValidator()) 
);

 

The implementation of the PolymorphicCollectionValidator would be like this:

 

public class PolymorphicCollectionValidator<TBaseClass> : FluentValidation.Validators.NoopPropertyValidator {
  Dictionary<Type, IValidator> derivedValidators = new Dictionary<Type, IValidator>();
  IValidator<TBaseClass> baseValidator;

  public PolymorphicCollectionValidator(IValidator<TBaseClass> baseValidator) {
    this.baseValidator = baseValidator;
  }

  public PolymorphicCollectionValidator<TBaseClass> Add<TDerived>(IValidator<TDerived> derivedValidator) where TDerived : TBaseClass {
    derivedValidators[typeof(TDerived)] = derivedValidator;
    return this;
  }

  public override IEnumerable<ValidationFailure> Validate(PropertyValidatorContext context) {
    var collection = context.PropertyValue as IEnumerable<TBaseClass>; 

    // bail out if the property is null or the collection is empty
    if (collection == null) return Enumerable.Empty<ValidationFailure>();
    if (!collection.Any()) return Enumerable.Empty<ValidationFailure>();

    // get the first element out of the collection and check its real type. 
    var actualType = collection.First().GetType();

    IValidator derivedValidator;

    if(derivedValidators.TryGetValue(actualType, out derivedValidator)) {
      // we found a validator for the specific subclass. 
      var collectionValidtor = new ChildCollectionValidatorAdaptor(derivedValidator);
      return collectionValidtor.Validate(context);
    }

    // Otherwise fall back to the validator for the base class.
    var collectionValidator = new ChildCollectionValidatorAdaptor(baseValidator);
    return collectionValidator.Validate(context);
  }
}

Edit: Of course, this assumes that all items in the collection are of the same type and uses the same validator for each item in the collection (it uses the type of the first item in the collection to determine which validator to use), which isn't ideal. To support this scenario you'd have to change the above code to iterate over each item in the collection and make the decision about whether to validate (take a look at the source for the ChildCollectionValidatorAdaptor for a template of the sort of code this would need)

Hope this helps

Jeremy

Feb 14, 2013 at 1:00 PM
Hi!

I started with ServiceStack the last days and reached the validation part today.
FluentValidation is a really nice validation lib.

I had to test this kind of polymorphism too, but i decided to do it with just one entity instead of a collection. I started using your sample code.
So you can create the exact same validators (base and inherited validators) and use them.
In my version, u just need to register them once with the PolymorphicAutoValidator:
PolymorphicAutoValidator.RegisterDerivedValidator(new HR.RoBP.Contracts.Validators.Model.EmployeeValidator());   
` So you end up with:
            RuleFor(r => r.PropertyWithABaseTypeOfPerson).SetValidator(new PolymorphicAutoValidator());
If the Property PropertyWithABaseTypeOfPerson is an Employee class, its validated with the registered EmployeeValidator.

Its up to you to implement the default Validator if the type is not defined.
I also could think of a mechanism scanning an assembly and registering all types of IValidator once. This would remove the requirement of registering each derived validator.
public class PolymorphicAutoValidator: NoopPropertyValidator
{
    private static readonly Dictionary<Type, IValidator> DerivedValidators = new Dictionary<Type, IValidator>();

    public static void RegisterDerivedValidator<TDerived>(IValidator<TDerived> derivedValidator)
    {
        DerivedValidators.Add(typeof(TDerived),derivedValidator);   
    }

    public override IEnumerable<ValidationFailure> Validate(PropertyValidatorContext context)
    {
        var type = context.PropertyValue.GetType();
        if (DerivedValidators.ContainsKey(type))
        {
            var validator = new ChildValidatorAdaptor(DerivedValidators[type]);
            return validator.Validate(context);
        }
        return null;
    }

}
```
Feb 14, 2013 at 2:53 PM
Since i am new to this framework, i now noticed this nice snippet in the forum.

Instead of registration inside a dictionary, u could use a factory like this:
public class StructureMapValidatorFactory : ValidatorFactoryBase {
    public override IValidator CreateInstance(Type validatorType) {
        return ObjectFactory.TryGetInstance(validatorType) as IValidator;
    }
}
Sep 25, 2013 at 8:28 PM
I have implemented the above example for inheritance, the validation worked correctly but the error messages are not being submitted properly to the screen.
Only the field marked in red and the error messages are not displayed on the screen, I noticed that they are not being displayed during the assembly of the display html as well.
Any idea what might be missing?
Tks
Sep 26, 2013 at 5:40 PM
Edited Sep 26, 2013 at 5:46 PM
This code that was implemented:

[FluentValidation.Attributes.Validator(typeof(AddressValidator))]
public class Address : BaseEntity
{
    public string City { get; set; }

    public string StreetComplement { get; set; }

    public string Country { get; set; }

    public string District { get; set; }

    public string Street { get; set; }

    public string StreetNumber { get; set; }

    public string State { get; set; }

    [MaskHtml(CssClass = "cep")]
    public string ZipCode { get; set; }

    public string Reference { get; set; }
}

[FluentValidation.Attributes.Validator(typeof(ShippingAddressValidator))]
public class ShippingAddress : Address
{
    public int Id { get; set; }

    public string ShippingRecipientName { get; set; }

    public string Local { get; set; }

    public string PhoneAreaCode { get; set; }

    public string PhoneNumber { get; set; }

    public string PhoneAreaCodeAlternative { get; set; }

    public string PhoneNumberAlternative { get; set; }

    public static explicit operator ShippingAddress(EnderecoEntrega enderecoEntrega)
    {
        return new ShippingAddress
        {
            ShippingRecipientName = enderecoEntrega.nome,
            Local = enderecoEntrega.local,
            Street = enderecoEntrega.logradouro,
            StreetNumber = enderecoEntrega.numero,
            StreetComplement = enderecoEntrega.complemento,
            District = enderecoEntrega.bairro,
            City = enderecoEntrega.cidade,
            State = enderecoEntrega.estado,
            Country = enderecoEntrega.pais,
            ZipCode = enderecoEntrega.cep,
            PhoneNumber = enderecoEntrega.telefone,
            PhoneAreaCode = enderecoEntrega.ddd_telefone,
            PhoneNumberAlternative = enderecoEntrega.celular,
            PhoneAreaCodeAlternative = enderecoEntrega.ddd_celular
        };
    }
}

public class AddressBaseValidator<TAddress> : AbstractValidator<TAddress> where TAddress : Address
{
    public AddressBaseValidator()
    {
        RegisterRules();
    }

    private void RegisterRules()
    {
        RuleFor(model => model.City)
            .NotNull().WithLocalizedMessage(() => Resources.Address.City)
            .NotEmpty().WithLocalizedMessage(() => Resources.Address.City)
            .Length(1, 100).WithLocalizedMessage(() => Resources.Address.CityLength);

        RuleFor(model => model.StreetComplement)
            .Length(0, 255).WithLocalizedMessage(() => Resources.Address.ComplementLength);

        RuleFor(model => model.Country)
            .Length(0, 3).WithLocalizedMessage(() => Resources.Address.CountryLength);

        RuleFor(model => model.District)
            .NotNull().WithLocalizedMessage(() => Resources.Address.District)
            .NotEmpty().WithLocalizedMessage(() => Resources.Address.District)
            .Length(1, 80).WithLocalizedMessage(() => Resources.Address.DistrictLength);

        RuleFor(model => model.Street)
            .NotNull().WithLocalizedMessage(() => Resources.Address.Street)
            .NotEmpty().WithLocalizedMessage(() => Resources.Address.Street)
            .Length(1, 200).WithLocalizedMessage(() => Resources.Address.StreetLength);

        RuleFor(model => model.StreetNumber)
            .NotNull().WithLocalizedMessage(() => Resources.Address.StreetNumber)
            .NotEmpty().WithLocalizedMessage(() => Resources.Address.StreetNumber)
            .Length(1, 10).WithLocalizedMessage(() => Resources.Address.StreetNumberLength);

        RuleFor(model => model.State)
            .NotNull().WithLocalizedMessage(() => Resources.Address.State)
            .NotEmpty().WithLocalizedMessage(() => Resources.Address.State)
            .Length(1, 20).WithLocalizedMessage(() => Resources.Address.StateLength);

        RuleFor(model => model.ZipCode)
            .NotNull().WithLocalizedMessage(() => Resources.Address.ZipCode)
            .NotEmpty().WithLocalizedMessage(() => Resources.Address.ZipCode)
            .Length(1, 10).WithLocalizedMessage(() => Resources.Address.ZipCodeLength);
    }
}

public class AddressValidator : AddressBaseValidator<Address>
{
    public AddressValidator()
    {
    }
}

public class ShippingAddressValidator : AddressBaseValidator<ShippingAddress>
{
    public ShippingAddressValidator()
    {
        RegisterRules();
    }

    private void RegisterRules()
    {
        RuleFor(model => model.PhoneAreaCode)
            .Length(0, 5).WithLocalizedMessage(() => Resources.Address.AreaCodeLength);

        RuleFor(model => model.PhoneNumber)
            .Length(0, 15).WithLocalizedMessage(() => Resources.Address.PhoneLength);

        RuleFor(model => model.PhoneAreaCodeAlternative)
            .Length(0, 5).WithLocalizedMessage(() => Resources.Address.CellphoneAreaCodeLength);

        RuleFor(model => model.PhoneNumberAlternative)
            .Length(0, 15).WithLocalizedMessage(() => Resources.Address.CellphoneLength);

    }
}

public class PolymorphicCollectionValidator<TBaseClass> : FluentValidation.Validators.NoopPropertyValidator
{
    Dictionary<Type, IValidator> derivedValidators = new Dictionary<Type, IValidator>();
    IValidator<TBaseClass> baseValidator;

    public PolymorphicCollectionValidator(IValidator<TBaseClass> baseValidator)
    {
        this.baseValidator = baseValidator;
    }

    public PolymorphicCollectionValidator<TBaseClass> Add<TDerived>(IValidator<TDerived> derivedValidator) where TDerived : TBaseClass
    {
        derivedValidators[typeof(TDerived)] = derivedValidator;
        return this;
    }

    public override IEnumerable<ValidationFailure> Validate(FluentValidation.Validators.PropertyValidatorContext context)
    {
        var collection = context.PropertyValue as IEnumerable<TBaseClass>;

        // bail out if the property is null or the collection is empty
        if (collection == null) return Enumerable.Empty<ValidationFailure>();
        if (!collection.Any()) return Enumerable.Empty<ValidationFailure>();

        // get the first element out of the collection and check its real type. 
        var actualType = collection.First().GetType();

        IValidator derivedValidator;

        if (derivedValidators.TryGetValue(actualType, out derivedValidator))
        {
            // we found a validator for the specific subclass. 
            var collectionValidtor = new ChildCollectionValidatorAdaptor(derivedValidator);
            return collectionValidtor.Validate(context);
        }

        // Otherwise fall back to the validator for the base class.
        var collectionValidator = new ChildCollectionValidatorAdaptor(baseValidator);
        return collectionValidator.Validate(context);
    }
}

public class ConvidadoModelValidator : AbstractValidator<ConvidadoModel>
{
    public ConvidadoModelValidator()
    {
        RuleFor(m => m.Customer).SetValidator(new CustomerValidator());
        RuleFor(x => x.ShippingAddresses).SetValidator(new PolymorphicCollectionValidator<Address>(new AddressValidator()).Add<ShippingAddress>(new ShippingAddressValidator()));
    }

    public override ValidationResult Validate(ConvidadoModel instance)
    {
        var validationResult = base.Validate(instance);
        foreach (var item in validationResult.Errors)
        {
            instance.AddError(item.PropertyName, item.ErrorMessage);
        }
        return validationResult;
    }
}
The problem only occurs on the client side, the validation messages are not displayed correctly.

Please give me a hint.

Tks.