TL;DR

  • Fluent Validation is a powerful library for validating data in .NET applications, offering more flexibility and testability than the built-in Data Annotations.
  • Key benefits of Fluent Validation:
  • Validation logic is separated from the model, following the Separation of Concerns principle.
  • Allows for more complex validations, including dependent validations and custom validations.
  • Makes unit testing validation logic easier.
  • Validation rules are defined in a fluent, readable syntax using the RuleFor method.
  • Validation messages can be customized and use placeholders for dynamic values.
  • Validation execution can be controlled using the Cascade method to stop at the first error.
  • Validation rules can be organized into separate classes and included in the main validator.
  • Fluent Validation provides a great alternative to Data Annotations when you need more flexibility and maintainability in your validation logic.
  • Visit Fluent Validation to learn more.

As developers, we are well aware of the importance of validating the data that users enter into our application, whether it's through a form in an application or a request to our API.

Experience has taught us that we can't trust the data entered by users - those mischievous beings who will eventually find that use case you hadn't considered; or worse, the QA team, who will undoubtedly use the limit values and beyond to test the robustness of our software.

Fluent Validation vs. Data Annotations

Surely, when you read the title of this article, your first thought was: "My models are already validated and limited with Data Annotations. What would using Fluent Validation add for me?"

Data Annotations is perfectly valid and provides simplicity when validating your models, but in my opinion, it has certain shortcomings and drawbacks that you won't find in Fluent Validation.

One of these drawbacks is that from the moment you use Data Annotations with your model, you're creating a dependency between your validation rules and your model, thus violating one of the SOLID principles: Separation of Concerns.

Another "but" is that the [Required] attribute, for example, doesn't give you much flexibility on the validation. How would you do if a property is required based on another? Or if you need to apply some logic to know that the field is correct? With Fluent Validation, we'll see how to do this in a very simple way.

And finally, the unit tests of your model with Data Annotations are more complex than with Fluent Validation, where your validation is a separate and fully testable class.

The idea of this article is to show you the most notable features and advantages of using Fluent Validation. We won't see all its potential or features, but I hope it serves as a solid foundation to start using it.

Creating our test project

To illustrate the different possibilities of Fluent Validation, we'll create a simple desktop application that will allow us to register users in our car insurance company.

Once the project is created, the first thing will be to import the corresponding NuGet package. If your project is a web project of type ASP.Net Core, you should select the FluentValidation.AspNetCore package, for the rest of the projects FluentValidation will fit what you need.

Let's start with the important part and create the data model of the users to be validated. In this case, we'll have the name of the insured, their last name, date of birth, whether they have a car, and if so, the license plate of the car.

public class UserModel
{
    public string Name { get; set; }
    public string LastName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool HasCar { get; set; }
    public string PlateNumber { get; set; }
}

And finally, a form from which to register the clients and that will show us the validation errors that will occur.

With this, we already have the basis to start making our first validations and "tinker" a bit with Fluent Validation.

A simple example

Validations with Fluent Validation are created in a class that inherits from AbstractValidator, where T is the type of the class we want to validate.

using FluentValidation;

public class UserValidator : AbstractValidator<UserModel>
{
    public UserValidator()
    {
        RuleFor(user => user.Name).NotEmpty();
    }
}

As you can see, using a fluent syntax, the method is very easy to read. At a glance and without knowing anything about Fluent Validation, we know which property we are validating and what validation is being applied.

NotEmpty is just one of the many validations that Fluent Validation has predefined for us. We'll see some more in this article, but if you want to see the complete list of validations, you can check them here.

Let's expand our validation a bit. Just by looking at the code, what do you think we're validating now?

using FluentValidation;

public class UserValidator : AbstractValidator<UserModel>
{
    public UserValidator()
    {
        RuleFor(user => user.Name).NotEmpty().Length(2, 50);
    }
}

It's insultingly simple. The name of our user cannot be empty and must be between 2 and 50 characters long.

With this, I wanted to show you how Fluent Validation allows us to chain as many validation rules for a single property as we want.

Calling our validation

To execute the validation, we'll invoke the Validate method that we inherit in our UserValidator class. To do this, in our example, we'll add the Click event to the "Validate" button in our form with the following code:

private void ValidateButton_Click(object sender, EventArgs e) { // Clear previous validation error messages ErrorTextBox.Clear();

var user = new UserModel
{
    Name = NameTextBox.Text,
    LastName = LastnameTextBox.Text,
    HasCar = HasCarCheckBox.Checked,
    PlateNumber = PlateTextBox.Text
};

var validator = new UserValidator();
ValidationResult result = validator.Validate(user);

if (result.IsValid)
{
    ErrorTextBox.AppendText("All correct");
}
else
{
    // Include all validation errors in our error text box
    foreach (var error in result.Errors)
    {
        ErrorTextBox.AppendText(error.ErrorMessage);
        ErrorTextBox.AppendText(Environment.NewLine);
    }
}

}

The really important thing in this code, and what you should mainly focus on, is the call to the Validate method and the response of the method, which is a ValidationResult object.

The ValidationResult tells us if the instance of our model that we pass to it meets all our rules (IsValid) or, if not, a list with the validation errors.

Customizing response messages

If you don't want to use the predefined response messages from Fluent Validation, you can customize the response message with the WithMessage method with the desired text:

RuleFor(user => user.Name)  
    .NotEmpty().WithMessage("You didn't enter the user's name.")
    .Length(2,50).WithMessage("The name must be between 2 and 50 characters long");

Notice that we've repeated the call to the .WithMessage() method for each of the validations we're doing. If we only wanted to use a single error message, we could just put it at the end of all the validations.

Let's do the same test as before and see how it has changed.

Example

We can go a bit further and customize our messages using placeholders within the text.

Some of these placeholders are:

  • {PropertyName} which contains the name of the property being validated.
  • {PropertyValue} which contains the value of the property being validated.
  • {MinLength} which, in Length type validations, indicates the minimum number of characters to be entered.
  • {MaxLength} which, in Length type validations, indicates the maximum number of characters to be entered.
  • {TotalLength} which, in Length type validations, indicates the number of characters entered.

If you want to see the complete list, you can do so here.

Let's use these placeholders by modifying our error message for the name length validation:

RuleFor(user => user.Name)  
    .NotEmpty().WithMessage("You have not provided a username.")
    .Length(2,50).WithMessage("{PropertyName} has {TotalLength} letters. It must have a length between {MinLength} and {MaxLength} letters.");

If we try to break this validation by entering only one character in the name, the result will be something like this:

Example

The advantage of using these placeholders is that they prevent us from making errors by omission. In other words, if we changed the allowed range for the name to 5 and 100, we wouldn't have to change the error text because it takes those values directly from the rule.

Cascading validations

With our current code, if we don't enter the user's name, it returns all the error messages for the rules for that property. But could we make it only show the first message? We can, using the Cascade method.

Cascade allows us to indicate the behavior we want for our rules by passing one of the two allowed values:

  • CascadeMode.Continue => All rules are validated and all errors are returned (default value).
  • CascadeMode.Stop => The validation stops at the first error produced.

To make our application only show the first message, we'd need to change our rule to this:

RuleFor(user => user.Name)
    .Cascade(CascadeMode.Stop)
    .NotEmpty()
    .WithMessage("You didn't enter the user's name.")
    .Length(2, 50)
    .WithMessage("The name must be between {MinLength} and {MaxLength} characters long.");

This way, as soon as the first validation rule we have defined is not met, the validation stops and the error message is returned.

Dependent validations

One of the things I consider most interesting about Fluent Validation is the ability to apply rules based on other properties.

For example, we can compare two properties to prevent them from being the same or, on the contrary, to ensure they are the same, as when we confirm a password when registering on a website. To do this, we'll use another predefined rule: NotEqual.

Let's see an example of this rule in our application, validating that the user's name and last name are not the same.

RuleFor(user => user.Name).NotEqual(user => user.LastName);

In this way, we will show a message whenever the name and last name are the same, preventing Jar Jar from registering in our application.

Jar Jar Binks

Sorry, Jar.Jar. Not in our program.

Another very interesting use of dependent validations between properties is the fact that you can apply or not apply a rule based on the value of another property.

For this, we have two methods: When and Unless.

If you remember, one of the requirements of our application was that if we checked the 'Has car' option, we had to indicate the user's car license plate.

We can achieve this using the When method like this:

RuleFor(user => user.PlateNumber).Length(7, 12).When(user => user.HasCar);

In a very simple way, we've created a rule to validate that the license plate has between 7 and 12 characters only if the user has checked the 'Has car' option (HasCar).

Extending our validations

So far, we've been able to see some of the predefined validations of Fluent Validation and how to link them between properties. But what if none of the predefined validations meet your needs?

For these cases, we can create custom validations. There are several ways to create custom validations, but the simplest is using the Must method. Let's see an example:

List<string> blackListWords = new List<string> { "crap", "butt", "fart", "pee" };
RuleFor(user => user.LastName).Must(lastname => !blackListWords.Contains(lastname));

With this example, we've implemented a rule to prevent any troll user from trying to enter some text for the last name that we consider inappropriate and have in our blacklist.

Another interesting use of the Must method is the ability to pass it a function with our validation so that our code is cleaner and we can reuse a validation in another property. As an example, let's see how to tell our system that only those over 18 can register.

public class UserValidator : AbstractValidator<UserModel>
{
    public UserValidator()
    {
        RuleFor(user => user.BirthDate)
            .Must(IsOver18)
            .WithMessage("You must be of legal age to register.");
    }

    private bool IsOver18(DateTime birthDate)
    {
        return DateTime.Now.AddYears(-18) >= birthDate;
    }
}

Grouping validations

I hope that by now you know how to validate your model with the predefined rules, create your own validations if you need them, and display appropriate error messages, both generic and customized. If we review our code, it should be something like this:

public class UserValidator : AbstractValidator<UserModel>
{
    public UserValidator()
    {
        Include(new UserNameIsSpecified());
        Include(new LastNameDistinctThanName());
        Include(new PlateNumberSpecifiedIfHasCar());
        Include(new LastNameIsNotBlacklisted());
        Include(new UserIsOver18());
    }
}

public class UserNameIsSpecified : AbstractValidator<UserModel>
{
    public UserNameIsSpecified()
    {
        RuleFor(user => user.Name)
            .Cascade(CascadeMode.Stop)
            .NotEmpty().WithMessage("You didn't enter the user's name.")
            .Length(2, 50).WithMessage("The name has {TotalLength} characters. It must be between {MinLength} and {MaxLength} characters long.");
    }
}

public class LastNameDistinctThanName : AbstractValidator<UserModel>
{
    public LastNameDistinctThanName()
    {
        RuleFor(user => user.Name).NotEqual(user => user.LastName);
    }
}

public class PlateNumberSpecifiedIfHasCar : AbstractValidator<UserModel>
{
    public PlateNumberSpecifiedIfHasCar()
    {
        RuleFor(user => user.PlateNumber).Length(7, 12).When(user => user.HasCar);
    }
}

public class LastNameIsNotBlacklisted : AbstractValidator<UserModel>
{
    public LastNameIsNotBlacklisted()
    {
        List<string> blackListWords = new List<string> { "crap", "butt", "fart", "pee" };
        RuleFor(user => user.LastName).Must(name => !blackListWords.Contains(name));
    }
}

public class UserIsOver18 : AbstractValidator<UserModel>
{
    public UserIsOver18()
    {
        RuleFor(user => user.BirthDate)
            .Must(IsOver18)
            .WithMessage("You must be of legal age to register.");
    }

    private bool IsOver18(DateTime birthDate)
    {
        return DateTime.Now.AddYears(-18) >= birthDate;
    }
}

The key is the Include method that allows you to group the rules you need and keep your code clean and well-structured.

Conclusion

I hope this introduction to Fluent Validation serves as a base and helps you weigh whether to use it in your projects or not.

As I said at the beginning of the article, I believe that having your validations completely independent of the data model is a very important point to consider when choosing it, as well as the ability to use a fluent syntax to create your rules in a simple and very versatile way.

Regarding using Data Annotations or Fluent Validation, it depends on your needs and requirements. Both have their pros and cons.

With Data Annotations, when you access your model, you can see the rules that apply to that property in a simple way, while in Fluent Validation, you would need to navigate through the different created classes.

On the other hand, if your validations are complex or require some logic, Fluent Validation becomes a great ally, greatly facilitating your life, while with Data Annotations, the attributes to apply to the property would pile up, dirtying your code, and even then, you would probably have to code the validations you need.

I've left some things out to avoid extending this article too much, and also because the main objective was to serve as an introduction to Fluent Validation, but I encourage you to consult the official page to discover the rest of the possibilities, such as the use of resource files for error messages, validations with regular expressions, or how to invoke the validations using dependency injection.

If I've sparked your interest, visit Fluent Validation to learn more and stay up to date with the latest versions. And if you want to download the application we've been working on, it's available in my github repository here.