Implement FluentValidation in a .NET Web API

Introduction

Data validation is important for creating secure applications. It ensures that user entered data is accurate, reliable, and safe for processing. In this article, we will explore how to implement data validation using FluentValidation in a .NET Web API.

What is FluentValidation?

FluentValidation is a robust open-source .NET validation library that simplifies creating and maintaining clean validations. It allows us to build strongly typed validation rules and separates the model classes from the validation logic.

How do you implement fluent validation in .NET web API?

To install the FluentValidation package, run the commands below in the NuGet package manager console within Visual Studio.

Install-Package FluentValidation  
Install-Package FluentValidation.DependencyInjectionExtensions

To integrate with ASP.NET Core, install the FluentValidation.AspNetCore package from Visual Studio.

Install-Package FluentValidation.AspNetCore

Note. The FluentValidation.The aspNetCore package is now deprecated.

Types of Validation

  • Simple Validation
  • Nested Objects Validation
  • Custom Validation
  • Conditional Validation

Simple Validation

Simple validation using FluentValidation involves creating a validator class that defines rules for validating the properties of a model.

Step 1. Create a Model.

Create a simple model class that you want to validate. We have created the Person class.

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}

Step 2. Create a Validator.

Create a validator class for the Person model. This class will define the validation rules for the model properties. The PersonValidator class inherits from AbstractValidator<Person> and defines validation rules for the Person properties. We are going to validate the below conditions.

  • FirstName and LastName should not be empty.
  • Age should be between 18 and 60.
  • Email should be a valid email address.
    using FluentValidation;
    public class PersonValidator : AbstractValidator<Person>
    {
        public PersonValidator()
        {
            RuleFor(x => x.FirstName)
                .NotEmpty()
                .WithMessage("First name is required.");
          
            RuleFor(x => x.LastName)
                .NotEmpty()
                .WithMessage("Last name is required.");
            
            RuleFor(x => x.Age)
                .InclusiveBetween(18, 60)
                .WithMessage("Age must be between 18 and 60.");
            
            RuleFor(x => x.Email)
                .EmailAddress()
                .WithMessage("A valid email address is required.");
        }
    }
    
  • RuleFor: Defines a validation rule for a specific property of the class.
  • x => x.FirstName: Specifies the property to validate using a lambda expression.
  • NotEmpty(): Checks that the property is not empty.
  • WithMessage(""): Sets a custom error message if the validation rule fails

Step 3. Register FluentValidation in Startup.

FluentValidation is registered in the ConfigureServices() method of Program.cs to enable dependency injection and automatic validation.

using FluentValidation.AspNetCore;
public class Startup
{
    static void ConfigureServices(IServiceCollection services)
	{
		services.AddControllers();
		services.AddScoped<IValidator<Person>, PersonValidator>(); // Register the validator
	}
}

Step 4. Use the Validator in a Controller.

The PersonController uses the PersonValidator to validate Person objects in the CreatePerson action method. If validation fails, it returns a BadRequest response with validation errors. If validation succeeds, it returns an Ok response.

[ApiController]
[Route("api/[controller]")]
public class PersonController : ControllerBase
{
    private readonly IValidator<Person> _personValidator;
    public PersonController(IValidator<Person> personValidator)
    {
        _personValidator = personValidator;
    }
    [HttpPost]
    public IActionResult CreatePerson([FromBody] Person person)
    {
        var result = _personValidator.Validate(person);
        if (!result.IsValid)
        {
            return BadRequest(result.Errors);
        }
        return Ok("Person is valid!");
    }
}

Input Data

Request body

Response

Response

Nested Objects Validation

Let's consider a person model that includes an address model.

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string PostalCode { get; set; }
}
public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
    public Address Address { get; set; }
}

Create a validator for both the Person and Address model. Assign AddressValidator to validate the Address property in the Person model using SetValidator.

public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(x => x.FirstName).NotEmpty().WithMessage("First name is required.");
        RuleFor(x => x.LastName).NotEmpty().WithMessage("Last name is required.");
        RuleFor(x => x.Age).InclusiveBetween(18, 60).WithMessage("Age must be between 18 and 60.");
        RuleFor(x => x.Email).EmailAddress().WithMessage("A valid email address is required.");
		RuleFor(x => x.Address).SetValidator(new AddressValidator()); // Assign AddressValidator to validate the Address property
    }
}
public class AddressValidator : AbstractValidator<Address>
{
    public AddressValidator()
    {
        RuleFor(x => x.Street).NotEmpty().WithMessage("Street is required.");
        RuleFor(x => x.City).NotEmpty().WithMessage("City is required.");
        RuleFor(x => x.PostalCode).NotEmpty().Matches(@"^\d{5}$").WithMessage("Postal Code must be a 5 digit number.");
    }
}

We are going to validate the conditions below in the address model.

  • The street should not be empty.
  • The city should not be empty.
  • PostalCode length should be 5.

Register all validators in the Startup file.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddScoped<IValidator<Person>, PersonValidator>();
	services.AddScoped<IValidator<Address>, AddressValidator>();
}

Input Data

Input data

Response

Status

Custom Validation

Let's implement a custom validation rule for the "Email" property, which checks below.

  • The "Email" property should not be empty.
  • The "Email" property must be a valid email format(end with "@example.com").

The above conditions are checked using the HaveValidDomain() method in the Person validator.

public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(x => x.FirstName).NotEmpty().WithMessage("First name is required.");
        RuleFor(x => x.LastName).NotEmpty().WithMessage("Last name is required.");
        RuleFor(x => x.Age).InclusiveBetween(18, 60).WithMessage("Age must be between 18 and 60.");
        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress()
            .Must(HaveValidDomain).WithMessage("Email must be from the domain example.com.");
    }
    private bool HaveValidDomain(string email)
    {
        return email.EndsWith("@example.com");
    }
}

Conditional Validation

Validate the PostalCode only if the City is not empty in the AddressValidator.

public class AddressValidator : AbstractValidator<Address>
{
    public AddressValidator()
    {
        RuleFor(x => x.Street).NotEmpty().WithMessage("Street is required.");
        RuleFor(x => x.City).NotEmpty().WithMessage("City is required.");
        RuleFor(x => x.PostalCode)
            .NotEmpty()
            .Matches(@"^\d{5}$")
            .When(x => !string.IsNullOrEmpty(x.City)) // Validate if the City is not empty in PostalCode validation
            .WithMessage("Postal Code must be a 5 digit number.");
    }
}

Customize the Error Message for Users

Instead of sending all error details in the JSON response, we can pass only specific details that the user can understand.

Step 1. Create a Custom Error Class.

[Serializable]
public class Errors
{
    public string ErrorMessage { get; set; } = null!;
    public string PropertyName { get; set; } = null!;
} 

Step 2. Modify the Error Details in the Controller.

[HttpPost]
public IActionResult CreatePerson([FromBody] Person person)
{
    var result = _personValidator.Validate(person);
    if (!result.IsValid)
    {
        return BadRequest(result.Errors.Select(x => new Errors() { ErrorMessage = x.ErrorMessage, PropertyName = x.PropertyName })); // Modify the error message structure
    }
    return Ok("Person is valid!");
}

Response

ErrorMessage

Summary

In this article, you have learned the following topics.

  • What is FluentValidation?
  • How to Implement FluentValidation?
  • Types of Validation and How to Implement Them.
  • Customizing Error Messages.