DEV Community

Fabrizio Bagalà
Fabrizio Bagalà

Posted on • Edited on

FluentValidation: A Validation Library for .NET

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:

Install-Package FluentValidation
Enter fullscreen mode Exit fullscreen mode

Or using the .NET CLI from a terminal window:

dotnet add package FluentValidation
Enter fullscreen mode Exit fullscreen mode

First validator

Imagine that you have a Person record:

public record Person(string FirstName, string LastName);
Enter fullscreen mode Exit fullscreen mode

📝 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>:

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

Person person = new Person("John", "Doe");
PersonValidator validator = new PersonValidator();
ValidationResult result = validator.Validate(person);
Enter fullscreen mode Exit fullscreen mode

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.

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

📝 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:

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

public record Address(string Street, string State, string City, string ZipCode);
Enter fullscreen mode Exit fullscreen mode

Define an AddressValidator:

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

Add the AddressValidator to the PersonValidator:

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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
}
Enter fullscreen mode Exit fullscreen mode

The resulting validators will be:

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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");
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Throw exception

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

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);
Enter fullscreen mode Exit fullscreen mode

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

validator.Validate(person, options => options.ThrowOnFailures());
Enter fullscreen mode Exit fullscreen mode

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:

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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

PersonValidator validator = new PersonValidator(new WebApiClient());
ValidationResult result = await validator.ValidateAsync(person);
Enter fullscreen mode Exit fullscreen mode

📝 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:

PersonValidator validator = new PersonValidator();
await validator.ValidateAndThrowAsync(person);
Enter fullscreen mode Exit fullscreen mode

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:

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.");
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

public record Person(string FirstName, string LastName);
Enter fullscreen mode Exit fullscreen mode
public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(person => person.FirstName).MustBeLettersAndHaveLength(5, 20);
        RuleFor(person => person.LastName).MustBeLettersAndHaveLength(5, 20);
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

RuleFor(person => person.FirstName).NotNull().WithMessage(x => MyLocalizedMessages.FirstNameRequired);
Enter fullscreen mode Exit fullscreen mode

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:

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

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:

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.");
    }
}
Enter fullscreen mode Exit fullscreen mode
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.
Enter fullscreen mode Exit fullscreen mode

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:

public record Person(string FirstName);
Enter fullscreen mode Exit fullscreen mode
public class PersonValidator : AbstractValidator<Person>
{
   public PersonValidator()
   {
      RuleFor(person => person.FirstName).NotNull();
   }
}
Enter fullscreen mode Exit fullscreen mode

You can verify the functionality of this validator by writing the following tests (using xUnit):

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IValidator<Person>, PersonValidator>();
Enter fullscreen mode Exit fullscreen mode

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

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddValidatorsFromAssemblyContaining<PersonValidator>();
Enter fullscreen mode Exit fullscreen mode

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.

public record Person(long Id, string FirstName);
Enter fullscreen mode Exit fullscreen mode
public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(person => person.FirstName).NotEmpty();
    }
}
Enter fullscreen mode Exit fullscreen mode
[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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssemblyContaining<PersonValidator>();
Enter fullscreen mode Exit fullscreen mode

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

Top comments (2)

Collapse
 
xaberue profile image
Xavier Abelaira Rueda

Awesome article @fabriziobagala !! Great and simple examples covering all needed for a real world implementation.

Thanks! 👌

Collapse
 
fabriziobagala profile image
Fabrizio Bagalà

I am glad the article was of interest to you @xelit3 😄

Thank you!