Masterclass: FluentValidation & CQRS Pipeline Integration

Architectural Purpose

FluentValidation decouples validation logic from domain entities and DTOs into dedicated validator classes. By pairing it with MediatR pipeline behaviors, we enforce a Fail-Fast architectural guardrail before commands ever touch business handlers.


1. Architectural Q&A: Assembly Scanning Syntax

Q0: Why Use Assembly Scanning vs. Manual .AddScoped?

You could manually register every single validator one by one using .AddScoped:

C#

builder.Services.AddScoped<IValidator<CreateOrderCommand>, CreateOrderCommandValidator>();
builder.Services.AddScoped<IValidator<UpdateOrderCommand>, UpdateOrderCommandValidator>();
// ...and so on for dozens of commands

However, as an application scales to dozens or hundreds of commands and queries, manual registration becomes tedious and error-prone (forgetting to register a validator results in runtime bugs). AddValidatorsFromAssembly uses reflection to scan your code automatically, finds every class inheriting from AbstractValidator<>, and registers them all as Scoped services in a single line. It is the .NET equivalent of component scanning in Spring.

Q1: Why is the MediatR assembly syntax different from FluentValidation’s?

  • MediatR Syntax: builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly))
  • FluentValidation Syntax: builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly)

Answer: Both frameworks use Assembly Scanning via reflection to achieve the exact same goal—automatically discovering and registering components without manual .AddScoped<T>() boilerplate. The syntax difference is merely a design choice by the respective library authors (MediatR uses an explicit configuration lambda delegate cfg => ..., whereas FluentValidation provides a direct extension method on IServiceCollection).

Q2: Why don’t we specify a type parameter when calling RuleFor? (e.g., RuleFor<Guid>(x => x.CustomerId))

Answer: C# uses Type Inference (Generic Type Argument Inference). Because CreateOrderCommandValidator inherits from AbstractValidator<CreateOrderCommand>, the compiler already knows T is CreateOrderCommand. When you pass a lambda expression x => x.CustomerId, the compiler automatically inspects the return type of that expression (Guid) and infers TProperty without requiring explicit angle brackets.


2. Core FluentValidation Toolkit: Common Methods

When writing validators, you will rely on a consistent set of built-in rule components:

Rule MethodPurposeExample
`NotEmpty()“Ensures a string, collection, or GUID is not null, empty, or default values (Guid.Empty).RuleFor(x => x.CustomerId).NotEmpty();
NotNull()Ensures reference types or nullable types are not null.RuleFor(x => x.Description).NotNull();
GreaterThan(val)Ensures numeric values strictly exceed a threshold.RuleFor(x => x.TotalAmount).GreaterThan(0);
Length(min, max)Restricts string length within a boundary.RuleFor(x => x.Username).Length(3, 20);
EmailAddress()Validates standard RFC email syntax formatting.RuleFor(x => x.Email).EmailAddress();
Must(predicate)Custom business rule predicate lambda condition.RuleFor(x => x.Code).Must(BeAValidCoupon);

3. Essential Classes & Interfaces to Remember

The FluentValidation Cheat Sheet

  1. AbstractValidator<T>: The base class you inherit from. T is the target model or command being validated.
  2. RuleFor(...): The entry-point method that targets a specific property of T using a lambda expression.
  3. IValidator<T>: The core interface registered in the DI container, injected into pipeline behaviors or controllers to execute validations.

4. Mnemonic Study Guide: “N.G.L.M.” for Validation Rules

To easily recall how to structure rules and validation logic, remember the acronym N.G.L.M.:

  • N - Not Empty / Not Null: Always guard your entry points against missing data.
  • G - Greater Than / Greater Than Or Equal: Enforce numeric boundaries (prices, quantities, ages).
  • L - Length / Logic bounds: Guard string limits or collection sizes.
  • M - Must (Custom Rules): Implement complex domain invariants using custom predicate methods.

5. Complete Code Reference

using FluentValidation;
 
namespace OrderService.Application.Command;
 
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
    public CreateOrderCommandValidator()
    {
        RuleFor(x => x.CustomerId)
            .NotEmpty()
            .WithMessage("Customer ID cannot be empty or default.");
 
        RuleFor(x => x.TotalAmount)
            .GreaterThan(0)
            .WithMessage("Total amount must be strictly greater than zero.");
    }
}

6 : Wiring Up

To wire up your newly created CreateOrderCommandValidator so it automatically intercepts and validates incoming requests before they hit your handler, the next step is implementing a MediatR Pipeline Behavior.

Without a pipeline behavior, FluentValidation will sit in your container, but MediatR won’t automatically invoke it. A pipeline behavior acts as middleware around your handlers.