1
Vote

FluentValidation. Contextual validators & ViewModel Reuse

description

Suppose the following situation:
    // [Validator(typeof(AddressForSupplierValidator))] <-- Cannot put both
    // [Validator(typeof(AddressForCompanyValidator))]  <--
    // But even if I skip both of them it works great for SERVER SIDE
    public class Address 
    {
         public string AddressName { get; set; }
         public string StreetName { get; set; }
    }

    [Validator(typeof(SupplierValidator))]
    public class Supplier 
    {
         public string Name { get; set; }
         public Address Address { get; set; }
    }
    public class SupplierValidator: AbstractValidator<Supplier> 
    {
         public SupplierValidator() {
              RuleFor(x => x.Name).NotEmpty();
              RuleFor(x => x.Address).SetValidator(new AddressForSupplierValidator());
              
              // AddressForSupplierValidator - Requires AddressName to be .NotEmpty()
         }
    }

    [Validator(typeof(CompanyValidator))]
    public class Company 
    {
         public string Name { get; set; }
         public Address Address { get; set; }
    }
    public class CompanyValidator: AbstractValidator<Company> 
    {
         public CompanyValidator() {
              RuleFor(x => x.Name).NotEmpty();
              RuleFor(x => x.Address).SetValidator(new AddressForCompanyValidator());

              // AddressForCompanyValidator - Requires StreetName to be .NotEmpty()
         }
    }
As illustrated Address entity is being used by Company and by Supplier.

Now I need to validate Address depending on its context.

The above solution works for SERVER SIDE only. But for CLIENT SIDE:
It doesn't generate data-val- attributes on HTML elements. This is
because it lacks any [Validator(..)] on top Address class
definition. Adding [Validator(..)] solves the data-val- generation
but breaks the ideea of reusing Address entity and validate it
depending on context

Questions:

  1. Why the absence of [Validator()] ontop of Address breaks CLIENT SIDE validaton, even with .SetValidator()?
  2. Why do we need RuleFor(x => x.Address).SetValidator(..); if anyway the validator must be specified on top of the class ?

Some thoughts:

  1. My belief is that specifying .SetValidator(..) for a nested property should be enough. The requirement with adding [Validator()] breaks the ideea of reusing same ViewModel with many validation rules depending on context.
  2. The author already did great job integrating the client validation. And they work if leaving the ideea of reusing the viewmodel.

Temporary solution:

public class AddressBase
{
    public string AddressName { get; set;}
    public string StreetName { get; set;}
}

[Validator(typeof(AddressForSupplierValidator))]
public class SupplierAddress : AddressBase 
{}

[Validator(typeof(AddressForCompanyValidator))]
public class CompanyAddress : AddressBase 
{}
It works, but again whats the reason for .SetValidator(new AddressForSupplierValidator()); then !? Kind of redundant.

References:

Repost on SO : http://stackoverflow.com/questions/29412824/fluentvalidation-contextual-validators

Related SO question which demonstrates this issue is place at:

http://stackoverflow.com/questions/17238544/unobtrusive-client-validation-data-attributes-are-not-rendered-for-nested-proper

comments

JeremyS wrote Apr 13, 2015 at 8:29 AM

Apologies for not replying sooner - CodePlex has been in read-only mode for the last week so I've been unable to post comments. Because codeplex seems like it's now on its last legs, I've moved the project to github - if you'd like to continue the discussion please post again over there: http://github.com/JeremySkinner/FluentValidation
Why do we need RuleFor(x => x.Address).SetValidator(..); if anyway the validator must be specified on top of the class ?
The [Validator] attribute and SetValidator are different - they don't do the same job. SetValidator is part of FluentValidation's core API, and is the "normal" way of validating a complex child property.

The [Validator] attribute is different - this is a way of telling ASP.NET MVC's built-in validation API which validator should be connected to which class - primarily for the purposes of model-binding.

Usually, you would only put the [Validator] attribute on the top level class (supplier/company) - you would not put it on Address. This way, you're telling MVC's model-binding infrastructure that it should instantiate the CompanyValidator, and then you'd let FluentValidation run through as normal, using SetValidator for the Address.

Client-side validation breaks this - only a small subset of rules are available on the client:
  • NotNull/NotEmpty
  • Matches
  • Range
  • CreditCard
  • Email
  • EqualTo
  • Length
More complicated rules such as SetValidator, Must and the When/Unless conditionals are not supported on client-side. You can work around SetValidator not working on the client as you found by decorating the child validator with a [Validator] attribute, but this is only a workaround - SetValidator is not really supported on the client, only the small collection of rules above.