Upgrade your validation logic in ASP.NET Core with FluentValidation

Published 12/18/2020 9:12:32 AM
Filed under ASP.NET

Every web application or micro-service that you build will have some form of validation. I've been using the standard validation attributes in ASP.NET core for ages. And I've also been cursing at them for ages. Luckily, there's a better way!

In this post we're going to explore FluentValidation. A validation library that you can use in .NET Core and .NET 5 for validating the input for your application.

We'll cover the following topics:

  • Installing FluentValidation in your web app
  • Building a basic validator
  • Advanced validation scenarios

Let's get started by installing FluentValidation.

Installing FluentValidation in your web app

Although FluentValidation works with every type of .NET project, We're going to focus on using it in ASP.NET Core.

FluentValidation has a number of different packages, here are the most important ones you should know about:

  • FluentValidation - The core package that you can use anywhere
  • FluentValidation.AspNetCore - The package for ASP.NET Core

You can install FluentValidation as a NuGet package in your project. There's different options for installing nuget packages. For this post we'll use the command-line tooling of .NET.

Execute the following command in the folder where your ASP.NET Core project is located:

dotnet add package FluentValidation.AspNetCore
The command to install FluentValidation in an ASP.NET Core application.

After installing the package, we have to open up the Startup.cs file and add the following code to the ConfigureServices method:

public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
    services
      .AddControllers()
      .AddFluentValidation(x =>
      {
          x.RegisterValidatorsFromAssembly(typeof(Startup).Assembly);
      });
  }
  
  // ...
}
Code to add in Startup.cs

This registers  the controllers functionality in our application and sets up the fluent validation logic. We're using RegisterValidatorsFromAssembly to automatically register any validators that we may have in the application.

Note: Although we're using AddControllers, the same registration of fluent validation also works in combination with AddRazorPages and AddControllersWithViews.

Now that we have set up FluentValidation, let's take a look at building a basic validator.

Building a basic validator

FluentValidation is based on a class called the Validator. The Validator class encapsulates the validation logic of a single object that we want to validate.

Before we can start building a validator, we need to have something to validate. Let's build a basic controller with some input that we need to validate.

[ApiController]
[Route("/api/cakes")]
public class CakesController: ControllerBase    
{
    private readonly IProductCatalogService _productCatalogService;
    
    [HttpPost]
    [Route("")]
    public async Task<ActionResult<Cake>> CreateAsync(CreateCakeCommand cmd)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        
        var result = await _productCatalogService.CreateCake(cmd);

        return Ok(result.Cake);
    }
}

public class CreateCakeCommand
{
    public string Name { get; set; }
    public string Description { get; set; }
}
The application code that we're going to work on.

The controller has a method CreateAsync that accepts a CreateCakeCommand. Before doing anything with the input, it makes sure that the ModelState is valid. If it's invalid we'll let the user know. Otherwise, we're creating the new cake in the catalog.

We're assuming here that validation happens automatically. This is the case for validation attributes that come with ASP.NET Core. However, since we've registered FluentValidation, the same is true for the validators introduced by FluentValidation.

Let's take a look at a basic validator for the CreateCakeCommand.

public class CreateCakeCommandValidator: AbstractValidator<CreateCakeCommand>
{
    public CreateCakeCommandValidator()
    {
        RuleFor(x => x.Name).NotEmpty();
        RuleFor(x => x.Description).NotEmpty();
    }
}
The basic implementation of the validator.

A validator derives from the AbstractValidator<T> class. It needs a generic argument which is the class that you want to validate: CreateCakeCommand.

In the constructor of the class we can specify one or more rules for properties. As an example we've setup rules for the Name and Description property.

Note: There are a number of default rules available. You can find them in the documentation: https://docs.fluentvalidation.net/en/latest/built-in-validators.html

Once we have your validator class ready, we're done. When data comes into the application, the validator is automatically called and the results are mapped to the ModelState property.

Now that we have basic validation set up, let's take a look at some of the more advanced validation scenarios that you may run into.

Advanced validation scenarios

There are quite a few interesting scenarios that FluentValidation supports. Here are a couple of interesting ones:

  • Custom validations that require a database or service
  • Validation rules that depend on other validation rules
  • Validation of child collections

Let's take a look at each of these scenarios and discover how they're handled.

Custom validations that require a database or service

Sometimes we want to make sure that a particular value in the input is or isn't in the database. For example, if we let the user specify a category for a cake in the sample application, we want to make sure that the category exists.

Let's extend the validator to support this:

public class CreateCakeCommandValidator: AbstractValidator<CreateCakeCommand>
{
	private readonly ICategoryRepository _categoryRepository;

    public CreateCakeCommandValidator(ICategoryRepository categoryRepository)
    {
    	_categoryRepository = categoryRepository;
    
        RuleFor(x => x.Name).NotEmpty();
        RuleFor(x => x.Description).NotEmpty();
        RuleFor(x => x.CategoryId)
            .Must(BeExistingCategory)
            .WithMessage("Invalid category specified.");
    }
    
    private bool BeExistingCategory(int categoryId)
    {
    	return _categoryRepository.CategoryExists(categoryId);
    }
}
Extended implementation for custom validations.

Instead of using one of the standard rules, we're specifying a custom validation method. This method accepts the value of the property and returns a boolean.

We can now use other components such as a ICategoryRepository to call into our database to make sure that the data is valid.

But what if we wanted to make sure that the categoryId is at least higher than zero before we call into the database? We can do that too, using dependent validation rules.

Validation rules that depend on other validation rules

We can make validation rules depend on other validation rules in two ways. First, we can use the DependendRules:

public class CreateCakeCommandValidator: AbstractValidator<CreateCakeCommand>
{
    private readonly ICategoryRepository _categoryRepository;

    public CreateCakeCommandValidator(ICategoryRepository categoryRepository)
    {
    	_categoryRepository = categoryRepository;
    
        RuleFor(x => x.Name).NotEmpty();
        RuleFor(x => x.Description).NotEmpty();
        RuleFor(x => x.CategoryId)
            .GreaterThan(0).DependendRules(() => {
                RuleFor(x => x.CategoryId)
                  .Must(BeExistingCategory)
                  .WithMessage("Invalid category specified.");
            });
    }
    
    private bool BeExistingCategory(int categoryId)
    {
    	return _categoryRepository.CategoryExists(categoryId);
    }
}
Extended implementation for dependent rules.

Notice how we changed the rules for CategoryId so that we first validate that the value is greater than zero. If that's the case, we can then validate that it exists.

This works great for basic scenarios, but it gets pretty messy if you have more rules that you need to cascade.

We can solve this problem in another way, using the When method. We can use the When method to specify a condition for a validation rule.

public class CreateCakeCommandValidator: AbstractValidator<CreateCakeCommand>
{
    private readonly ICategoryRepository _categoryRepository;

    public CreateCakeCommandValidator(ICategoryRepository categoryRepository)
    {
    	_categoryRepository = categoryRepository;
    
        RuleFor(x => x.Name).NotEmpty();
        RuleFor(x => x.Description).NotEmpty();
        RuleFor(x => x.CategoryId).GreaterThan(0);
        RuleFor(x => x.CategoryId)
            .Must(BeExistingCategory)
            .When(x => x.CategoryId > 0);
    }
    
    private bool BeExistingCategory(int categoryId)
    {
    	return _categoryRepository.CategoryExists(categoryId);
    }
}
Extended implementation for conditional rules.

We end up with a much cleaner Validator by using conditions on validation rules.

Note: Personally, I think it's fine to use the dependent rules in scenarios that aren't too complicated. The use of When may sometimes be even less unreadable. The meaning of dependent rules can be quite different from the conditional When.

Now that we've seen how to chain rules and make them conditional, let's see what we can do for validating child collections.

Validation of child collections

Sometimes we have a collection of child elements in an input class. We can validate these too using child rules or a separate validator. Let's start by looking at using child rules.

As an example we'll modify the input command so that it has a collection of ingredients. We're assuming that an ingredient has a relative weight and a name.

We need to extend the validator in order for us to validate the new ingredients collection like so:

public class CreateCakeCommandValidator: AbstractValidator<CreateCakeCommand>
{
    private readonly ICategoryRepository _categoryRepository;

    public CreateCakeCommandValidator(ICategoryRepository categoryRepository)
    {
    	_categoryRepository = categoryRepository;
    
        RuleFor(x => x.Name).NotEmpty();
        RuleFor(x => x.Description).NotEmpty();
        RuleFor(x => x.CategoryId).GreaterThan(0);
        RuleFor(x => x.CategoryId)
            .Must(BeExistingCategory)
            .When(x => x.CategoryId > 0);
            
        RuleForEach(x => x.Ingredients)
            .ChildRules(x => 
            {
                x.RuleFor(x => x.Name).NotEmpty();
                x.RuleFor(x => x.RelativeWeight).GreaterThan(0.0);
            });
    }
    
    private bool BeExistingCategory(int categoryId)
    {
    	return _categoryRepository.CategoryExists(categoryId);
    }
}
Extended implementation for validating collections.

Note the new RuleForEach method call in the validator. We tell the validator to check the Ingredients property using a set of rules. We then call the ChildRules method to specify the rules for each ingredient.

While this is great for some basic set of rules, it can be quite tricky to make this work for a more complex scenario.

Instead of using ChildRules we can also call SetValidator which allows us to specify a separate validator class that is focused on the type of object that is contained in the collection.

public class CreateCakeCommandValidator: AbstractValidator<CreateCakeCommand>
{
    private readonly ICategoryRepository _categoryRepository;

    public CreateCakeCommandValidator(ICategoryRepository categoryRepository)
    {
    	_categoryRepository = categoryRepository;
    
        RuleFor(x => x.Name).NotEmpty();
        RuleFor(x => x.Description).NotEmpty();
        RuleFor(x => x.CategoryId).GreaterThan(0);
        RuleFor(x => x.CategoryId)
            .Must(BeExistingCategory)
            .When(x => x.CategoryId > 0);
            
        RuleForEach(x => x.Ingredients)
            .SetValidator(new IngredientValidator());
    }
    
    private bool BeExistingCategory(int categoryId)
    {
    	return _categoryRepository.CategoryExists(categoryId);
    }
}

public class IngredientValidator: AbstractValidator<Ingredient>
{
    public IngredientValidator()
    {
        RuleFor(x => x.Name).NotEmpty();
        RuleFor(x => x.RelativeWeight).GreaterThan(0.0);
    }
}
Extended implementation for validating collections with a separate validator.

The validator that we specify in the SetValidator method is implemented in the same way as the validator of the root object.

Depending on the scenario you may want to choose ChildRules over the SetValidator technique. I personally think the former is easier for smaller objects, while the latter is easier with more complex objects.

There's a lot more to cover in the library, but I think you'll get the gist of what this library is capable of.

Summary and resources

In this article we've covered how to set up FluentValidation in ASP.NET Core and how to build a basic validator. We then looked at building more complicated validators using custom validation rules, conditional rules, and collection rules.

If you're interested in learning more, I can highly recommend reading the documentation. It contains a very detailed explanation of what you can do with FluentValidation.

Have fun!