Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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();
    }
}
like image 855
Stephen Patten Avatar asked Dec 07 '16 17:12

Stephen Patten


People also ask

How does fluent validation work?

Fluent validations use Fluent interface and lambda expressions to build validation rules. Fluent validation is a free-to-use . NET validation library that helps you make your validations clean and easy to both create and maintain. It even works on external models that you don't have access to, with ease.

Is fluent validation open source?

FluentValidation is an open source validation library for . NET. It supports a fluent API, and leverages lambda expressions to build validation rules.

What is custom validation in Laravel?

Custom Validation Rule Using ClosuresThe function in it is getting 3 values: attribute, value, and fail. The attribute is the field for which the validation is happening. The value corresponds to the actual value of the said object and failure is the callback method that would be executed once the validation fails.


2 Answers

Centralised Extension Methods Approach

I wanted to use them across multiple different types of objects.

I did this by creating centralised extension methods.

A simple example:

Extension Method

namespace FluentValidation
{
    public static class LengthValidator
    {
        public static IRuleBuilderOptions<T, string> 
           CustomerIdLength<T>(this IRuleBuilder<T, string> ruleBuilder)
        {
            return ruleBuilder.Length<T>(1, 0);
        }
    }
}

Usage

public class CreateCustomerValidator : AbstractValidator<CreateCustomerCommand>
{
    public CreateCustomerValidator()
    {
        RuleFor(x => x.CustomerId).CustomerIdLength();
    }
}

As the typed object is passed through with generics it can be used across multiple objects rather than just one i.e.

public class UpdateCustomerValidator : AbstractValidator<UpdateCustomerCommand>

like image 70
hutchonoid Avatar answered Sep 24 '22 14:09

hutchonoid


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.

like image 33
Ouarzy Avatar answered Sep 23 '22 14:09

Ouarzy