C#  

How to Use MediatR for Clean Architecture in .NET Applications

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.

  • Queries → return data

  • Commands → change data (create, update, delete)

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.