Validation


1. About

In order to validate user input, you need to create validators. Validators are lambdas that return a boolean with a success status and a message of the reason to fail. They could be represent by:

(T e) => (bool, string?)

2. Validator<T>

In order to allow Generics usage and more flexibility, validators are build from the class Validator<T>. And by default all validators are treated as a Task.

A validator that check if the user inputted an empty text, could be written as:

var validator = new Validator<string>(e => (!string.IsNullOrEmpty(e), "Can't be empty"))

Async validators are also accepted, actually all validators are treated as async. An async validator could be written as:

var validator = new Validator<string>(async e => (await LongCall(e), "LongCall failed to validate input"))

3. Exceptions

Sometimes returning a message for each error in a long validator could be laborious, so it's possible to write a method that report errors by throwing an Exception with a custom message. By default all exception are handled, but if the Exception ValidatorException is thrown the full message is shown instead of a default one.

Example output:

  • throw new Exception("Invalid length") => "Error validating input: Invalid Length"
  • throw new ValidatorException("Invalid length") => "Invalid Length"

4. Collection

The best way to represent a series of validations is using the ValidatorCollection<T>, it allows to aggregate multiple validators and when validates an Input it keeps the insert order.

Collections are also the main way to run a validation, which mean that most interaction is done through this object.

An example of a collection that could check an input password:

var validators = ValidatorCollection
    .Create<string>()
    .Add(e => (e.Length >= 8, "Length must be at least 8"))
    .Add(e => (e.Length < 64, "Length must be less than 64"))
    .Add(async e => (await CheckPwnedPassword(e), "Password has appear in a data breach"));

var (valid, errorMessage) = validators.ValidateInput(password);

5. ValidatorProvider

A ValidatorProvider is a way to reference a ValidatorCollection<T> by name, which allow to call validation for an input without even knowing the type. It also allow to use Enum as a name and Flags to aggregate multiple collection. Every time a provider is required and none is available, the default ValidatorProvider.Global is used.

Register a collection and validate inputs:

var validators = ValidatorCollection
    .Create<string>()
    .Add(e => (e.Length >= 8, "Length must be at least 8"))
    .Add(e => (e.Length < 64, "Length must be less than 64"))
    .Add(async e => (await CheckPwnedPassword(e), "Password has appear in a data breach"));

ValidatorProvider.Global.Register<string>("PASSWORD", validators);

// It can be used in any moment by
var (valid, errorMessage) = await ValidatorProvider.Global.Validate("PASSWORD", password);

Using Enums:

[Flags]
enum StringValidator {
    NotEmpty = 1,
    MinLength3 = 2,
    MinLength7 = 4,
    Lowercase = 8,
}

ValidatorProvider.Global.Register(
    StringValidator.NotEmpty,
    ValidatorCollection.Create<string>()
        .Add(e => (!string.IsNullOrEmpty(e), "Can't be empty")));

ValidatorProvider.Global.Register(
    StringValidator.MinLength3,
    ValidatorCollection.Create<string>()
        .Add(e => (e.Length >= 3, "Length must be at least 3")));

ValidatorProvider.Global.Register(
    StringValidator.MinLength7,
    ValidatorCollection.Create<string>()
        .Add(e => (e.Length >= 7, "Length must be at least 7")));

ValidatorProvider.Global.Register(
    StringValidator.Lowercase,
    ValidatorCollection.Create<string>()
        .Add(e => (e == e.ToLower(), "Must be in lowercase")));

var validator = StringValidator.MinLength7 | StringValidator.Lowercase;

// Check that input has min length 7 and is in lowercase
var (valid, errorMessage) = await ValidatorProvider.Global.Validate(validator, input);