Loading...

Using FluentValidation in C#

Learn FluentValidation in C# to define clean and maintainable validation rules for models and application inputs.

FluentValidation is a popular library in C# that allows you to define validation rules using a fluent API. It lets you create reusable and testable rules that are independent of model classes. This article provides a practical guide—from installation to advanced features, including ASP.NET Core integration and unit testing.


Installation

For the core package and optional ASP.NET Core integration:


dotnet add package FluentValidation
dotnet add package FluentValidation.AspNetCore   # For web projects

For desktop (WPF/WinForms) or service projects, only FluentValidation is needed.


Basic Usage: A Simple Validator

A validator class derives from AbstractValidator<T>, and rules are written using RuleFor.


using FluentValidation;

public class UserDto
{
    public string? Name { get; set; }
    public string? Email { get; set; }
    public int Age { get; set; }
}

public class UserDtoValidator : AbstractValidator<UserDto>
{
    public UserDtoValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Name cannot be empty")
            .Length(2, 50).WithMessage("Name must be between 2 and 50 characters");

        RuleFor(x => x.Email)
            .NotEmpty().WithMessage("Email is required")
            .EmailAddress().WithMessage("Please provide a valid email");

        RuleFor(x => x.Age)
            .InclusiveBetween(18, 120).WithMessage("Age must be between 18 and 120");
    }
}

To validate: var result = new UserDtoValidator().Validate(model); Errors can be read via result.Errors.


Messages, Severity, and Cascade Mode


RuleFor(x => x.Name)
    .Cascade(CascadeMode.Stop)
    .NotEmpty().WithMessage("Name is required").WithSeverity(Severity.Error)
    .MinimumLength(2).WithMessage("Name must be at least 2 characters long");

Conditional Rules: When / Unless

Used to apply a rule only under certain conditions.


RuleFor(x => x.Email)
  .NotEmpty().EmailAddress();

RuleFor(x => x.Age)
  .GreaterThan(0);

RuleFor(x => x)
  .Must(x => x.Email!.EndsWith("@company.com"))
  .When(x => x.Age >= 18)
  .WithMessage("Users aged 18+ must use a corporate email address");

Related Objects and Collections

Use SetValidator for nested model validation and RuleForEach for collection elements.


public class AddressDto { public string? City { get; set; } public string? Zip { get; set; } }

public class AddressValidator : AbstractValidator<AddressDto>
{
    public AddressValidator()
    {
        RuleFor(x => x.City).NotEmpty();
        RuleFor(x => x.Zip).NotEmpty().Length(5, 10);
    }
}

public class CustomerDto
{
    public string? Name { get; set; }
    public AddressDto? Address { get; set; }
    public List<string> Phones { get; set; } = new();
}

public class CustomerValidator : AbstractValidator<CustomerDto>
{
    public CustomerValidator()
    {
        RuleFor(x => x.Name).NotEmpty();

        RuleFor(x => x.Address)
            .NotNull().WithMessage("Address is required")
            .SetValidator(new AddressValidator());

        RuleForEach(x => x.Phones)
            .NotEmpty().Matches(@"^\+?\d{7,15}$")
            .WithMessage("Invalid phone number");
    }
}

Custom and Asynchronous Rules

Use Must/MustAsync for complex or service-dependent validations.


RuleFor(x => x.Email)
  .MustAsync(async (email, ct) =>
  {
      // Example: uniqueness check (pseudo repository)
      bool exists = await _userRepository.ExistsByEmailAsync(email, ct);
      return !exists;
  })
  .WithMessage("Email already exists");

Scenario-Based Validation with RuleSet

You can group rules for different scenarios (e.g., Create, Update).


public class ProductDto { public string? Name { get; set; } public decimal Price { get; set; } }

public class ProductValidator : AbstractValidator<ProductDto>
{
    public ProductValidator()
    {
        RuleSet("Create", () =>
        {
            RuleFor(x => x.Name).NotEmpty();
            RuleFor(x => x.Price).GreaterThan(0);
        });

        RuleSet("Update", () =>
        {
            RuleFor(x => x.Price).GreaterThanOrEqualTo(0);
        });
    }
}

// Usage:
// validator.Validate(model, options: o => o.IncludeRuleSets("Create"));

Customizing and Localizing Error Messages


RuleFor(x => x.Email)
  .WithName("Email")
  .NotEmpty().WithMessage("{PropertyName} cannot be empty")
  .EmailAddress().WithMessage("{PropertyName} is not valid");

ASP.NET Core Integration

Register services to enable automatic model validation and integrate with ModelState.


// Program.cs (Minimal API / ASP.NET Core 7+)
using FluentValidation;
using FluentValidation.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddFluentValidationAutoValidation();  // Works automatically with MVC model binding
builder.Services.AddValidatorsFromAssemblyContaining<UserDtoValidator>(); // Scans all validators

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

In a controller, you don’t need to call the validator manually; after model binding, FluentValidation runs automatically and writes errors to ModelState.


Advanced Topics: Dependencies, Injection, and Reusable Rules


public class NameRules : AbstractValidator<UserDto>
{
    public NameRules()
        => RuleFor(x => x.Name).NotEmpty().MaximumLength(50);
}

public class UserDtoValidator2 : AbstractValidator<UserDto>
{
    public UserDtoValidator2()
    {
        Include(new NameRules());
        RuleFor(x => x.Email).EmailAddress();
    }
}

Testability: FluentValidation.TestHelper

Use helper extensions to quickly test your validators.


dotnet add package FluentValidation.TestHelper

using FluentValidation.TestHelper;
using Xunit;

public class UserDtoValidatorTests
{
    [Fact]
    public void Name_Should_Not_Be_Empty()
    {
        var v = new UserDtoValidator();
        var result = v.TestValidate(new UserDto { Name = "" });
        result.ShouldHaveValidationErrorFor(x => x.Name);
    }

    [Fact]
    public void Email_Should_Be_Valid()
    {
        var v = new UserDtoValidator();
        var result = v.TestValidate(new UserDto { Email = "x" });
        result.ShouldHaveValidationErrorFor(x => x.Email);
    }
}

Real-World Example: Registration Form + Address & Phones

The following example demonstrates user registration, nested address validation, and phone list validation all together.


public class RegisterRequest
{
    public string? FullName { get; set; }
    public string? Email { get; set; }
    public AddressDto? Address { get; set; }
    public List<string> Phones { get; set; } = new();
}

public class RegisterValidator : AbstractValidator<RegisterRequest>
{
    public RegisterValidator(IEmailBlacklistService blacklist)
    {
        RuleFor(x => x.FullName)
            .NotEmpty().WithMessage("Full name is required")
            .MaximumLength(80);

        RuleFor(x => x.Email)
            .NotEmpty().EmailAddress()
            .MustAsync(async (mail, ct) => !await blacklist.IsBlacklistedAsync(mail, ct))
            .WithMessage("Email is not accepted");

        RuleFor(x => x.Address)
            .NotNull().SetValidator(new AddressValidator());

        RuleForEach(x => x.Phones)
            .NotEmpty()
            .Matches(@"^\+?\d{7,15}$").WithMessage("Invalid phone format");

        // Conditional: If address country is 'TR', postal code must be 5 digits
        When(x => x.Address?.Country == "TR", () =>
        {
            RuleFor(x => x.Address!.Zip)
                .Matches(@"^\d{5}$").WithMessage("Postal code must be 5 digits for TR");
        });
    }
}

Performance and Best Practices


TL;DR

  • FluentValidation separates rules from models, providing readable and testable validation.
  • RuleFor, RuleForEach, and SetValidator are the core building blocks.
  • Use When/Unless for conditional rules; RuleSet for scenario-based validation.
  • Must/MustAsync is ideal for custom or external service checks.
  • In ASP.NET Core, enable automatic integration with AddFluentValidationAutoValidation().
  • Secure your validators with unit tests using TestHelper.

Related Articles