Mastering FluentValidation in .NET Core

Introduction Data validation is a fundamental aspect of any application, ensuring that user inputs meet the required criteria before processing. FluentValidation is a powerful .NET library that simplifies the validation process while maintaining clean and maintainable code. In this article, we will explore: Why FluentValidation? Setting up FluentValidation in .NET Core Writing custom validators Integrating FluentValidation with CQRS Handling localization for multilingual validation messages Why FluentValidation? Key Benefits: ✅ Separation of Concerns – Keeps validation logic separate from business rules. ✅ Improved Readability – Uses an expressive, fluent API for defining validation rules. ✅ Reusable & Maintainable – Eliminates repetitive validation logic across multiple components. ✅ Supports Complex Validations – Easily handle conditional and cross-field validations. ✅ Built-in Localization – Supports multi-language validation messages. ✅ Seamless Integration – Works well with ASP.NET Core and Dependency Injection. 1️⃣ Installing FluentValidation To get started, install the FluentValidation package via NuGet: Install-Package FluentValidation.AspNetCore Then, register FluentValidation in Program.cs: var builder = WebApplication.CreateBuilder(args); // Register FluentValidation builder.Services.AddControllers(); builder.Services.AddFluentValidationAutoValidation(); builder.Services.AddValidatorsFromAssemblyContaining(); var app = builder.Build(); app.UseAuthorization(); app.MapControllers(); app.Run(); 2️⃣ Creating a Validator Class Let's define a Car model and create a validator for it: public class Car { public string Make { get; set; } public string Model { get; set; } public string VIN { get; set; } public int Year { get; set; } public decimal PricePerDay { get; set; } } Writing a Validator for the Car Model using FluentValidation; public class CarValidator : AbstractValidator { public CarValidator() { RuleFor(car => car.Make) .NotEmpty().WithMessage("Make is required") .MaximumLength(50).WithMessage("Make cannot exceed 50 characters"); RuleFor(car => car.Model) .NotEmpty().WithMessage("Model is required") .MaximumLength(50).WithMessage("Model cannot exceed 50 characters"); RuleFor(car => car.VIN) .NotEmpty().WithMessage("VIN is required") .Matches("^[A-HJ-NPR-Z0-9]{17}$").WithMessage("VIN must be exactly 17 characters"); RuleFor(car => car.Year) .InclusiveBetween(1886, DateTime.UtcNow.Year) .WithMessage($"Year must be between 1886 and {DateTime.UtcNow.Year}"); RuleFor(car => car.PricePerDay) .GreaterThan(0).WithMessage("Price per day must be greater than zero"); } } 3️⃣ Using FluentValidation in Controllers [ApiController] [Route("api/cars")] public class CarController : ControllerBase { [HttpPost] public IActionResult CreateCar([FromBody] Car car) { if (!ModelState.IsValid) { return BadRequest(ModelState); } return Ok("Car successfully created"); } } With FluentValidation, validation happens automatically, and invalid requests return structured error messages. 4️⃣ Advanced: Integrating FluentValidation with CQRS In a CQRS (Command Query Responsibility Segregation) architecture, FluentValidation helps validate commands before they reach the handler. Define a CreateCarCommand: using MediatR; public class CreateCarCommand : IRequest { public string Make { get; set; } public string Model { get; set; } public int Year { get; set; } } Create a Validator for the Command: public class CreateCarCommandValidator : AbstractValidator { public CreateCarCommandValidator() { RuleFor(command => command.Make).NotEmpty().WithMessage("Make is required"); RuleFor(command => command.Model).NotEmpty().WithMessage("Model is required"); RuleFor(command => command.Year).InclusiveBetween(1886, DateTime.UtcNow.Year); } } Register and Validate in a Handler: public class CreateCarCommandHandler : IRequestHandler { public async Task Handle(CreateCarCommand request, CancellationToken cancellationToken) { // Business logic for car creation return await Task.FromResult(1); } } Now, whenever CreateCarCommand is executed, FluentValidation ensures the request is valid before reaching the handler. 5️⃣ Localization Support (Multi-language Validation Messages) To support multiple languages, integrate a translation service: RuleFor(car => car.Make) .NotEmpty().WithMessage(_ => _translationService.GetTranslation("MakeRequired")); Example Translation Service: public interface I

Jan 18, 2025 - 20:05
Mastering FluentValidation in .NET Core

Introduction

Data validation is a fundamental aspect of any application, ensuring that user inputs meet the required criteria before processing. FluentValidation is a powerful .NET library that simplifies the validation process while maintaining clean and maintainable code.

In this article, we will explore:

  • Why FluentValidation?
  • Setting up FluentValidation in .NET Core
  • Writing custom validators
  • Integrating FluentValidation with CQRS
  • Handling localization for multilingual validation messages

Why FluentValidation?

Key Benefits:

Separation of Concerns – Keeps validation logic separate from business rules.
Improved Readability – Uses an expressive, fluent API for defining validation rules.
Reusable & Maintainable – Eliminates repetitive validation logic across multiple components.
Supports Complex Validations – Easily handle conditional and cross-field validations.
Built-in Localization – Supports multi-language validation messages.
Seamless Integration – Works well with ASP.NET Core and Dependency Injection.

1️⃣ Installing FluentValidation

To get started, install the FluentValidation package via NuGet:

Install-Package FluentValidation.AspNetCore

Then, register FluentValidation in Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Register FluentValidation
builder.Services.AddControllers();
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssemblyContaining<CarValidator>();

var app = builder.Build();
app.UseAuthorization();
app.MapControllers();
app.Run();

2️⃣ Creating a Validator Class

Let's define a Car model and create a validator for it:

public class Car
{
    public string Make { get; set; }
    public string Model { get; set; }
    public string VIN { get; set; }
    public int Year { get; set; }
    public decimal PricePerDay { get; set; }
}

Writing a Validator for the Car Model

using FluentValidation;

public class CarValidator : AbstractValidator<Car>
{
    public CarValidator()
    {
        RuleFor(car => car.Make)
            .NotEmpty().WithMessage("Make is required")
            .MaximumLength(50).WithMessage("Make cannot exceed 50 characters");

        RuleFor(car => car.Model)
            .NotEmpty().WithMessage("Model is required")
            .MaximumLength(50).WithMessage("Model cannot exceed 50 characters");

        RuleFor(car => car.VIN)
            .NotEmpty().WithMessage("VIN is required")
            .Matches("^[A-HJ-NPR-Z0-9]{17}$").WithMessage("VIN must be exactly 17 characters");

        RuleFor(car => car.Year)
            .InclusiveBetween(1886, DateTime.UtcNow.Year)
            .WithMessage($"Year must be between 1886 and {DateTime.UtcNow.Year}");

        RuleFor(car => car.PricePerDay)
            .GreaterThan(0).WithMessage("Price per day must be greater than zero");
    }
}

3️⃣ Using FluentValidation in Controllers

[ApiController]
[Route("api/cars")]
public class CarController : ControllerBase
{
    [HttpPost]
    public IActionResult CreateCar([FromBody] Car car)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        return Ok("Car successfully created");
    }
}

With FluentValidation, validation happens automatically, and invalid requests return structured error messages.

4️⃣ Advanced: Integrating FluentValidation with CQRS

In a CQRS (Command Query Responsibility Segregation) architecture, FluentValidation helps validate commands before they reach the handler.

Define a CreateCarCommand:

using MediatR;
public class CreateCarCommand : IRequest<int>
{
    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }
}

Create a Validator for the Command:

public class CreateCarCommandValidator : AbstractValidator<CreateCarCommand>
{
    public CreateCarCommandValidator()
    {
        RuleFor(command => command.Make).NotEmpty().WithMessage("Make is required");
        RuleFor(command => command.Model).NotEmpty().WithMessage("Model is required");
        RuleFor(command => command.Year).InclusiveBetween(1886, DateTime.UtcNow.Year);
    }
}

Register and Validate in a Handler:

public class CreateCarCommandHandler : IRequestHandler<CreateCarCommand, int>
{
    public async Task<int> Handle(CreateCarCommand request, CancellationToken cancellationToken)
    {
        // Business logic for car creation
        return await Task.FromResult(1);
    }
}

Now, whenever CreateCarCommand is executed, FluentValidation ensures the request is valid before reaching the handler.

5️⃣ Localization Support (Multi-language Validation Messages)

To support multiple languages, integrate a translation service:

RuleFor(car => car.Make)
    .NotEmpty().WithMessage(_ => _translationService.GetTranslation("MakeRequired"));

Example Translation Service:

public interface ITranslationService
{
    string GetTranslation(string key);
}

public class TranslationService : ITranslationService
{
    private readonly Dictionary<string, string> _translations = new()
    {
        { "MakeRequired", "La marque est requise" } // French Example
    };

    public string GetTranslation(string key)
    {
        return _translations.TryGetValue(key, out var value) ? value : key;
    }
}

This way, validation messages can be translated dynamically based on the user's language.

Conclusion

FluentValidation is a powerful tool for managing input validation in .NET Core applications. It keeps validation logic clean, reusable, and maintainable while supporting complex business rules, CQRS integration, and localization.