Introdução
O MediatR é uma biblioteca .NET que facilita a implementação prática do design pattern comportamental Mediator. O mediator centraliza a comunicação entre os componentes do sistema usando mensagens (Requests) e handlers (Handlers).
Existem dois tipos principais de mensagens:
| Tipo | Uso | Retorno |
|---|---|---|
| Command | Executa uma ação (criar, atualizar, deletar) | Pode retornar Unit (void) ou algum resultado |
| Query | Busca dados | Retorna um valor, DTO ou objeto |
Estrutura básica:
IRequest<TResponse>: representa a requisição (Command ou Query)
IRequestHandler<TRequest, TResponse>: lida com a requisição
IMediator: interface usada para enviar as requisições
Commands, Handlers e Queries
Antes de aprofundarmos tecnicamente e de forma prática em MediatR é importante entender sobre os Commands e Queries. Esses dois termo são conceitos que estão diretamente ligados ao CQRS (Command Query Responsability Segregation).
No MediatR, você envia um Command ou Query, e o Handler é responsável por processar e orquestrar toda a lógica relacionada a essa requisição. Dessa forma, Controllers ou Services não precisam conhecer os detalhes internos de implementação do Command ou Query, mantendo o código mais desacoplado e organizado.
Commands
- Definem a ação que muda o estado do objeto (entidade no banco)
- Devem implementar
IRequest<T>, ondeTé o tipo de retorno (pode serUnitse não retornar nada)
public record CreateUserCommand(string Name, string Email) : IRequest<int>;
Aqui int é o ID do usuário criado.
Queries
- Definem a ação de leitura de um objeto (entidade no banco), sem alteração de dados
- Também implementam um
IRequest<T>
public record GetUserByIdQuery(int Id) : IRequest<UserDto>;
- Retorna
UserDto, ou seja, os dados do usuário.
Handlers
É aqui que a mágica acontece, cada Command/Query tem um handler separado, implementando IRequestHandler<TRequest, TResponse>
Exemplo de Command Handler
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, int>
{
private readonly IUserRepository _repo;
public CreateUserCommandHandler(IUserRepository repo) => _repo = repo;
public async Task<int> Handle(CreateUserCommand request, CancellationToken ct)
{
var user = new User { Name = request.Name, Email = request.Email };
await _repo.AddAsync(user);
return user.Id;
}
}
Exemplo de Query Handler
public class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery, UserDto>
{
private readonly IUserRepository _repo;
public GetUserByIdQueryHandler(IUserRepository repo) => _repo = repo;
public async Task<UserDto> Handle(GetUserByIdQuery request, CancellationToken ct)
{
var user = await _repo.GetByIdAsync(request.Id);
return new UserDto(user.Id, user.Name, user.Email);
}
}
Exemplo prático
Instalação
Install-Package MediatR
Install-Package MediatR.Extensions.Microsoft.DependencyInjection
Exemplo prático – CRUD de Produto
Vamos comparar o projeto de API em Clean Architecture com camadas. Teremos dois cenários, o primeiro sem a utilização do MediatR, e o segundo cenário com ele aplicado.
Application
├── Commands
│ ├── CreateProductCommand.cs
│ └── UpdateProductCommand.cs
├── Queries
│ ├── GetProductByIdQuery.cs
│ └── GetAllProductsQuery.cs
├── Handlers
│ ├── CreateProductHandler.cs
│ ├── GetProductByIdHandler.cs
Infrastructure
└── Repositories
Presentation (API)
└── Controllers
Sem MediatR (código acoplado)
Aqui o Controller conhece diretamente o serviço/repositório e faz toda a lógica:
// Controller
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
private readonly IUserRepository _userRepository;
public UsersController(IUserRepository userRepository)
{
_userRepository = userRepository;
}
[HttpPost]
public async Task<IActionResult> Create(UserCreateDto dto)
{
// Lógica de negócio diretamente no controller
if (string.IsNullOrEmpty(dto.Name) || string.IsNullOrEmpty(dto.Email))
return BadRequest("Nome e email são obrigatórios.");
var user = new User { Name = dto.Name, Email = dto.Email };
await _userRepository.AddAsync(user);
return Ok(user.Id);
}
}
O problema aqui é:
- Controller faz validação e persistência, acumulando responsabilidades.
- Difícil testar a lógica isoladamente.
- Difícil reaproveitar a lógica em outro ponto da aplicação.
Com MediatR (código desacoplado)
Nesse cenário separamos Command, Handler e Controller.
// Command
public record CreateUserCommand(string Name, string Email) : IRequest<int>;
// Handler
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, int>
{
private readonly IUserRepository _userRepository;
public CreateUserCommandHandler(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task<int> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(request.Name) || string.IsNullOrEmpty(request.Email))
throw new ArgumentException("Nome e email são obrigatórios.");
var user = new User { Name = request.Name, Email = request.Email };
await _userRepository.AddAsync(user);
return user.Id;
}
}
// Controller
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
private readonly IMediator _mediator;
public UsersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateUserCommand command)
{
var id = await _mediator.Send(command); // envia o comando para o Handler
return Ok(id);
}
}
- Controller não conhece a lógica interna, só envia o comando.
- Lógica isolada no Handler, fácil de testar unitariamente.
- Fácil adicionar validações, logs, notificações, etc., sem mudar o Controller.
- Código mais organizado e escalável em projetos grandes.
Mediator aplicado ao Clean Arch + CQRS
Ao aplicar o padrão MediatR + CQRS + Clean Arch em um cenário simples como apenas a listagem de alguns produtos (uma única ação) pode deixar a visão dos desenvolvedores nebulosa.
Em sistemas complexos, essa separação evita acoplamento rígido, quando uma mudança na infraestrutura é dada, como trocar o banco de dados, que bra a lógica de negócio.
O objetivo aqui é entender o propósito de “tanta separação de camadas e responsabilidades” e deixar claro como o Handle isola a complexidade do serviço.
O Fluxo Complexo com CQRS/Mediator:
Vamos imaginar o sistema de gerenciamentos de pedidos e estoques (e-commerce)
1. Controller e Command
O Controller recebe a requisição de finalização e despacha um Command.
// OrdersController (Complexo)
public async Task<IActionResult> PlaceOrder([FromBody] PlaceOrderDTO orderDto)
{
// 1. Mapeia DTO para Command
var command = _mapper.Map<PlaceOrderCommand>(orderDto);
// 2. Despacha o Command via Mediator
var orderId = await _mediator.Send(command);
return Ok(new { orderId });
2. Service/Mediator e CommandHandler (Onde a Complexidade é Isolada)
O PlaceOrderCommand é enviado para o PlaceOrderCommandHandler. É aqui que o Mediator/CQRS justifica sua existência.
// Em CleanArchMvc.Application.Products.Handlers;
// Dependências injetadas que representam o acesso à infraestrutura e a outros serviços
// As interfaces (IOrderRepository, IInventoryService, IPaymentGateway, etc.)
// estão definidas na Camada de Domínio e implementadas na Camada de Infraestrutura.
public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, int>
{
private readonly IOrderRepository _orderRepository;
private readonly IInventoryService _inventoryService; // Ex: Serviço para chamar API de Estoque
private readonly IPaymentGateway _paymentGateway; // Ex: Serviço para chamar API de Pagamento (Stripe/PagSeguro)
public PlaceOrderCommandHandler(
IOrderRepository orderRepository,
IInventoryService inventoryService,
IPaymentGateway paymentGateway)
{
_orderRepository = orderRepository;
_inventoryService = inventoryService;
_paymentGateway = paymentGateway;
}
public async Task<int> Handle(PlaceOrderCommand request, CancellationToken cancellationToken)
{
// 1. Lógica de Domínio: Criação da Entidade
// Assumimos que a Entidade 'Order' tem um método estático de criação que
// aplica as regras de negócio de criação (ex: define o status inicial).
var order = Order.CreateNew(
request.CustomerEmail,
request.ShippingAddress,
request.TotalAmount,
request.Items.Select(i => new OrderItem(i.ProductId, i.Quantity)).ToList()
);
// 2. Orquestração: Verificação de Estoque (Chama um serviço de infraestrutura)
// Isso pode ser uma chamada a uma API externa de gerenciamento de estoque.
var stockCheck = await _inventoryService.CheckStockAvailability(order.Items);
if (!stockCheck.IsAvailable)
{
// Lança uma exceção de domínio (que será tratada globalmente na WebUI)
throw new DomainException($"Estoque indisponível para o(s) produto(s): {stockCheck.UnavailableProducts}.");
}
// 3. Persistência (Ponto 1): Salva o Pedido Inicial (Status: Pendente/Aguardando Pagamento)
// Isso garante que o pedido existe no banco antes de tentar o pagamento,
// permitindo o rastreio de pagamentos falhos.
await _orderRepository.AddAsync(order);
// 4. Orquestração: Processamento de Pagamento (Chama outro serviço de infraestrutura)
var paymentResult = await _paymentGateway.ProcessPayment(
order.Id,
request.TotalAmount,
request.PaymentInfo // Assumindo que o Command tem dados de cartão/boleto
);
if (!paymentResult.Success)
{
// 5. Lógica de Compensação (Rollback Lógico)
// Se o pagamento falhar, atualiza o status do pedido e lança uma exceção.
order.CancelOrder(); // Método na Entidade Order
await _orderRepository.UpdateAsync(order);
throw new PaymentFailedException(paymentResult.ErrorMessage);
}
// 6. Lógica de Domínio: Pedido Aprovado
order.ApproveOrder(paymentResult.TransactionId); // Método na Entidade Order (muda o status)
// 7. Persistência (Ponto 2): Salva as Mudanças da Aprovação
await _orderRepository.UpdateAsync(order);
return order.Id;
}
}
Como dá pra perceber acima, o código é a materialização da SoC, separação de preocupações (Separation of Concerns) e do SRP, Princípio da Responsabilidade única (SRP).
A função principal do handle acima não é executar nenhuma tarefa bruta (como acessar o banco de dados), mas sim ORQUESTRAR a sequência dos passos que definem o processo de “Fazer um pedido”. (a lógica de negócio complexa)
Top comments (0)