Introduction
Clean Architecture promotes writing software that is easy to understand, maintain, and scale. One powerful tool that helps achieve this in .NET applications is MediatR, a lightweight library that implements the Mediator Pattern.
MediatR helps remove tight coupling between controllers and business logic by sending commands and queries through a central mediator, keeping your application clean and organized.
In this article, you’ll learn how MediatR works, how to integrate it into a real .NET project, and how it supports Clean Architecture principles—all explained in simple language with practical examples.
1. What Is MediatR?
MediatR is a library that helps implement the Mediator Pattern, where instead of calling services directly, you “send” a request and receive a response from a handler.
Without MediatR (Tightly Coupled)
var users = _userService.GetUsers();
With MediatR (Loosely Coupled)
var users = await _mediator.Send(new GetUsersQuery());
Benefits of MediatR
Reduces coupling between layers
Encourages CQRS pattern (Commands & Queries)
Improves testability
Makes business logic more organized
Helps implement Clean Architecture easily
2. Installing MediatR in .NET
Install the required packages:
Install-Package MediatR
Install-Package MediatR.Extensions.Microsoft.DependencyInjection
Register MediatR in Program.cs:
builder.Services.AddMediatR(typeof(Program));
3. Understanding CQRS with MediatR
CQRS stands for Command Query Responsibility Segregation.
Using MediatR naturally enforces CQRS, improving separation of concerns.
4. Creating a Query with MediatR
Step 1: Create a Query Request
public record GetAllUsersQuery() : IRequest<List<UserDto>>;
Step 2: Create a Handler
public class GetAllUsersHandler : IRequestHandler<GetAllUsersQuery, List<UserDto>>
{
private readonly IUserRepository _repo;
public GetAllUsersHandler(IUserRepository repo)
{
_repo = repo;
}
public async Task<List<UserDto>> Handle(GetAllUsersQuery request, CancellationToken cancellationToken)
{
return await _repo.GetUsersAsync();
}
}
Step 3: Use It in Controller
[HttpGet]
public async Task<IActionResult> GetUsers()
{
var users = await _mediator.Send(new GetAllUsersQuery());
return Ok(users);
}
5. Creating a Command with MediatR
Step 1: Create Command
public record CreateUserCommand(string Name, string Email) : IRequest<int>;
Step 2: Create Handler
public class CreateUserHandler : IRequestHandler<CreateUserCommand, int>
{
private readonly IUserRepository _repo;
public CreateUserHandler(IUserRepository repo)
{
_repo = repo;
}
public async Task<int> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
var userId = await _repo.CreateUserAsync(request.Name, request.Email);
return userId;
}
}
Step 3: Use in Controller
[HttpPost]
public async Task<IActionResult> CreateUser(CreateUserCommand command)
{
var id = await _mediator.Send(command);
return Ok(new { UserId = id });
}
6. Adding Validation with MediatR Pipeline Behaviors
Pipeline behaviors let you run logic before or after handlers execute.
Common uses for Pipeline Behaviors
Validation
Logging
Caching
Exception handling
Example: Validation Behavior
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
// Perform validation here
return await next();
}
}
Register behavior:
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
7. Structuring Clean Architecture with MediatR
A typical folder structure:
src/
├── Application/
│ ├── Commands/
│ ├── Queries/
│ ├── Handlers/
│ ├── Interfaces/
│ └── Behaviors/
├── Domain/
│ ├── Entities/
│ ├── ValueObjects/
│ └── Events/
├── Infrastructure/
│ ├── Persistence/
│ └── Repositories/
└── API/
├── Controllers/
└── Program.cs
Why This Helps
Strong separation of concerns
Each layer is independent
Easy to scale and maintain
8. Using Notifications (Publish/Subscribe Pattern)
Notifications let you broadcast events to multiple handlers.
Example Notification
public class UserCreatedNotification : INotification
{
public int UserId { get; set; }
}
Example Handler
public class SendWelcomeEmailHandler : INotificationHandler<UserCreatedNotification>
{
public Task Handle(UserCreatedNotification notification, CancellationToken cancellationToken)
{
Console.WriteLine($"Sending welcome email to User {notification.UserId}");
return Task.CompletedTask;
}
}
Publish Notification
await _mediator.Publish(new UserCreatedNotification { UserId = newId });
9. Best Practices for Using MediatR in Clean Architecture
Keep handlers small and focused
Use CQRS for better separation
Avoid placing business logic inside controllers
Move repeated logic to pipeline behaviors
Use interfaces for repositories to enable testing
Use notifications for decoupling side effects
Conclusion
MediatR is a powerful tool for implementing Clean Architecture in .NET applications. It reduces coupling, organizes business logic, and encourages the use of patterns like CQRS and the Mediator pattern. By using commands, queries, handlers, and pipeline behaviors, you can build scalable and maintainable applications with clean and testable code. Whether you're building simple APIs or large enterprise systems, MediatR helps keep your architecture clean, modular, and future-proof.