3

The system I'm developing uses FluentValidation (v5.0.0.1).

What I want to do is create several validators that partially validate an object, which I can then combine in other validators depending on what is required at the time.

For example, say my class has name and address. (This can't be split into a separate class like in the examples).

For scenario 1, I want to validate the name only, so I write a name validator class.

For scenario 2, I only want to validate the address, so I write an address validator class.

For scenario 3, I want to validate both the name and the address, so I want to write a new validator class that calls the name validator and then the address validator.

I don't want to repeat the code in different places, which is why I want them separate. I also don't want to use the When operator as there is no way to determine the when from the contents of the object.

I know I can call SetValidator, but how do I write the call?

RuleFor(j=>j).SetValidator(new NameValidator());
RuleFor(j=>j).SetValidator(new AddressValidator());

doesn't work.

Neil
  • 11,059
  • 3
  • 31
  • 56

2 Answers2

3

I will explain the solution with this example. I'm going to validate this Contact entity:

public class Contact
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public string Address1 { get; set; }
    public string Address2 { get; set; }
    public string City { get; set; }
    public string PostalCode { get; set; }
}

The requirement is validate FirstName and LastName then Address1, Address2, City, PostalCode and have the posibility to reuse our validators in other entities.

Create interfaces to define what an specific entity is.

public interface IAmPerson
{
    string FirstName { get; set; }
    string LastName { get; set; }
}

public interface IHaveAddress
{
    string Address1 { get; set; }
    string Address2 { get; set; }
    string City { get; set; }
    string PostalCode { get; set; }
}

Now Contact entity has to implement both interfaces:

public class Contact : IAmPerson, IHaveAddress
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public string Address1 { get; set; }
    public string Address2 { get; set; }
    public string City { get; set; }
    public string PostalCode { get; set; }
}

Then, create the first validator for an IAmPerson entity

public class PersonValidator : AbstractValidator<IAmPerson>
{
    public PersonValidator()
    {
        RuleFor(data => data.FirstName).Length(3, 50).WithMessage("Invalid firstName");
        RuleFor(data => data.LastName).Length(3, 50).WithMessage("Invalid LastName");
    }
}

The second one for IHaveAddress entity

public class AddressValidator : AbstractValidator<IHaveAddress>
{
    public AddressValidator()
    {
        RuleFor(data => data.Address1).NotNull().NotEmpty().WithMessage("Invalid address1");
        RuleFor(data => data.Address2).NotNull().NotEmpty().WithMessage("Invalid address2");
        RuleFor(data => data.City).NotNull().NotEmpty().WithMessage("Invalid City");
        RuleFor(data => data.PostalCode).NotNull().NotEmpty().WithMessage("Invalid PostalCode");
    }
}

Way to use your custom validators

public class ContactValidator: AbstractValidator<Contact>
{
    public ContactValidator()
    {
        RuleFor(contact => contact).SetValidator(new PersonValidator());
        RuleFor(contact => contact).SetValidator(new AddressValidator());
    }
}

Now you can use your validators to validate person data or address data in any other entity. The unique thing you have to do is implement specific interfaces in the entities you are going to validate.

[UPDATE]

You can increase readability of your code by adding extension methods

public static class ValidatorExtensions
{
    public static IRuleBuilderOptions<T, IHaveAddress> MustHaveAValidAddress<T>(this IRuleBuilder<T, IHaveAddress> ruleBuilder) 
    {
        return ruleBuilder.SetValidator(new AddressValidator());
    }

    public static IRuleBuilderOptions<T, IAmPerson> MustBeAValidPerson<T>(this IRuleBuilder<T, IAmPerson> ruleBuilder)
    {
        return ruleBuilder.SetValidator(new PersonValidator());
    }
}

This is the final result using the extension methods I have just added:

RuleFor(contact => contact).MustBeAValidPerson();
RuleFor(contact => contact).MustHaveAValidAddress();
greybeard
  • 2,249
  • 8
  • 30
  • 66
foxhard
  • 496
  • 4
  • 8
  • It's a long time ago when I asked this, but I think this is exactly what the question said I didn't want to do. What if address is multiple lines (address1, address2, town, country, postcode)? I don't want to write a Validator for address1, address2 etc, I just want one for all fields that make up an address. From the bottom of the original question: RuleFor(data=>data).SetValidator (etc) doesn't work. – Neil Sep 13 '15 at 14:44
  • Let me know if I understand well that. Are you trying to validate a dto like this?: 'class Contact { public string Name { get; set; } public string LastName { get; set; } public string Address1 { get; set; } public string Address2 { get; set; } public string Town { get; set; } public string PostCode { get; set; } }' – foxhard Sep 15 '15 at 13:37
  • Yes. And I want to make 2 rules. Rule1 to validate Name & LastName, and Rule2 to validate Address1, Address2, Town & Postcode. Sometimes I want to validate a combination of Rule1 and/or Rule2. – Neil Sep 16 '15 at 14:10
  • @Neil check my updated answer. Let me know if it help to solve your problem. – foxhard Sep 18 '15 at 03:18
  • A very interesting method. – Neil Sep 21 '15 at 10:38
0

If you set a validator on the same type as you have, it will only use the last validator set on the type (in your case the AddressValidator). You can create some methods to encapsulate the validation and use Must.

Note that you won't be able to re-use the same error code or error message across these different validations.

public static class Validations
{
    public static bool ValidateName(string name)
    {
        return name != null; //Or any other validation
    }

    public static bool ValidateAddress(string address)
    {
        return !string.IsNullOrEmpty(address); //Or any other validation
    }
}

Scenario 1

 RuleFor(j=>j.Name).Must(Validations.ValidateName);

Scenario 2

 RuleFor(j=>j.Address).Must(Validations.ValidateAddress);

Scenario 3

 RuleFor(j=>j.Name).Must(Validations.ValidateName);
 RuleFor(j=>j.Address).Must(Validations.ValidateAddress);
bpruitt-goddard
  • 3,174
  • 2
  • 28
  • 34
  • Thanks @bpruitt-goddard. The main reason for wanting to make a separate rule is for the .When and .WithMessage parts. At the moment, I have to repeat these bits (which are sometimes complicated), which is not really DRY. I'm currently trying http://stackoverflow.com/questions/13198471/fluentvalidation-multiple-validators?rq=1 which is OK, but not quite what I wanted – Neil Apr 16 '14 at 07:55