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
Or using the .NET CLI from a terminal window:
dotnet add package FluentValidation
First validator
Imagine that you have a Person
record:
public record Person(string FirstName, string LastName);
📝 Note
With FluentValidation you can use bothclass
andrecord
.
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");
}
}
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);
The Validate
method returns a ValidationResult
object. This contains two properties:
-
IsValid
- a boolean that says whether the validation succeeded. -
Errors
- a collection ofValidationFailure
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
📝 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);
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");
}
}
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());
}
}
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 isnull
, 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
}
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());
}
}
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");
});
}
}
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
TheStop
option is only available in FluentValidation 9.1 and newer. In older versions, you can useStopOnFirstFailure
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());
}
}
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());
}
}
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);
}
}
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);
The ValidateAndThrow
method is an extension method and is equivalent to doing the following:
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:
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
:
PersonValidator validator = new PersonValidator(new WebApiClient());
ValidationResult result = await validator.ValidateAsync(person);
📝 Note
CallingValidateAsync
will run both synchronous and asynchronous rules.⚠️ Warning
If your validator contains asynchronous validators or asynchronous conditions, it is important that you always callValidateAsync
on your validator and never Validate. If you callValidate
, 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);
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.");
}
}
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);
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:
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:
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:
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.");
}
}
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:
public record Person(string FirstName);
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):
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
:
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
:
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.
public record Person(long Id, string FirstName);
public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
RuleFor(person => person.FirstName).NotEmpty();
}
}
[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:
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.
Top comments (2)
Awesome article @fabriziobagala !! Great and simple examples covering all needed for a real world implementation.
Thanks! 👌
I am glad the article was of interest to you @xelit3 😄
Thank you!