DEV Community

Yuri Peixinho
Yuri Peixinho

Posted on

MediatR (bônus CQRS + Clean Architecture)

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>, onde T é o tipo de retorno (pode ser Unit se não retornar nada)
public record CreateUserCommand(string Name, string Email) : IRequest<int>;
Enter fullscreen mode Exit fullscreen mode

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 umIRequest<T>
public record GetUserByIdQuery(int Id) : IRequest<UserDto>;
Enter fullscreen mode Exit fullscreen mode
  • 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Exemplo prático

Instalação

Install-Package MediatR
Install-Package MediatR.Extensions.Microsoft.DependencyInjection
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}

Enter fullscreen mode Exit fullscreen mode
  • 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 });
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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)