Reputation: 837
I would like to implement fluent validation without repetition of validating same properties. I am looking for a way so I can reuse validation.
I have three classes as below, both Customer and NewClass are same except that NewClass inherits PageRequest.
public sealed class Customer {
public int Id{get; set;}
public string FirstName {get; set;}
public string LastName {get; set;}
public string MiddleName {get; set;}
public string Address {get; set;}
}
public class PageRequest
{
public int CurrentPage {get; set;}
public int PerPage {get; set;}
public string SortBy {get; set;}
}
public class NewClass : PageRequest
{
public int Id{get; set;}
public string FirstName {get; set;}
public string LastName {get; set;}
public string MiddleName {get; set;}
public string Address {get; set;}
}
Fluent validations are as below:
public abstract class GetPaginatedDataRequestValidator<TRequest, TModel> : AbstractValidator<TRequest>
where TRequest : PageRequest
{
protected GetPaginatedDataRequestValidator()
{
var properties = typeof(TModel).GetProperties().Select(x => x.Name).ToList();
RuleFor(x => x.CurrentPage).Required().GreaterThan(0);
RuleFor(x => x.PerPage).Required().GreaterThan(0);
RuleFor(x => x.SortBy)
.Must(x => properties.Contains(x, StringComparer.OrdinalIgnoreCase))
.When(x => !string.IsNullOrEmpty(x.SortBy))
.WithMessage("{PropertyName} must be a known property name of a " + typeof(TModel).Name.Humanize());
}
}
public class NewClassValidator : GetPaginatedDataRequestValidator<
NewClass, SomeDto>
{
public NewClassValidator()
{
const string message = "At least one of either first name, last name, address and postcode are required.";
RuleFor(x => x.FirstName)
.Required()
.When(x => string.IsNullOrEmpty(x.LastName)
&& string.IsNullOrEmpty(x.Address) && string.IsNullOrEmpty(x.PostCode))
.WithMessage(message);
RuleFor(x => x.LastName)
.Required()
.When(x => string.IsNullOrEmpty(x.FirstName)
&& string.IsNullOrEmpty(x.Address) && string.IsNullOrEmpty(x.PostCode))
.WithMessage(message);
RuleFor(x => x.Address)
.Required()
.When(x => string.IsNullOrEmpty(x.LastName)
&& string.IsNullOrEmpty(x.FirstName) && string.IsNullOrEmpty(x.PostCode))
.WithMessage(message);
RuleFor(x => x.PostCode)
.Required()
.When(x => string.IsNullOrEmpty(x.LastName)
&& string.IsNullOrEmpty(x.Address) && string.IsNullOrEmpty(x.FirstName))
.WithMessage(message);
}
}
public class CustomerValidator : AbstractValidator<Customer>
{
public CustomerValidator()
{
const string message = "At least one of either first name, last name, address and postcode are required.";
RuleFor(x => x.FirstName)
.Required()
.When(x => string.IsNullOrEmpty(x.LastName)
&& string.IsNullOrEmpty(x.Address) && string.IsNullOrEmpty(x.PostCode))
.WithMessage(message);
RuleFor(x => x.LastName)
.Required()
.When(x => string.IsNullOrEmpty(x.FirstName)
&& string.IsNullOrEmpty(x.Address) && string.IsNullOrEmpty(x.PostCode))
.WithMessage(message);
RuleFor(x => x.Address)
.Required()
.When(x => string.IsNullOrEmpty(x.LastName)
&& string.IsNullOrEmpty(x.FirstName) && string.IsNullOrEmpty(x.PostCode))
.WithMessage(message);
RuleFor(x => x.PostCode)
.Required()
.When(x => string.IsNullOrEmpty(x.LastName)
&& string.IsNullOrEmpty(x.Address) && string.IsNullOrEmpty(x.FirstName))
.WithMessage(message);
}
}
You could see the same property validations in both customer and newclass validators. Is there anyway I could create a custom validator and reuse in both both Customer and NewClass validators? Is any modification to class structure is required as properties are repeated?
Upvotes: 1
Views: 2655
Reputation: 3213
You may be able to unify the Customer and NewClass classes by way of an interface (ICustomer or similar), write a validator for the interface, then include that validator in Customer/NewClass validators.
Edit: MVP LINQPad sample
void Main()
{
var customerValidator = new CustomerValidator();
var customer1 = new Customer();
var customerResult1 = customerValidator.Validate(customer1);
Console.WriteLine(customerResult1.Errors.Select(x => x.ErrorMessage));
var newClassValidator = new NewClassValidator();
var newClass1 = new NewClass { CurrentPage = -1, PerPage = -1, SortBy = "Foo" };
var newClassResult1 = newClassValidator.Validate(newClass1);
Console.WriteLine(newClassResult1.Errors.Select(x => x.ErrorMessage));
}
public interface ICustomer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string MiddleName { get; set; }
public string Address { get; set; }
public string PostCode { get; set; }
}
public sealed class Customer : ICustomer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string MiddleName { get; set; }
public string Address { get; set; }
public string PostCode { get; set; }
}
public class PageRequest
{
public int CurrentPage { get; set; }
public int PerPage { get; set; }
public string SortBy { get; set; }
}
public class NewClass : PageRequest, ICustomer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string MiddleName { get; set; }
public string Address { get; set; }
public string PostCode { get; set; }
}
public class SomeDto
{
}
public class ICustomerValidator : AbstractValidator<ICustomer>
{
public ICustomerValidator()
{
const string message = "At least one of either first name, last name, address and postcode are required.";
RuleFor(x => x.FirstName)
.NotEmpty()
.When(x => string.IsNullOrEmpty(x.LastName) && string.IsNullOrEmpty(x.Address) && string.IsNullOrEmpty(x.PostCode))
.WithMessage(message);
RuleFor(x => x.LastName)
.NotEmpty()
.When(x => string.IsNullOrEmpty(x.FirstName) && string.IsNullOrEmpty(x.Address) && string.IsNullOrEmpty(x.PostCode))
.WithMessage(message);
RuleFor(x => x.Address)
.NotEmpty()
.When(x => string.IsNullOrEmpty(x.LastName) && string.IsNullOrEmpty(x.FirstName) && string.IsNullOrEmpty(x.PostCode))
.WithMessage(message);
RuleFor(x => x.PostCode)
.NotEmpty()
.When(x => string.IsNullOrEmpty(x.LastName) && string.IsNullOrEmpty(x.Address) && string.IsNullOrEmpty(x.FirstName))
.WithMessage(message);
}
}
public class CustomerValidator : AbstractValidator<Customer>
{
public CustomerValidator()
{
Include(new ICustomerValidator());
}
}
public abstract class GetPaginatedDataRequestValidator<TRequest, TModel> : AbstractValidator<TRequest> where TRequest : PageRequest
{
protected GetPaginatedDataRequestValidator()
{
var properties = typeof(TModel).GetProperties().Select(x => x.Name).ToList();
RuleFor(x => x.CurrentPage).GreaterThan(0);
RuleFor(x => x.PerPage).GreaterThan(0);
RuleFor(x => x.SortBy)
.Must(x => properties.Contains(x, StringComparer.OrdinalIgnoreCase))
.When(x => !string.IsNullOrEmpty(x.SortBy))
//.WithMessage("{PropertyName} must be a known property name of a " + typeof(TModel).Name.Humanize());
.WithMessage("{PropertyName} must be a known property name of a " + typeof(TModel).Name);
}
}
public class NewClassValidator : GetPaginatedDataRequestValidator<NewClass, SomeDto>
{
public NewClassValidator()
{
Include(new ICustomerValidator());
}
}
Result:
This is using your definitions; I've added PostCode
as the validators referenced it and changed the Required
validators (removed if the properties cannot be null (e.g., int) or, for strings, replaced with NotEmpty
as Required
isn't a built-in validator).
Upvotes: 2