Search code examples
c#fluentvalidation

How to organize FluentValidation rules so that they may be reused in multiple validators?


I have a domain model/entity that, depending on what how it's populated needs to be validated differently. Say I come up with 3 validators like the ones below:

public class Product1Validator : AbstractValidator<Ticket>
{
    public Product1Validator()
    {
        RuleFor(ticket => ticket.Policy.PolicyNumber)
         .NotEmpty()
         .WithMessage("Policy Number is missing.");

        RuleFor(ticket => ticket.Policy.ApplSignedInState)
         .NotEmpty()
         .WithMessage("Application Signed In State is missing or invalid.");
    }
}

public class Product2Validator : AbstractValidator<Ticket>
{
    public Product2Validator()
    {
        RuleFor(ticket => ticket.Policy.PolicyNumber)
         .NotEmpty()
         .WithMessage("Policy Number is missing.");

        RuleFor(ticket => ticket.Policy.ApplSignedInState)
         .NotEmpty()
         .WithMessage("Application Signed In State is missing or invalid.");
    }
}


public class Product3Validator : AbstractValidator<Ticket>
{
    public Product3Validator()
    {
        RuleFor(ticket => ticket.Policy.PolicyNumber)
         .NotEmpty()
         .WithMessage("Policy Number is missing.");

        RuleFor(ticket => ticket.Policy.ApplSignedInState)
         .NotEmpty()
         .WithMessage("Application Signed In State is missing or invalid.");

        RuleFor(ticket => ticket.Policy.DistributionChannel)
         .NotEmpty()
         .WithMessage("Distribution Channel is missing."); 
    }
}

How can I refactor the repeated RuleFor(s) so that there are only one of them and are shared by different validators?

Thank you, Stephen

UPDATE

I ran with Ouarzy's idea but when I write the code to validate it won't compile.

[TestMethod]
public void CanChainRules()
{
    var ticket = new Ticket();
    ticket.Policy = new Policy();
    ticket.Policy.ApplSignedInState = "CA";
    ticket.Policy.PolicyNumber = "";
    ticket.Policy.DistributionChannel = null;

    var val = new Product1Validator();
    var result = val.Validate(ticket); //There is no Method 'Validate'
    Assert.IsTrue(!result.IsValid);
    Console.WriteLine(result.Errors.GetValidationText());
} 

UPDATE 2

The problem was that the new composite validators didn't inherit from AbstractValidator, once I corrected this it compiles, but they don't seem to work.

public class Product1Validator : AbstractValidator<Ticket>
{
    public Product1Validator()
    {
        TicketValidator.Validate().Policy().ApplSignedState();
    }
} 

UPDATE 3

After scathing my head about the original answer and reaching out to Jeremy directly on GitHub I came up with the following:

class Program{
    static void Main(string[] args){
        var p = new Person();
        var pv = new PersonValidator();
        var vr = pv.Validate(p);
        //Console.ReadKey();
    }
}

class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime BirthDate { get; set; }
}

class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        CascadeMode = CascadeMode.Continue;
        this.FirstName();
        this.LastName();
    }
}

static class Extensions
{
    public static void FirstName(this AbstractValidator<Person> a)
    {
        a.RuleFor(b => b.FirstName).NotEmpty();
    }
    public static void LastName(this AbstractValidator<Person> a)
    {
        a.RuleFor(b => b.LastName).NotEmpty();
    }
}

Solution

  • In your case, I would probably try to build a fluent validation for the Ticket, with all the rules, and then call the required validation per product. Something like:

    public class TicketValidator : AbstractValidator<Ticket>
    {
        public TicketValidator Policy()
        {
            RuleFor(ticket => ticket.Policy.PolicyNumber)
             .NotEmpty()
             .WithMessage("Policy Number is missing.");
    
            return this;
        }
    
        public TicketValidator ApplSignedState()
        {
            RuleFor(ticket => ticket.Policy.ApplSignedInState)
             .NotEmpty()
             .WithMessage("Application Signed In State is missing or invalid.");
    
            return this;
        }
    
        public TicketValidator DistributionChannel()
        {
            RuleFor(ticket => ticket.Policy.DistributionChannel)
            .NotEmpty()
            .WithMessage("Distribution Channel is missing.");
    
            return this;
        }
    
        public static TicketValidator Validate()
        {
            return new TicketValidator();
        }
    }
    

    And then one validator per product using the fluent syntax:

    public class Product1Validator
    {
        public Product1Validator()
        {
            TicketValidator.Validate().Policy().ApplSignedState();
        }
    }  
    
    public class Product2Validator
    {
        public Product2Validator()
        {
            TicketValidator.Validate().Policy().ApplSignedState();
        }
    }  
    
    public class Product3Validator
    {
        public Product3Validator()
        {
            TicketValidator.Validate().Policy().ApplSignedState().DistributionChannel();
        }
    }  
    

    I didn't try to compile this code, but I hope you see the idea.

    Hope it helps.