ChinnaR
ChinnaR

Reputation: 837

Custom validator to reuse in fluent validation

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

Answers (1)

rgvlee
rgvlee

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:

enter image description here

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

Related Questions