Creating a new validator for your domain model

There are 2 type of base validator you can inherit to create validator for a domain model: TypeValidator and CompositeValidator
  • TypeValidator: the basic validator for a type that allows you to create rules on properties. However, it will not validate nested object even though the validator type for nested object is registered with ValidatorFactory.
Example:
public class AddressValidator : TypeValidator<Address>
{    
}
  • CompositeValidator: works like TypeValidator. However, it will resolve the default validator for the type of nested complex object (even in the collection) and try to validate the nested object. The validator for nested object needs to be registered as default with ValidatorFactory in order to use this function.
Example:
public class UserValidator : CompositeValidator<User>
{    
}

Adding rules

Both the TypeValidator and CompositeValidator define a method RuleFor that accepts a lambda expression to specify the target property you wish to validate. The RuleFor method can be called after you create an instance of the validator or just simply call it right inside the constructor. Doing that way help you encapsulate all the validation logics inside 1 validator class. Later on, we can utilize the power of some IOC libraries like StructureMap or Autofac to scan all the validator classes in the project assemblies then return the validator for a type depend on different scenarios.

Example:
public class UserValidator : CompositeValidator<User>
{    
     RuleFor(user => user.UserName)
         .NotNull()
         .NotEmpty()
         .Length(6, 10)
         .Must(x => !x.Contains(" ")).WithMessage("@PropertyName cannot contain empty string.");
}
There are quite a lot of common rules supported in NValidator. You are open to implement your custom validators by implementing interface IValidator or IValidator<T> and then make an extension method to make your new validator visible in the rule chain.

Rules for nested object

NValidator supports you register rule for nested object and even nested object inside a collection. The way is just like creating rules for normal property because we just need to provide the lambda experssion:

public class UserValidator : CompositeValidator<User>
{    
     RuleFor(user => user.Address.PostCode)
         .StopOnFirstError()
         .Length(4, 5)
         .Match(@"\d]{4, 5}")
         .AllWithMessage(address => string.Format("'{0}' is not an valid postcode.", address.PostCode));
}

Rules for nested type in a collection

For nested object in a collection, we can do the same way to specify the rule:
Example: given that user can have multiple address. Property Addresses is a array of Address, then we can specify the rule like below:

public class UserValidator : CompositeValidator<User>
{    
     RuleFor(user => user.Address[0].PostCode)
         .StopOnFirstError()
         .Length(4, 5)
         .Match(@"\d]{4, 5}")
         .AllWithMessage(address => string.Format("'{0}' is not an valid postcode.", address.PostCode));

     RuleFor(user => user.Address[1].PostCode)
         .StopOnFirstError()
         .Length(4, 5)
         .Match(@"\d]{4, 5}")
         .AllWithMessage(address => string.Format("'{0}' is not an valid postcode.", address.PostCode));
}

Reuse rules for complex types using SetValidator

It's obviously not convenient to specify rules for every single item in the collection like the above example. In other cases, we may want to reuse a validator class for a type.
For example, we have a AddressValidator and we want to use that validator for the nested property:

public class UserValidator : CompositeValidator<User>
{    
     RuleFor(user => user.Address)
         .SetValidator<AddressValidator>();

     // or

     RuleFor(user => user.Address)
         .SetValidator(new AddressValidator());
}

Reuse rules for complex types using CompositeValidator

If we want to reuse predefined validator class of complex type for a property which is an array or a collection, the CompositeValidator is definitely the good choice. Because the class UserValidator already implements CompositeValidator<User>, the last thing we need to do is registering AddressValidator as the default validator for any Address object. However, if we call SetValidator manually when we adding the rules, the default validator for that property will be ignored.

ValidatorFactory.Current.Register<AddressValidator>(true);
// Or
ValidatorFactory.Current.Register(new AddressValidator(), true);

Manually specify rules for nested type in collection using ForEach

In some case, implement a custom validator is not suitable. For example, we want each string value in a list of string must have the specific length. We can use ForEach extension method to define the rule that will be apply for all item in the collection:

class OrderValidator : TypeValidator<Order>
{
    public OrderValidator()
    {
        RuleFor(x => x.OrderDetails)
            .ForEach(t =>
            {
                t.RuleFor(p => p.Amount).GreaterThan(0);
                t.RuleFor(p => p.Price).GreaterThan(0);
            });
    }
}

Manually specify rules for nested type in collection using RuleForEach

We can do the same thing but using other built-in method of TypeValidator to specify rule for item in an enumerable:

class OrderValidator2 : TypeValidator<Order>
{
    public OrderValidator2()
    {
        RuleForEach(x => x.OrderDetails)
            .Must(i => i.Amount > 0).WithMessage("Amount must be greater than 0.")
            .Must(i => i.Price > 0).WithMessage("Price must be greater than 0.");
    }
}

However, there is one small different. When using RuleForEach, the error message and member name will refer to the item in the collection, not like the ForEach where we can specify rules for deeper nested properties. For example, here are the error messages produce by different methods:
  • Using RuleFor:
    • PropertyName: Amount
    • MemberName: Order.OrderDetails[0].Amount
    • ErrorMessage: _Amount must be greater than 0.
  • Using RuleForEach:
    • PropertyName: OrderDetails[0]
    • MemberName: Order.OrderDetails[0]
    • ErrorMessage: _Amount must be greater than 0.

Specify shared rules for properties with the same data type

For some cases, repeat adding the same rules for some different properties could be annoying. Don't repeat your self :D. You can specify rules for those properties by using RulesFor

public class AddressValidator : TypeValidator<Address>
{
    public AddressValidator()
    {
        RulesFor(x => x.Number, x => x.Street, x => x.Suburb, x => x.PostCode)
            .StopOnFirstError()
            .Not().Null()
            .NotEmpty().When(x => !string.IsNullOrEmpty(x.Country)) // Well the condition might not make sense, this is just an example
            .AllWithMessage(x => "The Address detail is not valid.");
    }
}

This is quite convenient. However, we cannot specify different error messages for different properties. -,-

Last edited Dec 5, 2011 at 9:14 AM by nvthoai, version 11

Comments

No comments yet.