Click or drag to resize

Custom validators.

This section explains how to create a custom validator.

Altough the validators that come with the Validation library are suitable in many situations, you may find yourself in a situation where they do not suffice.
To solve this problem you can extend the Validation framework with a custom (reusable)validator. There are several techniques to extend the framework with your validators which are described below.

The IValidator interface.

Each validator must implement the IValidator interface to be recognized by the framework as a validator. The purpose of this interface is to validate an entity context which contains the old and new value of that entity.
The IValidator interface comes in two flavors, a non generic and a type safe generic version.
Both define a single method to validate a context, which contains the old and new state of an entity.
If the context is valid the method returns true; otherwise false. Before the method returns it must register a validation result with the context.

Its the responsibility of the validator to:

  1. Validate the context and return true if its valid, otherwise false.

  2. Create and register a validation result with the context.

IValidator

You could create a validator just by implementing the IValidator interface. However, the Validation library provides you with a Validator base class that does most of the work for you.

Derive from the Validator class.

Deriving from the validator class is a convenient way to create a custom validator. It implements the needed IValidator interface and does the validation result registration for you.
The validator class comes in two flavors, a non generic and generic version. If you know beforehand which type you want to validate it's recommended to use the generic version.

Validator

When deriving from the Validator class it requires you to implement the abstract IsValidAsync() and the Equals() method.
The IsValidAsync() method requires you to at least validate the state of the new entity and return true if it's valid.
The framework will use the Equals() method to determine if two different validator instances are the same.
As an example the code snippet below shows the implementation of a custom validator which checks an age.

C#
public class AgeValidator : Validator<int>
{
  protected readonly int _maxAge;

  protected override bool Equals(IValidator validator)
  {
    // Validator is equal if it is the same type and both have the same max age.
    if (validator is AgeValidator other)
      return this._maxAge == other._maxAge;
    else
      return false;
  }

  protected override Task<bool> IsValidAsync(StateTransition transition, int oldValue, int newValue)
  {
    return Task.Run(() =>
    {
      // Only validating the newValue will suffice.
      return newValue >= 0 && newValue <= _maxAge;
    });
  }

  public AgeValidator(int maxAge)
  {
    // Set the maximum allowed age.
    _maxAge = maxAge;
  }
}

In the above example a class named AgeValidator is defined. The validator will check that an age is equal or greater then 0 and smaller or equal to a certain maximum age.
Since we know on forehand that age is going to be an int the class derives from the generic version of the Validator class.
The maximum allowed age for this validator is passed as a parameter in the constructor. The validator will use this value to validate an age.

The example below shows how you can validate an int with the age validator using the Fluent API.

C#
// Create an instance of the AgeValidator with a maximum age of 100.
AgeValidator validator = new AgeValidator(100);

int age = 100;
// valid becomes true since the given 'age' is not greater then 100
bool valid = await age.Check().On(validator).ValidateAsync();
age = 101;
// valid becomes false since the given 'age' is greater then 100
valid = await age.Check().On(validator).ValidateAsync();
Implementing the IValidator interface.

When you do not wish to derive from the Validator class you can create a custom validator by implementing the IValidator interface.
The IValidator interface comes in two flavors, a non generic and a type safe generic version.
If you decide to implement the generic version of the IValidator interface you need to implement both the non generic and generic versions.
This boils down into implementing the ValidateAsync() method for both interfaces.
The generic version of this method requires a type safe validation context for the ValidateAsync() method.
Since this is not the case for the non generic method you need to make sure that the given context parameter is compatible with the type safe context.

For the framework to be able to determine if two different validator instances are the same you need to override the Equals() method.
If you override the Equals() method you are also required to override the GetHashCode() method.

In the example below we created an age validator by implementing the IValidator<T> interface instead of deriving from the Validator class.

C#
public class AgeIValidator : IValidator<int>
{
  private readonly int _maxAge;

  // Required when overriding Equals()
  public override int GetHashCode()
  {
    return 0;
  }

  // Required so framework can determine if two different validator instances are the same.
  public override bool Equals(object obj)
  {
    if (obj == null)
      return false;
    if (base.Equals(obj))
      return true;

    // Return true if max age is the same
    if (obj is AgeIValidator other)
      return this._maxAge == other._maxAge;
    else
      return false;
  }

  // IValidator implementation
  public Task<bool> ValidateAsync(ValidationContext context)
  {
    if (context == null)
      throw new ArgumentNullException(nameof(context));

    // Delegate to IValidator<int>.ValidateAsync implementation
    if (context is ValidationContext<int> ct)
      return this.ValidateAsync(ct);

    // Check that the given context is compatible with a generic int version.
    ValidationContext<int>.CheckContextCompatibility(context);

    return Task.Run(() =>
    {
      // Only validating the newValue will suffice.
      bool result = (int)context.NewValue >= 0 && (int)context.NewValue <= _maxAge;
      // Register the result to the context
      context.RegisterResult(this, result);
      return result;
    });
  }

  // IValidator<T> implementation
  public Task<bool> ValidateAsync(ValidationContext<int> context)
  {
    if (context == null)
      throw new ArgumentNullException(nameof(context));

    return Task.Run(() =>
    {
      // Only validating the newValue will suffice.
      bool result = context.NewValue >= 0 && context.NewValue <= _maxAge;
      // Register the result to the context
      context.RegisterResult(this, result);
      return result;
    });
  }

  public AgeIValidator(int maxAge)
  {
    // Set the maximum allowed age.
    _maxAge = maxAge;
  }
}

In the example above a class named AgeIValidator is defined which will validate an int.
Since we know on forehand that age is going to be an int the class implements the generic IValidator<int> interface.
The class will check that an age is equal or greater then 0 and smaller or equal to a certain maximum age.
The maximum allowed age for this validator is passed as a parameter in the constructor. The validator will use this value to validate an age.

The example below shows how you can validate an int with the age validator using the Fluent API.

C#
// Create an instance of the AgeIValidator with a maximum age of 100.
AgeIValidator validator = new AgeIValidator(100);

int age = 100;
// valid becomes true since the given 'age' is not greater then 100
bool valid = await age.Check().On(validator).ValidateAsync();
age = 101;
// valid becomes false since the given 'age' is greater then 100
valid = await age.Check().On(validator).ValidateAsync();
The IErrorProvider interface.

When creating a custom validator you have the option to implement the IErrorProvider interface. The purpose of this interface is to provide an error entity for a given validation result.
This interface defines a single method named GetError(). Based uppon a given ValidationResult parameter, this method returns an user defined error entity.
The error entity can be of any type that is meaningful to your application. It isn't restricted to an error message, although you can certainly do that.

IError Provider

In the example below the AgeValidator class from the previous example is extended with the IErrorProvider interface.
The implementation of the GetError() method returns a string that contains an error message. The returned message varies based uppon the maximum age and the validated age.

C#
public class AgeValidatorWithMessage : AgeValidator, IErrorProvider
{

  public object GetError(ValidationResult result)
  {
    return $"Age cant be less then 0 or greater then {_maxAge.ToString()}. You supplied {result.Context.NewValue.ToString()}.";
  }

  public AgeValidatorWithMessage(int maxAge) : base(maxAge)
  {
  }
}

Now each time the validation fails the framework will call the GetError() method of the IErrorProvider interface to retrieve an error entity, in this case a string.
The framework then includes the returned error entity with the validation result.
In the example below this is demonstrated by inspecting the validation results of validating an age.

Note Note

Note that it is not required to return an error entity for each validation result. This can depend on you'r businessrules.

C#
// Create an instance of the AgeValidatorWithMessage with a maximum age of 100.
AgeValidatorWithMessage validator = new AgeValidatorWithMessage(100);

int age = 100;
// results will contain no failures since the given 'age' is not greater then 100
IEnumerable<ValidationResult> results = await age.Check().On(validator).ExecuteAsync();
// Get all error entities from the results that are of type string and take the first. Error will become null.
string error = results.CastErrors<string>().FirstOrDefault(); 

age = 101;
// results will contain a failure since the given 'age' is greater then 100
results = await age.Check().On(validator).ExecuteAsync();
// Error will become "Age cant be less the 0 or greater then 100. You supplied 101."
error = results.CastErrors<string>().FirstOrDefault();