FluentValidation: A Validation Library for .NET
Tags: programming, coding, csharp, dotnet
Published: 2023-06-30 15:41:32

FluentValidation is an open-source validation library for .NET. It was developed to provide a more flexible and customizable alternative to the Data Annotations. The library allows you to build validation rules using a fluent interface, which leads to cleaner and more readable code.

It supports .NET and ASP.NET and has extensive integration points, making it suitable for various types of applications.

## Installation

You can install FluentValidation through the NuGet Package Manager:

```bash
Install-Package FluentValidation
```

Or using the .NET CLI from a terminal window:

```bash
dotnet add package FluentValidation
```

## First validator

Imagine that you have a `Person` record:

```csharp
public record Person(string FirstName, string LastName);
```

> 📝 **Note**
> With FluentValidation you can use both `class` and `record`.

You would define a set of validation rules for this record by inheriting from `AbstractValidator<Person>`:

```csharp
using FluentValidation;

public class PersonValidator : AbstractValidator<Person> 
{
    public PersonValidator() 
    {
        RuleFor(person => person.FirstName).NotEmpty().WithMessage("Please specify a first name");
        RuleFor(person => person.LastName).NotEmpty().WithMessage("Please specify a last name");
    }
}
```

To specify a validation rule for a particular property, call the `RuleFor` method, passing a lambda expression that indicates the property that you wish to validate.

To run the validator, instantiate the validator object and call the `Validate` method, passing in the object to validate:

```csharp
Person person = new Person("John", "Doe");
PersonValidator validator = new PersonValidator();
ValidationResult result = validator.Validate(person);
```

The `Validate` method returns a `ValidationResult` object. This contains two properties:

- `IsValid` - a boolean that says whether the validation succeeded.
- `Errors` - a collection of `ValidationFailure` objects containing details about any validation failures.

You can also call `ToString` on the `ValidationResult` to combine all error messages into a single string. By default, the messages will be separated with new lines, but if you want to customize this behaviour you can pass a different separator character to `ToString`.

```csharp
ValidationResult results = validator.Validate(person);
string allErrorMessages = results.ToString(", "); // Prints: Please specify a first name, Please specify a last name
```

> 📝 **Note**
> If there are no validation errors, `ToString()` will return an empty string.

## Complex properties

Validators can be re-used for complex properties. For example, given two record `Person` and `Address`:

```csharp
public record Person(string FirstName, string LastName, Address Address);

public record Address(string Street, string State, string City, string ZipCode);
```

Define an `AddressValidator`:

```csharp
public class AddressValidator : AbstractValidator<Address>
{
    public AddressValidator()
    {
        RuleFor(address => address.Street).NotEmpty().WithMessage("Please specify a street");
        RuleFor(address => address.State).NotEmpty().WithMessage("Please specify a state");
        RuleFor(address => address.City).NotEmpty().WithMessage("Please specify a city");
        RuleFor(address => address.ZipCode).Matches("^\\d{5}$").WithMessage("Invalid zip code");
    }
}
```

Add the `AddressValidator` to the `PersonValidator`:

```csharp
public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(person => person.FirstName).NotEmpty().WithMessage("Please specify a first name");
        RuleFor(person => person.LastName).NotEmpty().WithMessage("Please specify a last name");
        RuleFor(person => person.Address).SetValidator(new AddressValidator());
    }
}
```

Finally, when you call `Validate` on the `PersonValidator`, it goes through the validators defined in both the `PersonValidator` and `AddressValidator` and combines the results into a single `ValidationResult`.

> 📝 **Note**
> If the child property is `null`, then the child validator will not be executed.

## Collections

You can combine `RuleForEach` with `SetValidator` when the collection is of another complex objects. For instance, given the models:

```csharp
public record Person(string FirstName, string LastName, Address Address, List<EmailAddress> EmailAddresses);

public record Address(string Street, string State, string City, string ZipCode);

public record EmailAddress(string Email, EmailType Type);

public enum EmailType
{
    Private,
    Work
}
```

The resulting validators will be:

```csharp
public class EmailAddressValidator : AbstractValidator<EmailAddress>
{
    public EmailAddressValidator()
    {
        RuleFor(emailAddress => emailAddress.Email).EmailAddress().WithMessage("Invalid email");
        RuleFor(emailAddress => emailAddress.Type).IsInEnum().WithMessage("Invalid email type");
    }
}

public class AddressValidator : AbstractValidator<Address>
{
    public AddressValidator()
    {
        RuleFor(address => address.Street).NotEmpty().WithMessage("Please specify a street");
        RuleFor(address => address.State).NotEmpty().WithMessage("Please specify a state");
        RuleFor(address => address.City).NotEmpty().WithMessage("Please specify a city");
        RuleFor(address => address.ZipCode).Matches("^\\d{5}$").WithMessage("Invalid zip code");
    }
}

public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(person => person.FirstName).NotEmpty().WithMessage("Please specify a first name");
        RuleFor(person => person.LastName).NotEmpty().WithMessage("Please specify a last name");
        RuleFor(person => person.Address).SetValidator(new AddressValidator());
        RuleForEach(person => person.EmailAddresses).SetValidator(new EmailAddressValidator());
    }
}
```

Alternatively, as of FluentValidation 8.5, you can also define rules for child collection elements in-line using the `ChildRules` method:

```csharp
public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(person => person.FirstName).NotEmpty().WithMessage("Please specify a first name");
        RuleFor(person => person.LastName).NotEmpty().WithMessage("Please specify a last name");
        RuleFor(person => person.Address).SetValidator(new AddressValidator());
        RuleForEach(person => person.EmailAddresses).ChildRules(emailAddressValidator =>
        {
            emailAddressValidator.RuleFor(emailAddress => emailAddress.Email).EmailAddress().WithMessage("Invalid email");
            emailAddressValidator.RuleFor(emailAddress => emailAddress.Type).IsInEnum().WithMessage("Invalid email type");
        });
    }
}
```

## Cascade mode

Cascade mode is a configuration setting that determines how the library should execute validation rules when one of the rules fails. It checks whether the validation process should continue checking subsequent rules or whether it should stop immediately after encountering the first validation failure. This is particularly useful in scenarios where certain validation rules are contingent upon others or where performance is a critical concern and it is unnecessary to evaluate every rule if one has already failed.

The two cascade modes are:

- `Continue` (the default) - always invokes all rules in a validator class, or all validators in a rule, depending on where it is used.
- `Stop` - stops executing a validator class as soon as a rule fails, or stops executing a rule as soon as a validator fails, depending on where it is used.

> ⚠️ **Warning**
> The `Stop` option is only available in FluentValidation 9.1 and newer. In older versions, you can use `StopOnFirstFailure` instead.

In addition, you can set two different levels:

1️⃣ **ClassLevelCascadeMode**: This is set at the validator class level. It dictates the default behavior for all the rules within that validator class. For example, if you set the cascade mode to `Stop` at the class level, it means that as soon as one validation rule fails for a property, the rest of the rules for that property will not be executed. This behavior will be applied to all the properties and their rules within that validator class unless a specific rule overrides it with its own setting.

```csharp
public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        ClassLevelCascadeMode = CascadeMode.Stop;

        RuleFor(person => person.FirstName).NotEmpty().WithMessage("Please specify a first name");
        RuleFor(person => person.LastName).NotEmpty().WithMessage("Please specify a last name");
        RuleFor(person => person.Address).SetValidator(new AddressValidator());
        RuleForEach(person => person.EmailAddresses).SetValidator(new EmailAddressValidator());
    }
}
```

2️⃣ **RuleLevelCascadeMode**: This is set for an individual validation rule. It overrides the Class Level setting for that particular rule. For instance, if you have set the cascade mode to `Stop` at the class level, but you want a specific rule to continue evaluating subsequent conditions even if one fails, you can set the cascade mode for that particular rule to `Continue`.

```csharp
public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        ClassLevelCascadeMode = CascadeMode.Stop;

        RuleFor(person => person.FirstName).NotEmpty().WithMessage("Please specify a first name");
        RuleFor(person => person.LastName).NotEmpty().WithMessage("Please specify a last name");
        RuleFor(person => person.Address).Cascade(CascadeMode.Continue).SetValidator(new AddressValidator());
        RuleForEach(person => person.EmailAddresses).SetValidator(new EmailAddressValidator());
    }
}
```

## Severity level

Severity level is a way to categorize the importance or criticality of a validation failure. This feature allows you to mark specific validation rules with different levels of severity. This can be helpful in scenarios where you might want to treat different validation failures in distinct ways based on their criticality.

FluentValidation provides an enumeration called `Severity` with the following levels:

- **Error**: This is the default severity level. It indicates that the validation failure is critical, and the data should not be processed further until the issue is resolved.
- **Warning**: This level can be used to indicate that the validation failure is not critical and does not necessarily block further processing. However, it is important enough to warrant attention.
- **Info**: This is the lowest level of severity. It can be used to indicate that the validation failure is more informational and may not require immediate action.

Here’s an example:

```csharp
public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(person => person.FirstName).NotEmpty().WithMessage("Please specify a first name").WithSeverity(Severity.Error);
        RuleFor(person => person.LastName).NotEmpty().WithMessage("Please specify a last name").WithSeverity(Severity.Error);
        RuleFor(person => person.Address).SetValidator(new AddressValidator()).WithSeverity(Severity.Warning);
        RuleForEach(person => person.EmailAddresses).SetValidator(new EmailAddressValidator()).WithSeverity(Severity.Info);
    }
}
```

## Throw exception

Instead of returning a `ValidationResult`, you can alternatively tell FluentValidation to throw an exception if validation fails by using the `ValidateAndThrow` method:

```csharp
Address address = new Address("Street 5", "Austin", "Texas", "73301");
List<EmailAddress> emailAddresses = new List<EmailAddress>
{
    new EmailAddress("john.doe@private.com", EmailType.Private),
    new EmailAddress("john.doe@work.com", EmailType.Work)
};
Person person = new Person("John", "Doe", address, emailAddresses);
PersonValidator validator = new PersonValidator();
validator.ValidateAndThrow(person);
```

The `ValidateAndThrow` method is an extension method and is equivalent to doing the following:

```csharp
validator.Validate(person, options => options.ThrowOnFailures());
```

## Asynchronous validation

Asynchronous validation allows you to perform validation checks that involve asynchronous operations. This is especially useful when you need to perform validation that involves calling external resources such as a database or an API, and you do not want to block the execution thread while waiting for the response.

Let's look at an example that checks whether a person ID is already in use using an external Web API:

```csharp
public class PersonValidator : AbstractValidator<Person> 
{
    WebApiClient _client;

    public PersonValidator(WebApiClient client)
    {
        _client = client;
        
        RuleFor(person => person.Id).MustAsync(async (id, cancellation) => 
        {
            bool exists = await _client.ExistsAsync(id);
            return !exists;
        }).WithMessage("ID must be unique");
    }
}
```

Invoking the validator is essentially the same, but you should now invoke it by calling `ValidateAsync`:

```csharp
PersonValidator validator = new PersonValidator(new WebApiClient());
ValidationResult result = await validator.ValidateAsync(person);
```

> 📝 **Note**
> Calling `ValidateAsync` will run both synchronous and asynchronous rules.

> ⚠️ **Warning**
> If your validator contains asynchronous validators or asynchronous conditions, it is important that you always call `ValidateAsync` on your validator and never Validate. If you call `Validate`, then an exception will be thrown.

As with synchronous operations, you can throw an exception if validation fails rather than return the `ValidationResult`. To do this, you use the asynchronous `ValidateAndThrowAsync` method:

```csharp
PersonValidator validator = new PersonValidator();
await validator.ValidateAndThrowAsync(person);
```

## Custom validators

Creating a custom validator in FluentValidation allows you to encapsulate a validation rule that can be reused in different validators.

Here is how to create custom validator to check whether a string contains only letters and has a certain length:

```csharp
public static class CustomValidators
{
    public static IRuleBuilderOptions<T, string> MustBeLettersAndHaveLength<T>(this IRuleBuilder<T, string> ruleBuilder, int minLength, int maxLength) 
    {
        return ruleBuilder
            .NotEmpty().WithMessage("The field is required.")
            .Length(minLength, maxLength).WithMessage($"The field must be between {minLength} and {maxLength} characters long.")
            .Matches("^[a-zA-Z]+$").WithMessage("The field can only contain letters.");
    }
}
```

This extension method `MustBeLettersAndHaveLength` extends the `IRuleBuilder` interface and takes two additional parameters for minimum and maximum length. It chains the rules for `NotEmpty`, `Length`, and `Matches` for validating that the string is not empty, within a certain length, and contains only letters.

Now, you can use the extension method you just created within the `PersonValidator`:

```csharp
public record Person(string FirstName, string LastName);
```

```csharp
public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(person => person.FirstName).MustBeLettersAndHaveLength(5, 20);
        RuleFor(person => person.LastName).MustBeLettersAndHaveLength(5, 20);
    }
}
```

## Localization

FluentValidation comes ready with translations for its standard validation messages in various languages. Normally, it uses the language that is set in .NET's current user interface settings (`CultureInfo.CurrentUICulture`) to decide which language to display the messages in.

If you are using Visual Studio and taking advantage of its built-in feature for handling .resx files and their strongly-typed counterparts, you can localize a message by using a version of `WithMessage` that accepts a lambda expression:

```csharp
RuleFor(person => person.FirstName).NotNull().WithMessage(x => MyLocalizedMessages.FirstNameRequired);
```

If you are employing `IStringLocalizer` for handling localization, the process is simple. You just need to inject your localizer into your validator, and utilize it inside a `WithMessage` callback, like this:

```csharp
public class PersonValidator : AbstractValidator<Person> 
{
  public PersonValidator(IStringLocalizer<Person> localizer)
   {
    RuleFor(person => person.FirstName).NotNull().WithMessage(x => localizer["FirstName is required"]);
  }
}
```

If you want to replace all (or some) of FluentValidation’s default messages then you can do this by implementing a custom version of the `ILanguageManager` interface.

For example, the default message for the `NotNull` validator is `'{PropertyName}' must not be empty.`. If you wanted to replace this message for all uses of the `NotNull` validator in your application, you could write a custom Language Manager:

```csharp
public class CustomLanguageManager : LanguageManager
{
    public CustomLanguageManager() 
    {
        AddTranslation("en", "NotNullValidator", "'{PropertyName}' is required.");
        AddTranslation("it-IT", "NotNullValidator", "'{PropertyName}' è richiesto.");
        AddTranslation("es-ES", "NotNullValidator", "'{PropertyName}' es necesario.");
    }
}
```

```csharp
CultureInfo cultureInfo = new CultureInfo("es-ES");
Thread.CurrentThread.CurrentUICulture = cultureInfo;

ValidatorOptions.Global.LanguageManager = new CustomLanguageManager();

Person person = new Person(null!, "Doe");
PersonValidator validator = new PersonValidator();
ValidationResult result = validator.Validate(person);
Console.WriteLine(result.ToString()); // Prints: 'First Name' es necesario.
```

## Testing

FluentValidation provides some extensions to assist you in testing your validator classes.

The `TestValidate` extension method can be used to invoke a validator for testing, and then perform assertions on the result. This makes writing tests for validators easier.

For instance, consider the `Person` record and its `PersonValidator` validator:

```csharp
public record Person(string FirstName);
```

```csharp
public class PersonValidator : AbstractValidator<Person>
{
   public PersonValidator()
   {
      RuleFor(person => person.FirstName).NotNull();
   }
}
```

You can verify the functionality of this validator by writing the following tests (using [xUnit](https://xunit.net/)):

```csharp
public class PersonValidatorTests
{
    private readonly PersonValidator _validator;
    
    public PersonValidatorTests()
    {
        _validator = new PersonValidator();
    }

    [Fact]
    public void ShouldHaveValidationError_WhenFirstNameIsNotSpecified()
    {
        // Arrange
        var person = new Person(null!);
        
        // Act
        var result = _validator.TestValidate(person);
        
        // Assert
        result.ShouldHaveValidationErrorFor(p => p.FirstName);
    }

    [Fact]
    public void ShouldNotHaveValidationError_WhenFirstNameIsSpecified()
    {
        // Arrange
        var person = new Person("John");
        
        // Act
        var result = _validator.TestValidate(person);
        
        // Assert
        result.ShouldNotHaveValidationErrorFor(p => p.FirstName);
    }
}
```

There is also an asynchronous `TestValidateAsync` method available which corresponds to the regular `ValidateAsync` method. Usage is similar, except the method returns an awaitable `Task` instead.

## ASP.NET integration

FluentValidation can be applied in ASP.NET web applications to check if the data coming in through models is valid. You can do this in two primary ways:

- Manual validation
- Automatic validation

In manual validation, you add the validator to your controller (or API endpoint), then run the validator and do something based on the outcome. This method is pretty direct and trustworthy.

In automatic validation, FluentValidation connects itself to a built-in system in ASP.NET MVC that checks data. With this, the data in the models is checked even before the controller takes any action (during the time the model is being bound to data).

### Dependency injection

Like any other dependency, we can register our `PersonValidator` validator with the service provider within the `Program.cs` file by calling `AddScoped`:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IValidator<Person>, PersonValidator>();
```

Alternatively, when you need to register multiple validators, you can use the extension method `AddValidatorsFromAssemblyContaining`:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddValidatorsFromAssemblyContaining<PersonValidator>();
```

### Manual validation

Using the manual validation method, you would add the validator into your controller by injecting it, and then use it to check the model.

```csharp
public record Person(long Id, string FirstName);
```

```csharp
public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(person => person.FirstName).NotEmpty();
    }
}
```

```csharp
[ApiController]
[Route("[controller]")]
public class PersonController : ControllerBase
{
    private IValidator<Person> _validator;
    
    public PersonController(IValidator<Person> validator)
    {
        _validator = validator;
    }
    
    [HttpGet("{id}")]
    public IActionResult GetPerson(int id)
    {
        return Ok(new Person(1, "John"));
    }

    [HttpPost]
    public async Task<IActionResult> CreatePerson([FromBody] Person person)
    {
        var validationResult = await _validator.ValidateAsync(person);

        if (!validationResult.IsValid)
        {
            return BadRequest(validationResult.Errors);
        }
        
        var createdPersonId = 1;

        return CreatedAtAction(nameof(GetPerson), new { id = createdPersonId }, person);
    }
}
```

In this example, the `PersonController` class is a web API controller that is designed to handle HTTP requests related to persons.

The controller receives an instance of `IValidator<Person>` through its constructor. This instance, which is stored in the `_validator` field, will be used to validate the data of a `Person` object.

Within the `CreatePerson` method, the `_validator` instance is used to manually validate the `Person` object received in the request. If the object does not pass validation, the method returns a 400 Bad Request response, containing the validation errors.

If the validation passes, it is generally expected that the application would save the `Person` object to a database. However, for simplicity, this example does not include database storage. Instead, it simulates the creation of a new resource and returns a 201 Created HTTP response.

### Automatic validation

To use automatic validaton, you need to call `AddFluentValidationAutoValidation` within the `Program.cs` file.

Here is an example on how you can configure it:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssemblyContaining<PersonValidator>();
```

When you use `AddFluentValidationAutoValidation`, FluentValidation automatically connects to the model binding process in ASP.NET. This means that before a controller action is executed, FluentValidation will automatically perform validation of the incoming model according to the defined rules.

If the validation fails, the framework will automatically return a response with validation errors, without you having to write additional code to handle this scenario.

It is important to note that the AutoValidation feature has some limitations compared to manual validation, such as not supporting asynchronous validation and working only with MVC controllers and Razor Pages. Therefore, you should use it with caution and consider whether it is the best approach for your specific use case.

## Conclusion

FluentValidation is an invaluable tool for developers building .NET applications. Its fluent interface, extensibility, ease of testing, localization support, and integration capabilities make it an ideal choice for implementing validation logic in a clean and maintainable way. Whether you are building a small-scale application or an enterprise-level solution, FluentValidation can play a significant role in ensuring the integrity of your data.

## References

- [FluentValidation documentation](https://docs.fluentvalidation.net/en/latest/index.html)