CQRS Validation with MediatR Pipeline and FluentValidation in .NET
Learn centralized CQRS validation in .NET using a MediatR pipeline behavior and FluentValidation. Simple words, clear diagrams, and real C# code.
A guard at the school gate
Think about the main gate of your school.
Before any student walks in, a guard checks them. Is the uniform correct? Is the ID card there? Are the shoes black? Only after these checks does the guard say, "Okay, you may go in."
The guard does the same checks for every student. The guard does not let the class teacher do this job inside each classroom. That would be slow and messy. One guard, one gate, one set of rules for everybody.
In a .NET app, our commands are like students walking in. We want one friendly guard who checks every command before it reaches the part that does the real work. That guard is a MediatR pipeline behavior, and the rules it checks come from FluentValidation.
Let us build this guard together.
What problem are we solving?
In CQRS, we send commands (which change data) and queries (which read data) as small message objects. A tool called MediatR carries each message to the correct handler.
Without a guard, every handler has to check its own input:
public async Task<int> Handle(CreateStudentCommand command, CancellationToken ct)
{
// Validation mixed into business logic. Repeated everywhere.
if (string.IsNullOrWhiteSpace(command.Name))
throw new Exception("Name is required.");
if (command.Age < 5)
throw new Exception("Age must be at least 5.");
// ...the real work finally starts here
var student = new Student(command.Name, command.Age);
_db.Students.Add(student);
await _db.SaveChangesAsync(ct);
return student.Id;
}This is repeated in every handler. It is easy to forget. It is hard to read. And the validation rules are tangled with the business logic.
We want something better. We want the validation to live in one place and run automatically for every command. That is exactly what a pipeline behavior gives us.
How the MediatR pipeline works
When you call mediator.Send(command), the message does not jump straight to the handler. First it travels through a pipeline of behaviors. Each behavior can do work before and after the next step.
Think of it like a row of gates. The message passes through each gate in order. The last gate is the handler that does the real job.
The request pipeline
Steps
Send
App calls mediator.Send(command)
Logging
Behavior records the request
Validation
Behavior checks the rules
Handler
Real work runs if valid
Response
Result travels back out
Each behavior implements one interface: IPipelineBehavior<TRequest, TResponse>. Inside its Handle method, you get the request, and a next delegate. Calling next() means "let the message continue to the next gate." Not calling it means "stop here."
This next design is what lets us block a bad command. If validation fails, we simply do not call next(). The handler never runs.
Meet FluentValidation
FluentValidation is a small, free library that lets you write validation rules in a clean, readable way. You make a class that inherits from AbstractValidator<T> and list your rules.
Here is a validator for a command that creates a student:
using FluentValidation;
public sealed class CreateStudentCommandValidator
: AbstractValidator<CreateStudentCommand>
{
public CreateStudentCommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required.")
.MaximumLength(100);
RuleFor(x => x.Age)
.InclusiveBetween(5, 120)
.WithMessage("Age must be between 5 and 120.");
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress().WithMessage("Please enter a valid email.");
}
}Read it out loud. "Rule for Name: not empty, maximum length 100." It almost reads like plain English. That is the whole point. The rules are easy to write and easy to read later.
The nice thing is that this validator knows nothing about MediatR. It is just a class of rules. We will connect it to the pipeline next.
Building the validation behavior
Now we write the guard. This behavior will:
- Find every validator registered for the current command.
- Run all of them.
- Collect any errors.
- If there are errors, throw a
ValidationException. - If there are no errors, call
next()so the handler runs.
using FluentValidation;
using MediatR;
public sealed class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
=> _validators = validators;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
// No validators for this request? Just continue.
if (!_validators.Any())
return await next();
var context = new ValidationContext<TRequest>(request);
var results = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var failures = results
.SelectMany(r => r.Errors)
.Where(f => f is not null)
.ToList();
if (failures.Count != 0)
throw new ValidationException(failures);
return await next();
}
}Notice a few kind details:
- We inject
IEnumerable<IValidator<TRequest>>. The container hands us all validators for this command type. Most commands have one, but you can have several. - If there are no validators, we skip straight to
next(). Queries that need no validation are not slowed down. - We run validators with
ValidateAsync, so async rules (like checking a database) work too.
Wiring it all up
Now we register everything with dependency injection. This is the glue that makes the magic automatic.
var builder = WebApplication.CreateBuilder(args);
// Register MediatR and our behavior.
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
});
// Find and register all FluentValidation validators in this assembly.
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
var app = builder.Build();AddOpenBehavior(typeof(ValidationBehavior<,>)) plugs our guard into the pipeline for every request. The <,> means it works for any request and response type.
AddValidatorsFromAssembly(...) scans your project and registers every AbstractValidator it finds. So when you add a new validator class later, it is picked up automatically. You do not edit any wiring.
This is the part students love. You write a validator class, and it just works. No extra steps.
Adding a new rule later
Steps
Write validator
Create a new AbstractValidator class
Scanner finds it
AddValidatorsFromAssembly registers it
Pipeline uses it
Behavior runs it automatically
Turning failures into a friendly HTTP response
When validation fails, our behavior throws a ValidationException. We do not want that to crash the app with a scary 500 error. We want a clean 400 Bad Request with a clear message for the user.
In ASP.NET Core, the easiest way is a small exception handler or middleware that catches the exception and shapes a nice response.
app.UseExceptionHandler(handler =>
{
handler.Run(async context =>
{
var feature = context.Features.Get<IExceptionHandlerFeature>();
if (feature?.Error is ValidationException validationException)
{
var errors = validationException.Errors
.GroupBy(e => e.PropertyName)
.ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(new
{
title = "One or more validation errors occurred.",
status = 400,
errors
});
return;
}
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
});
});Now a request with a bad age comes back as a tidy JSON object listing exactly what was wrong. The user knows how to fix it.
Throw an exception, or return a Result?
There are two popular styles for reporting failures. Both are fine. Pick one and stay consistent across your app.
| Style | How it works | Good for | Watch out for |
|---|---|---|---|
Throw ValidationException | Behavior throws, middleware catches it | Simple apps, quick setup | Exceptions for expected errors can feel heavy |
Return a Result object | Behavior returns a failure result, no throw | Functional style, no exceptions for normal flow | Needs your response type to be a Result<T> |
The throwing style is shown above and is the most common. If you already use the Result pattern in your project, you can change the behavior to build a failed Result instead of throwing. The idea is the same: stop the bad command before the handler runs.
Here is a small sketch of the Result style for comparison:
if (failures.Count != 0)
{
// Build a typed failure Result instead of throwing.
var errors = failures.Select(f => f.ErrorMessage).ToArray();
return (TResponse)ResultFactory.Failure(errors);
}This needs a bit more plumbing so the behavior knows how to build a Result<T> of the right type, but it avoids exceptions for everyday validation mistakes.
Where this fits in CQRS
Let us zoom out and see the whole shape. In CQRS, commands change data and queries read data. Validation usually matters most for commands, because they write new information. But you can validate queries too, for example to check that a page number is not negative.
| Message type | What it does | Do we validate? |
|---|---|---|
| Command | Changes data (create, update, delete) | Yes, almost always |
| Query | Reads data only | Sometimes, for safe inputs |
| Notification | Tells other parts something happened | Rarely |
Because our behavior is registered for all requests, both commands and queries pass through it. If a query has no validator, the behavior skips straight to the handler. So you only pay the cost where you actually wrote rules.
Common mistakes and how to avoid them
Forgetting to register the behavior. If you write a validator but never add AddOpenBehavior, nothing runs. The validator just sits there unused. Always register both the behavior and the validators.
Order of behaviors matters. Behaviors run in the order you add them. Usually you want logging first, then validation, then maybe a transaction behavior. Put validation before anything that touches the database, so you never start a transaction for a bad request.
Validating things the validator cannot know. FluentValidation is great for shape checks: not empty, correct length, valid email. But deep business rules, like "this seat is already taken," often need the database and belong closer to the handler or domain. Use the validator for input shape, and the handler for true business rules.
Putting heavy work in a validator. A rule that calls a slow service on every request will slow your whole app. Keep validators light. If you must check the database, make it async and keep it simple.
Async all the way. Use ValidateAsync, not Validate, inside the behavior. This keeps async database rules working and avoids blocking threads.
A note on licensing
MediatR moved to a commercial license in July 2025 under Lucky Penny Software (the same move happened with AutoMapper). New versions use a dual-license model: a reciprocal open license and a paid commercial license.
There is a free Community edition for small companies (under a revenue limit), non-profits, schools, learning, and non-production use. Bigger companies usually need a paid license for production.
The good news: FluentValidation stays free and open source. And the pipeline idea itself is not owned by anyone. If you ever want to drop MediatR, you can hand-write a tiny dispatcher with the same behavior idea. The pattern in this post outlives any one library.
Putting it together: the full flow
Here is the whole story in one picture. A request comes in, passes the guard, and either gets handled or gets a clear error.
End to end request flow
Steps
Client
Calls Send with a command
MediatR
Starts the pipeline
Validation
Runs FluentValidation rules
Handler
Does the real work if valid
Database
Saves the change
Response
Returns success or 400 errors
By keeping validation in one behavior, your handlers stay short and clear. Each handler only worries about its real job. The guard at the gate handles the checking, every single time, for every single command. You write a rule once, and you never forget to use it.
Quick recap
- CQRS splits messages into commands (write) and queries (read). MediatR carries them to handlers.
- A pipeline behavior wraps every request. It can run code before and after the handler. It is the perfect spot for cross-cutting jobs like validation.
- FluentValidation lets you write clean, readable rules in
AbstractValidator<T>classes that know nothing about MediatR. - The ValidationBehavior finds all validators for a request, runs them, and stops the request if any rule fails.
- Register the behavior with
AddOpenBehaviorand the validators withAddValidatorsFromAssembly. New validators are then picked up automatically. - Catch the
ValidationExceptionand turn it into a friendly 400 Bad Request response. - You can throw or return a Result on failure. Pick one style and stay consistent.
- Keep validators light. Use them for input shape, and keep deep business rules near the handler.
- MediatR is now commercially licensed (free Community edition exists). FluentValidation stays free and open source.
References and further reading
- FluentValidation official documentation
- Microsoft Learn: CQRS and the application layer with MediatR
- Milan Jovanovic: CQRS Validation with MediatR Pipeline and FluentValidation
- codewithmukesh: Validation with MediatR Pipeline Behavior and FluentValidation
- Code Maze: CQRS Validation Pipeline with MediatR and FluentValidation
- AutoMapper and MediatR Commercial Editions Launch (Jimmy Bogard)
- Lucky Penny Software Licensing FAQ
Related Patterns
CQRS Pattern with MediatR in .NET: A Friendly Guide
Learn the CQRS pattern with MediatR in .NET using simple words, clear diagrams, and real C# code. Beginner friendly, with pitfalls and licensing notes.
The Result Pattern in .NET: Error Handling Without Exceptions
Learn the Result pattern in .NET for clean, explicit error handling. Replace hidden exceptions with type-safe return values using simple examples, railway-oriented diagrams, code, and clear advice on when to use it.
The Repository Pattern in .NET: A Friendly, Complete Guide
Learn the Repository Pattern in .NET with simple real-life examples, EF Core code, diagrams, and honest advice on when to use it and when to skip it.
Stop Conflating CQRS and MediatR: They Are Not the Same Thing
CQRS and MediatR are two different ideas. Learn what each one really does, why people mix them up, and how to use CQRS in .NET with or without MediatR.
Refactoring a Modular Monolith Without MediatR in .NET
Learn to remove MediatR from a .NET modular monolith using plain handlers and a tiny dispatcher, with CQRS, pipeline behaviors, and clear module boundaries.
Building a Better MediatR Publisher With Channels (and Why You Shouldn't)
Build a custom MediatR INotificationPublisher using System.Threading.Channels for background events in .NET, then learn why a queue this simple can quietly lose your data.