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 Method | Purpose | Example |
|---|---|---|
| `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
AbstractValidator<T>: The base class you inherit from.Tis the target model or command being validated.RuleFor(...): The entry-point method that targets a specific property ofTusing a lambda expression.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.