What Is MediatR and How to Implement CQRS in C
This article explores MediatR, a popular open-source .NET library that simplifies in-process messaging by implementing the Mediator design pattern. It explains the core concepts behind Command Query Responsibility Segregation (CQRS) and provides a clear, step-by-step technical guide demonstrating how MediatR bridges requests and handlers to enforce clean architecture in C# applications.
What Is MediatR?
MediatR is an in-process messaging library built for .NET by Jimmy
Bogard. It provides a simple mechanism for decoupling the sending of a
message from the code that processes it. Instead of components directly
referencing and invoking one another, they send messages through an
instance of IMediator. MediatR matches each message to its
corresponding handler and executes it, effectively decoupling the caller
from business logic implementation details.
Understanding CQRS
Command Query Responsibility Segregation (CQRS) is an architectural pattern that separates data modification operations from data retrieval operations:
- Commands: Operations that change the state of an application (e.g., creating, updating, or deleting data). Commands should not return domain data, although they may return status indicators or generated identifiers.
- Queries: Operations that retrieve data without altering system state. Queries should be side-effect-free and return tailored Data Transfer Objects (DTOs).
By segregating reads and writes, applications can optimize data models, scale components independently, and maintain clearer boundaries around business logic.
How MediatR Enables CQRS
MediatR naturally fits CQRS because it models every operation as a discrete request-response pair:
- Messages (
IRequest<TResponse>): Represent either a Command or a Query. - Handlers
(
IRequestHandler<TRequest, TResponse>): Contain the execution logic for a specific Command or Query. - Mediator (
IMediator): Acts as the dispatcher, taking a request from a caller (such as an API controller) and directing it to the registered handler.
This architecture ensures that API endpoints remain thin, delegating all domain interactions to single-purpose handlers.
Implementing CQRS with MediatR in C
1. Installation and Setup
Install the MediatR package via the NuGet Package Manager:
dotnet add package MediatRIn your application's startup configuration (e.g.,
Program.cs), register MediatR with the dependency injection
container:
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));2. Creating a Command
A command represents an action that alters state. Define the command data structure and its associated handler:
using MediatR;
// The Command
public record CreateUserCommand(string Name, string Email) : IRequest<Guid>;
// The Handler
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Guid>
{
public async Task<Guid> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
var userId = Guid.NewGuid();
// Persist the user to the database here
return await Task.FromResult(userId);
}
}3. Creating a Query
A query represents an operation that only retrieves data:
using MediatR;
// The DTO
public record UserDto(Guid Id, string Name, string Email);
// The Query
public record GetUserByIdQuery(Guid Id) : IRequest<UserDto?>;
// The Handler
public class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery, UserDto?>
{
public async Task<UserDto?> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
{
// Fetch the user from the database here
var user = new UserDto(request.Id, "Jane Doe", "jane@example.com");
return await Task.FromResult(user);
}
}4. Dispatching Requests from an API Controller
Use IMediator to dispatch commands and queries directly
from an API endpoint:
using MediatR;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IMediator _mediator;
public UsersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> CreateUser([FromBody] CreateUserCommand command)
{
var userId = await _mediator.Send(command);
return CreatedAtAction(nameof(GetUserById), new { id = userId }, userId);
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetUserById(Guid id)
{
var user = await _mediator.Send(new GetUserByIdQuery(id));
return user is not null ? Ok(user) : NotFound();
}
}Adding Cross-Cutting Concerns with Pipeline Behaviors
MediatR includes an interceptor concept called
IPipelineBehavior. This feature works like middleware,
allowing developers to handle cross-cutting concerns—such as logging,
validation, and performance tracking—before and after request handlers
execute.
using MediatR;
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
_logger.LogInformation("Handling {RequestName}", typeof(TRequest).Name);
var response = await next();
_logger.LogInformation("Handled {RequestName}", typeof(TRequest).Name);
return response;
}
}Register the behavior in Program.cs:
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));Summary
MediatR provides a structured, maintainable way to implement CQRS in C#. By isolating commands, queries, and their processing logic into self-contained units, applications gain improved testability, clear boundaries of responsibility, and a decoupled architecture that is easy to extend over time.