𝗙𝗹𝘂𝗲𝗻𝘁𝗩𝗮𝗹𝗶𝗱𝗮𝘁𝗶𝗼𝗻 𝗶𝗻 𝗔𝗦𝗣.𝗡𝗘𝗧 𝗖𝗼𝗿𝗲 - 𝗖𝗹𝗲𝗮𝗻, 𝗙𝗹𝗲𝘅𝗶𝗯𝗹𝗲 𝗠𝗼𝗱𝗲𝗹 𝗩𝗮𝗹𝗶𝗱𝗮𝘁𝗶𝗼𝗻 𝗳𝗼𝗿 𝗠𝗼𝗱𝗲𝗿𝗻 .𝗡𝗘𝗧 𝗔𝗽𝗽𝘀

FluentValidation is a popular .NET library that makes it easy to check if your objects have valid data. It’s like a set of rules you create to make sure your objects follow the correct format. The best part? It uses a clean and readable style that even beginners can understand.

In this guide, we’ll walk you through how to use FluentValidation step-by-step, using a practical User Registration example. By the end, you’ll see how it simplifies input validation, keeps your code clean, and improves maintainability in your ASP.NET Core applications.

📘 What is FluentValidation?

Think of FluentValidation as a rulebook for your objects.

Imagine you're building a library system. You need to ensure:

  • The library has a name

  • Each book has a valid ID, title, and author

  • The library has a valid address, phone number, and at least five books

Instead of writing lots of messy if conditions, FluentValidation allows you to define all these rules in one place — using a clean, fluent syntax. This approach keeps your logic centralized, testable, and easier to maintain as your application grows.

📁 Step 1: Install FluentValidation Packages

Run the following commands:

dotnet add package FluentValidation 
dotnet add package FluentValidation.DependencyInjectionExtensions

👴 The Traditional Way – Using Data Annotations

Here’s how user registration validation might look with Data Annotations:

public class UserRegistrationRequest
{
    [Required]
    [MinLength(4)]
    public string? FirstName { get; set; }

    [Required]
    [MaxLength(10)]
    public string? LastName { get; set; }

    [Required]
    [EmailAddress]
    public string? Email { get; set; }

    [Required]
    public string? Password { get; set; }

    [Compare("Password")]
    public string? ConfirmPassword { get; set; }
}

🔴 While it works, it tightly couples validation to your model and lacks flexibility for complex rules or testing.

✅ The FluentValidation Way – Clean & Flexible

Step 2: Define the Model

public class UserRegistrationRequest
{
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    public string? Email { get; set; }
    public string? Password { get; set; }
    public string? ConfirmPassword { get; set; }
}

Step 3: Create the Validator

using FluentValidation;

public class UserRegistrationValidator : AbstractValidator<UserRegistrationRequest>
{
    public UserRegistrationValidator()
    {
        RuleFor(x => x.FirstName)
            .NotEmpty().WithMessage("First name is required.")
            .MinimumLength(4).WithMessage("First name must be at least 4 characters long.");

        RuleFor(x => x.LastName)
            .NotEmpty().WithMessage("Last name is required.")
            .MaximumLength(10).WithMessage("Last name cannot exceed 10 characters.");

        RuleFor(x => x.Email)
            .NotEmpty().WithMessage("Email is required.")
            .EmailAddress().WithMessage("{PropertyName} is invalid! Please check!");

        RuleFor(x => x.Password)
            .NotEmpty().WithMessage("Password is required.");

        RuleFor(x => x.ConfirmPassword)
            .NotEmpty().WithMessage("Please confirm your password.")
            .Equal(x => x.Password).WithMessage("Passwords do not match!");
    }
}

Step 4: Register FluentValidation in Program.cs

// Add services to the container.

builder.Services.AddControllers(options =>
{
    options.Filters.Add<FluentValidationActionFilter>();
});

// Register validators from assembly
builder.Services.AddValidatorsFromAssemblyContaining<UserRegistrationValidator>();

Step 5: Custom Validation Filter

using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace FluentValidationSample.ActionFilter {
  public class FluentValidationActionFilter: IAsyncActionFilter {
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) {
      foreach(var argument in context.ActionArguments.Values) {
        if (argument == null) continue;

        var validatorType = typeof (IValidator < > ).MakeGenericType(argument.GetType());
        var validator = context.HttpContext.RequestServices.GetService(validatorType) as IValidator;

        if (validator != null) {
          var validationResult = await validator.ValidateAsync(new ValidationContext < object > (argument));
          if (!validationResult.IsValid) {
            foreach(var error in validationResult.Errors) {
              context.ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
            }
            context.Result = new BadRequestObjectResult(context.ModelState);
            return;
          }
        }
      }

      await next();
    }
  }
}

Step 6: Controller Example

[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> Create(UserRegistrationRequest userRegistration)
    {
        return Ok(new
        {
            Message = "User registration data is valid.",
            Data = userRegistration
        });
    }
}

📤 Sample Request

{
  "firstName": "John",
  "lastName": "Doe",
  "email": "john@example.com",
  "password": "secret123",
  "confirmPassword": "secret123"
}

❌ Example Validation Response

{
  "FirstName": ["First name is required."],
  "Email": ["Email is invalid! Please check!"],
  "ConfirmPassword": ["Passwords do not match!"]
}

📚 Built-in Validators in FluentValidation (Line-by-Line)

Here are commonly used validators:

  • NotNull
    Ensures the value is not null.
    RuleFor(x => x.Name).NotNull();

  • NotEmpty
    Ensures the value is not null, empty, or whitespace.
    RuleFor(x => x.Name).NotEmpty();

  • Equal / NotEqual
    Match or avoid matching a specific value or another property.
    RuleFor(x => x.Role).Equal("Admin");
    RuleFor(x => x.Name).NotEqual(x => x.Nickname);

  • Length / MinimumLength / MaximumLength
    Validate string length.
    RuleFor(x => x.Username).Length(4, 20);
    RuleFor(x => x.Password).MinimumLength(6);
    RuleFor(x => x.LastName).MaximumLength(10);

  • LessThan / LessThanOrEqualTo
    Numeric validation.
    RuleFor(x => x.Age).LessThan(60);
    RuleFor(x => x.Score).LessThanOrEqualTo(100);

  • GreaterThan / GreaterThanOrEqualTo
    RuleFor(x => x.Age).GreaterThan(18);
    RuleFor(x => x.Salary).GreaterThanOrEqualTo(5000);

  • InclusiveBetween / ExclusiveBetween
    Range validation.
    RuleFor(x => x.Rating).InclusiveBetween(1, 5);
    RuleFor(x => x.Id).ExclusiveBetween(1, 10);

  • Matches
    Regex pattern match.
    RuleFor(x => x.Email).Matches(@"^\S+@\S+\.\S+$");

  • Must
    Custom logic.
    RuleFor(x => x.Name).Must(name => name.StartsWith("A"));

  • EmailAddress / CreditCard
    Format validations.
    RuleFor(x => x.Email).EmailAddress();
    RuleFor(x => x.CardNumber).CreditCard();

  • IsInEnum / IsEnumName
    Enum validations.
    RuleFor(x => x.Level).IsInEnum();
    RuleFor(x => x.LevelName).IsEnumName(typeof(Level));

  • Empty / Null
    Opposites of NotEmpty/NotNull.
    RuleFor(x => x.Remarks).Empty();
    RuleFor(x => x.MiddleName).Null();

  • PrecisionScale
    Decimal precision and scale.
    RuleFor(x => x.Amount).PrecisionScale(5, 2, false);

🧠 Why Use FluentValidation?

  • ✅ Clean and readable syntax

  • 🔁 Reusable, testable validation logic

  • 📦 Keeps validation separate from business logic

  • 🌍 Supports localization

  • 🧪 Great for unit testing

⚠️ Important: Changes After FluentValidation 11.x

Starting from FluentValidation v11, the automatic integration with ASP.NET Core's validation pipeline was removed.

🔄 What Changed?

  • ❌ FluentValidation.AspNetCore is deprecated

  • ❌ AddFluentValidation() is no longer available

  • ✅ Manual wiring is required using ActionFilter and ModelState handling

✅ What You Should Do Instead

  1. Install the main FluentValidation package:

dotnet add package FluentValidation
  1. Register validators manually in Program.cs:

builder.Services.AddValidatorsFromAssemblyContaining<UserRegistrationValidator>();
  1. Use a custom FluentValidationActionFilter to validate models and return structured errors:

// Add services to the container.

builder.Services.AddControllers(options =>
{
    options.Filters.Add<FluentValidationActionFilter>();
});

This approach gives you full control over how validation errors are handled and avoids any hidden behaviors from the framework.

✅ Summary

FluentValidation gives you precise control over model validation in ASP.NET Core — all while keeping your code clean and maintainable. Whether you're validating user registration forms or complex objects, FluentValidation's rich API and built-in validators make it a powerful choice.



Comments

Popular posts from this blog

Performance Optimization in Sitecore

Strategies for Migrating to Sitecore from legacy or upgrading from older Sitecore

Azure Event Grid Sample code