Tutorials ASP.NET Core MVC Mastery

Fluent Validation

On this page

FluentValidation in ASP.NET Core — Enterprise-Grade Validation

Data Annotations (like [Required] and [StringLength]) are great for simple apps, but they pollute your domain models and crumble under complex business logic. FluentValidation is the industry standard for .NET enterprise applications, separating validation rules from classes using a clean, fluent interface with full Dependency Injection support.

1. WHY FluentValidation Over Data Annotations?

  • Separation of Concerns: Domain models stay clean. Validation logic lives in separate Validator classes.
  • Complex Cross-Property Rules: "EndDate must be greater than StartDate" is incredibly painful with Data Annotations, but trivial in FluentValidation.
  • Conditional Validation: "CreditCardNumber is required ONLY IF PaymentMethod == 'CreditCard'".
  • Dependency Injection: Validators can inject database contexts or external services to validate data against a live database.

2. Setup & Installation

Install the required NuGet packages. Note that you no longer need the bulky AutoValidation package; modern architecture prefers explicit manual validation or API Endpoint Filters.

dotnet add package FluentValidation
dotnet add package FluentValidation.DependencyInjectionExtensions

3. REAL-TIME PRODUCTION EXAMPLES

Step 1: The Model (Clean and Attribute-Free)

public class UserRegistrationDto
{
    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; }
    public DateTime DateOfBirth { get; set; }
    public AddressDto Address { get; set; } // Nested object
}

Step 2: The Validator (AbstractValidator)

// Validators/UserRegistrationValidator.cs
using FluentValidation;

public class UserRegistrationValidator : AbstractValidator<UserRegistrationDto>
{
    private readonly IUserRepository _userRepository;

    // We can use Dependency Injection inside validators!
    public UserRegistrationValidator(IUserRepository userRepository)
    {
        _userRepository = userRepository;

        // Base rules
        RuleFor(x => x.FirstName)
            .NotEmpty().WithMessage("First Name is required")
            .MaximumLength(50).WithMessage("Maximum 50 characters allowed");

        // Chained complex rules
        RuleFor(x => x.Password)
            .NotEmpty()
            .MinimumLength(8)
            .Matches("[A-Z]").WithMessage("Password must contain 1 uppercase letter")
            .Matches("[0-9]").WithMessage("Password must contain 1 number");

        // Cross-property validation
        RuleFor(x => x.ConfirmPassword)
            .Equal(x => x.Password).WithMessage("Passwords do not match");

        // Complex business rules
        RuleFor(x => x.DateOfBirth)
            .LessThan(DateTime.Now.AddYears(-18))
            .WithMessage("You must be at least 18 years old to register.");

        // Async Database Validation via DI
        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress()
            .MustAsync(async (email, cancellation) => {
                bool exists = await _userRepository.EmailExistsAsync(email);
                return !exists; // Must return true to pass
            })
            .WithMessage("This email is already registered.");

        // Validating nested objects
        RuleFor(x => x.Address).SetValidator(new AddressValidator());
    }
}

Step 3: Registration in Program.cs

// Using DependencyInjectionExtensions makes this a one-liner.
// It scans the assembly, finds all classes inheriting AbstractValidator, 
// and registers them in the DI container natively.
builder.Services.AddValidatorsFromAssemblyContaining<Program>();

Step 4: Explicit Execution in Controller

public class AccountController : Controller
{
    private readonly IValidator<UserRegistrationDto> _validator;

    public AccountController(IValidator<UserRegistrationDto> validator)
    {
        _validator = validator;
    }

    [HttpPost]
    public async Task<IActionResult> Register(UserRegistrationDto model)
    {
        // 1. Execute Validation
        var validationResult = await _validator.ValidateAsync(model);

        // 2. Check Result
        if (!validationResult.IsValid)
        {
            // 3. Map FluentValidation errors back to standard MVC ModelState
            // This ensures standard Tag Helpers () still work!
            foreach (var error in validationResult.Errors)
            {
                ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
            }

            return View(model);
        }

        // Proceed with registration...
        return RedirectToAction("Success");
    }
}

4. Elegant Extension: AddToModelState()

Writing that foreach loop in every controller violates DRY. Let's create an extension method to bridge FluentValidation and ASP.NET Core MVC instantly.

// Extensions/ValidationExtensions.cs
public static class ValidationExtensions
{
    public static void AddToModelState(this FluentValidation.Results.ValidationResult result, 
                                       ModelStateDictionary modelState)
    {
        foreach (var error in result.Errors)
        {
            modelState.AddModelError(error.PropertyName, error.ErrorMessage);
        }
    }
}

// Controller usage shrinks to:
if (!validationResult.IsValid)
{
    validationResult.AddToModelState(ModelState);
    return View(model);
}

5. Best Practices

✅ DO
  • Use Explicit Validation (injecting IValidator) rather than auto-validation packages, which obscure flow.
  • Use CascadeMode = CascadeMode.Stop if you want validation to halt at the first failure on a specific property, saving database calls (e.g. don't check DB uniqueness if email is empty).
❌ DON'T
  • Don't mix Data Annotations and FluentValidation in the same class. Commit to one architecture.
  • Avoid heavy, long-running processes inside MustAsync. Remember that validators block request execution.

6. Interview Mastery

Q: "Why has the community moved away from FluentValidation's AutoValidation package in modern .NET?"

Architect Answer: "AutoValidation hooks deep into the ASP.NET Core MVC input formatters to validate models before they even hit the controller. While this sounds convenient, it causes two critical problems in enterprise apps. First, it makes it impossible to use asynchronous validation rules (like database lookups via MustAsync) because the MVC binding pipeline is fundamentally synchronous. Second, it hides the control flow. Modern .NET architecture prefers Explicit Validation — injecting IValidator<T> into a controller, service, or API Endpoint Filter — allowing full asynchronous context, better testability, and clear execution flow."

Questions on this lesson 0

Sign in to ask a question or upvote helpful answers.

No questions yet — be the first to ask!

ASP.NET Core MVC Mastery
Course syllabus
1. Core Framework
MODULE 1: INTRODUCTION & ENVIRONMENT SETUP
2. View Engine
MODULE 2: .NET CORE FUNDAMENTALS
MODULE 3: ASP.NET CORE BASICS
MODULE 4: MVC FUNDAMENTALS
MODULE 5: DATA PASSING TECHNIQUES
MODULE 6: ROUTING
MODULE 7: VIEWS & UI
MODULE 8: ACTION RESULTS
MODULE 9: HTML HELPERS
MODULE 10: TAG HELPERS
MODULE 11: MODEL BINDING
MODULE 12: VALIDATION
MODULE 13: STATE MANAGEMENT
MODULE 14: FILTERS & SECURITY
MODULE 15: ENTITY FRAMEWORK CORE (DEEP DIVE)
MODULE 16: DESIGN PATTERNS
MODULE 17: FILE HANDLING
MODULE 18: ADVANCED ASP.NET CORE
MODULE 19: PERFORMANCE & BEST PRACTICES
MODULE 20: RAZOR PAGES (BONUS)
MODULE 21: REAL-WORLD PROJECTS (🔥 MUST DO)
Toolliyo Assistant
Ask about tutorials, ebooks, training, pricing, mentor services, and support. I use public site content only—not admin or internal tools.

care@toolliyo.com

Need callback? Share your details