Reputation: 6363
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();
}
}
Upvotes: 13
Views: 9986
Reputation: 1091
In my case, simple (built-in type) properties of different objects would have to be validated, so I needed another solution to get rid of duplication.
I have a CreateTodoItemCommand
and an UpdateTodoItemCommand
, both have a string Title
property which should be validated the same way.
My options:
PropertyValidator
because I don't have (and I don't want to create) common contract for only that field ("IHaveTitle" interface)Must
or Custom
validator code would require me to compose the error messages myself which I don't want because it is unnecessary boilerplate.What I came up with is the following extension method:
internal static IRuleBuilderOptions<T, string> ValidateTodoItemTitle<T>(
this IRuleBuilder<T, string> ruleBuilder)
{
return ruleBuilder
.NotEmpty()
.MaximumLength(5000);
}
And use it like RuleFor(x => x.Title).ValidateTodoItemTitle();
.
The only downside I see is that I created an extension method on a common type could pollute the type, but it shouldn't be a problem because the extension class is in its own namespace which is used only at validation.
This can be different for people using e.g. ReSharper which may show opportunities from unimported namespaces, causing real pollution. For this reason, it would be nice to have namespace-scoped visibility in C#.
For people who are bothered using an extension method, use a regular method, and the usage becomes like this: CommonTodoItemValidators.ValidateTodoItemTitle(RuleFor(x => x.Title));
. It's readability can also be improved by e.g. using static
.
Upvotes: 3
Reputation: 33306
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>
Upvotes: 21
Reputation: 3043
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.
Upvotes: 6