ASP.NET, CQRS e Mediator
CQRS e Mediator formam um trio muito eficiente com o ASP.NET Core, porque organizam a aplicação em fluxos de leitura e escrita bem definidos. Neste guia, você vai construir uma API do zero, primeiro só com CQRS e depois com o MediatR, para entender exatamente o que cada peça entrega.
O que é CQRS
CQRS é a sigla para Command Query Responsibility Segregation. Em português, significa separação de responsabilidades entre comandos e consultas.
Para facilitar o entendimento, pense nas consultas como a parte de leitura da aplicação. Os comandos, por sua vez, representam a parte de escrita.
A documentação do padrão CQRS no Azure Architecture Center resume bem a ideia: comandos atualizam dados e consultas apenas retornam dados. Além disso, uma consulta nunca altera o estado do sistema.
Leitura e escrita raramente estão em equilíbrio
Quando entendemos o conceito, ele parece óbvio. Afinal, nossas aplicações quase nunca têm cargas balanceadas: ou existe muita leitura, ou existe muita escrita.
Na maioria dos casos, a leitura vence com folga. Em um e-commerce, por exemplo, o usuário navega por horas no catálogo e fecha o pedido uma única vez.
Por isso, separar os dois lados permite otimizar cada um conforme a sua necessidade. Segundo a Microsoft, o padrão brilha justamente quando o número de leituras supera o de escritas.
Preparando a API para CQRS e Mediator
Para este exemplo de CQRS e Mediator, vamos construir tudo dentro da própria API, apenas em pastas separadas. Em projetos maiores, no entanto, essa divisão normalmente acontece em projetos distintos dentro da solução.
Navegue até uma pasta segura e execute o comando abaixo para criar uma nova Web API no ASP.NET Core 3.
dotnet new webapi -o Shop
Em seguida, abra a aplicação no Visual Studio Code para iniciar o desenvolvimento. A estrutura final terá as pastas Domain/Entities, Domain/Commands, Domain/Queries e Domain/Handlers.
Modelando a entidade Customer
Normalmente, a modelagem de domínios ricos começa pelas entidades e pelos objetos de valor. Você também pode usar entidades anêmicas e uma abordagem orientada a dados, mas aqui o foco fica em entidades mais ricas.
Na prática, vamos ser pragmáticos e apenas simular uma entidade rica, com setters privados. O arquivo fica em Domain/Entities/Customer.cs.
using System;
namespace Shop.Domain.Entities
{
public class Customer
{
public Customer(string name, string email)
{
Id = Guid.NewGuid();
Name = name;
Email = email;
}
public Guid Id { get; private set; }
public string Name { get; private set; }
public string Email { get; private set; }
}
}
O que importa aqui é perceber que a implementação funciona para ambos os modelos. Assim, o foco fica no CQRS básico, sem distrações.
Comandos: a requisição
Sempre que estudamos um padrão, vale começar sem frameworks adicionais, porque assim aprendemos como as coisas funcionam de fato. Com CQRS e Mediator, o raciocínio é o mesmo: primeiro o básico, depois a biblioteca.
Por isso, não vamos focar em reaproveitamento de código agora. Sim, poderíamos ter uma classe base para comandos, mas este código é apenas uma demonstração e não deve ir para produção.
Vamos então criar o comando que cadastra um novo cliente. Um comando representa uma ação de escrita que a aplicação executa.
Essa ação contém apenas as informações necessárias para criar o cliente, ou seja, nome e e-mail. O identificador, por outro lado, surge de forma automática.
namespace Shop.Domain.Commands.Requests
{
public class CreateCustomerRequest
{
public string Name { get; set; }
public string Email { get; set; }
}
}
Um pedido seria outro bom exemplo. Para criá-lo, precisamos apenas da lista de produtos do carrinho e do usuário que está comprando.
Todas as outras informações, como data, número, valor e estado, nascem dentro da aplicação. Afinal, você não quer que o frontend envie o valor total do pedido, não é mesmo?
Comandos: a resposta
Depois de enviar a requisição, surge uma dúvida: o que devemos retornar para a tela? Vamos supor que tudo deu certo e que queremos devolver o nome, o e-mail, o identificador e a data da requisição.
Não podemos retornar a entidade Customer, porque ela não tem a data. Da mesma forma, o CreateCustomerRequest também não serve, pois não tem o identificador nem a data.
Embora um objeto padrão de retorno resolva parte dos cenários, às vezes queremos uma resposta customizada. Dessa forma, criamos também o retorno do comando em Domain/Commands/Responses/CreateCustomerResponse.cs.
using System;
namespace Shop.Domain.Commands.Responses
{
public class CreateCustomerResponse
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public DateTime Date { get; set; }
}
}
Pronto, agora temos uma entrada e uma saída. Juntas, elas compõem a ação de criar um novo cliente.
Handlers: onde o fluxo acontece
Com os comandos criados, precisamos pensar no fluxo de execução da ação. Esse ponto varia bastante conforme a quantidade de regras do seu domínio.
Em fluxos grandes, talvez valha a pena quebrar a ação em partes menores para facilitar o reuso. No nosso cenário, cadastrar um cliente envolve três passos:
- Verificar se o e-mail já está cadastrado.
- Incluir o cliente no banco de dados.
- Enviar um e-mail de boas-vindas.
É um fluxo simples, mas continua sendo um fluxo. Inclusive, ele merece testes de unidade próprios.
Quando recebemos um comando, alguém precisa manipular essas informações. Esse papel cabe aos handlers, ou manipuladores, que recebem um comando, executam um fluxo e devolvem uma resposta.
Tanto no CQRS puro quanto na dupla CQRS e Mediator, o handler continua sendo a peça central do fluxo.
Antes do handler, vale declarar as abstrações que ele consome. Neste exemplo, as interfaces de repositório e de e-mail ficam bem enxutas.
using System;
using Shop.Domain.Entities;
using Shop.Domain.Queries.Responses;
namespace Shop.Domain.Repositories
{
public interface ICustomerRepository
{
void Save(Customer customer);
FindCustomerByIdResponse GetCustomerById(Guid id);
}
}
namespace Shop.Domain.Services
{
public interface IEmailService
{
void Send(string name, string email);
}
}
Em seguida, criamos a interface do manipulador em Domain/Handlers/ICreateCustomerHandler.cs. Ela será útil para gerenciar as dependências logo adiante.
using Shop.Domain.Commands.Requests;
using Shop.Domain.Commands.Responses;
namespace Shop.Domain.Handlers
{
public interface ICreateCustomerHandler
{
CreateCustomerResponse Handle(CreateCustomerRequest command);
}
}
Agora vem a implementação, que também fica na pasta Domain/Handlers.
using System;
using Shop.Domain.Commands.Requests;
using Shop.Domain.Commands.Responses;
using Shop.Domain.Entities;
using Shop.Domain.Repositories;
using Shop.Domain.Services;
namespace Shop.Domain.Handlers
{
public class CreateCustomerHandler : ICreateCustomerHandler
{
private readonly ICustomerRepository _repository;
private readonly IEmailService _emailService;
public CreateCustomerHandler(ICustomerRepository repository, IEmailService emailService)
{
_repository = repository;
_emailService = emailService;
}
public CreateCustomerResponse Handle(CreateCustomerRequest command)
{
// Aplicar validações rápidas (fail fast)
// Cria a entidade
var customer = new Customer(command.Name, command.Email);
// Persiste a entidade no banco
_repository.Save(customer);
// Envia o e-mail de boas-vindas
_emailService.Send(customer.Name, customer.Email);
// Retorna a resposta
return new CreateCustomerResponse
{
Id = customer.Id,
Name = customer.Name,
Email = customer.Email,
Date = DateTime.Now
};
}
}
}
Basicamente, o método Handle recebe um CreateCustomerRequest e devolve um CreateCustomerResponse. Enquanto isso, ele usa o repositório para persistir a entidade e o serviço para enviar o e-mail.
Repare que o handler depende apenas de interfaces. Dessa forma, seguimos a boa prática de depender de abstrações e facilitamos o uso de mocks nos testes de unidade.
Registrando as dependências sem o mediador
O handler gerou dependências que precisam ser resolvidas. Se você ainda não domina esse assunto, leia antes o artigo sobre injeção de dependência no ASP.NET Core.
No ASP.NET Core 3, o lugar certo é o método ConfigureServices do Startup.cs. Lembre também de registrar as implementações concretas do repositório e do serviço de e-mail.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddTransient<ICreateCustomerHandler, CreateCustomerHandler>();
}
A partir desse momento, o controlador já consegue chamar o handler. Ele recebe o CreateCustomerRequest e devolve o CreateCustomerResponse.
using Microsoft.AspNetCore.Mvc;
using Shop.Domain.Commands.Requests;
using Shop.Domain.Handlers;
namespace Shop.Controllers
{
[ApiController]
[Route("customers")]
public class CustomerController : ControllerBase
{
[HttpPost]
[Route("")]
public IActionResult Create(
[FromServices] ICreateCustomerHandler handler,
[FromBody] CreateCustomerRequest command)
{
var response = handler.Handle(command);
return Ok(response);
}
}
}
O atributo FromServices injeta o handler diretamente na action. Assim, cada endpoint pede apenas o que realmente usa.
Guarde esse registro manual na memória, porque mais adiante vamos compará-lo com a versão de CQRS e Mediator.
Queries no CQRS sem Mediator
No CQRS, as consultas não são muito diferentes dos comandos. Antes de chegar ao Mediator, veja como fica a requisição de busca por identificador.
using System;
namespace Shop.Domain.Queries.Requests
{
public class FindCustomerByIdRequest
{
public Guid Id { get; set; }
}
}
A resposta segue a mesma lógica e traz apenas o que a tela precisa exibir.
using System;
namespace Shop.Domain.Queries.Responses
{
public class FindCustomerByIdResponse
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
}
Novamente, precisamos de um handler, e começamos pela interface.
using Shop.Domain.Queries.Requests;
using Shop.Domain.Queries.Responses;
namespace Shop.Domain.Handlers
{
public interface IFindCustomerByIdHandler
{
FindCustomerByIdResponse Handle(FindCustomerByIdRequest query);
}
}
Em seguida, implementamos o manipulador da consulta.
using Shop.Domain.Queries.Requests;
using Shop.Domain.Queries.Responses;
using Shop.Domain.Repositories;
namespace Shop.Domain.Handlers
{
public class FindCustomerByIdHandler : IFindCustomerByIdHandler
{
private readonly ICustomerRepository _repository;
public FindCustomerByIdHandler(ICustomerRepository repository)
{
_repository = repository;
}
public FindCustomerByIdResponse Handle(FindCustomerByIdRequest query)
{
// TODO: lógica de leitura, se houver
// Retorna o resultado
return _repository.GetCustomerById(query.Id);
}
}
}
Note que o repositório já retorna o response, e não uma entidade. Além disso, quando não existe lógica de leitura, chamar o repositório direto no controlador também é aceitável.
Agora registramos o novo handler no Startup.cs.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddTransient<ICreateCustomerHandler, CreateCustomerHandler>();
services.AddTransient<IFindCustomerByIdHandler, FindCustomerByIdHandler>();
}
Com isso, o controlador ganha o endpoint de leitura.
[HttpGet]
[Route("")]
public IActionResult GetById(
[FromServices] IFindCustomerByIdHandler handler,
[FromQuery] FindCustomerByIdRequest query)
{
var result = handler.Handle(query);
return Ok(result);
}
Por ser um método GET, a requisição vem do FromQuery, e não do FromBody. Pronto, concluímos o CQRS básico sem nenhum framework adicional.
Os limites da abordagem manual
Neste ponto, surgem algumas preocupações. À medida que a aplicação cresce, como fica o Startup.cs com dependências registradas uma a uma?
Além disso, cada handler exige uma interface própria para a injeção de dependência. Em outras palavras, o número de arquivos cresce rapidamente.
Ter mais arquivos não é ruim, porque isso também é organização. Mesmo assim, dá para melhorar alguns pontos com um padrão chamado Mediator, e é aí que CQRS e Mediator se encontram.
O que é o padrão Mediator
A tradução de mediator é mediador, e é exatamente isso que o padrão faz. Ele intermedeia a comunicação entre os objetos e abstrai esse trabalho para nós.
Em resumo, recebemos um comando e pedimos ao mediador que resolva o restante. Então, ele encontra o handler responsável e o executa.
Segundo o guia de microsserviços .NET da Microsoft, o mediador funciona como um barramento em memória. Ele redireciona cada comando para o handler correto com base no tipo recebido.
CQRS e Mediator precisam andar juntos?
Não necessariamente. Como vimos no exemplo anterior, CQRS e Mediator são padrões independentes, e fizemos o processo completo sem o mediador.
Outro ponto importante: abstrair as dependências quebra o Explicit Dependencies Principle. Esse princípio defende que classes e métodos declarem explicitamente tudo o que precisam.
Quando ocultamos as dependências, os problemas tendem a aparecer só em tempo de execução. Consequentemente, fica mais difícil capturar e descobrir erros.
Entenda bem: não estamos dizendo para evitar o padrão. A mensagem é que CQRS e Mediator não estão atrelados, e cada time deve avaliar o custo e o benefício.
Instalando o MediatR
No .NET, o pacote que implementa o padrão Mediator é o MediatR, criado por Jimmy Bogard. Na versão usada neste artigo, instalamos o pacote principal e o pacote de integração com a injeção de dependência.
dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
Isso é tudo o que precisamos para começar. A partir daqui, refatoramos o código de CQRS e Mediator passo a passo.
Refatorando requests e handlers para CQRS e Mediator
Para o MediatR resolver as dependências, primeiro as requisições precisam implementar a interface IRequest<TResponse>. O tipo genérico indica qual resposta aquela requisição produz.
using MediatR;
using Shop.Domain.Commands.Responses;
namespace Shop.Domain.Commands.Requests
{
public class CreateCustomerRequest : IRequest<CreateCustomerResponse>
{
public string Name { get; set; }
public string Email { get; set; }
}
}
O mesmo vale para a consulta.
using System;
using MediatR;
using Shop.Domain.Queries.Responses;
namespace Shop.Domain.Queries.Requests
{
public class FindCustomerByIdRequest : IRequest<FindCustomerByIdResponse>
{
public Guid Id { get; set; }
}
}
Handlers com IRequestHandler
Agora as interfaces específicas de cada handler deixam de existir. No lugar delas, o MediatR procura sozinho o handler capaz de manipular o comando enviado.
Para isso, cada handler implementa IRequestHandler<TRequest, TResponse>, como no código abaixo.
using System;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Shop.Domain.Commands.Requests;
using Shop.Domain.Commands.Responses;
using Shop.Domain.Entities;
using Shop.Domain.Repositories;
using Shop.Domain.Services;
namespace Shop.Domain.Handlers
{
public class CreateCustomerHandler : IRequestHandler<CreateCustomerRequest, CreateCustomerResponse>
{
private readonly ICustomerRepository _repository;
private readonly IEmailService _emailService;
public CreateCustomerHandler(ICustomerRepository repository, IEmailService emailService)
{
_repository = repository;
_emailService = emailService;
}
public Task<CreateCustomerResponse> Handle(CreateCustomerRequest request, CancellationToken cancellationToken)
{
// Aplicar validações rápidas (fail fast)
// Cria a entidade
var customer = new Customer(request.Name, request.Email);
// Persiste a entidade no banco
_repository.Save(customer);
// Envia o e-mail de boas-vindas
_emailService.Send(customer.Name, customer.Email);
// Retorna a resposta
var result = new CreateCustomerResponse
{
Id = customer.Id,
Name = customer.Name,
Email = customer.Email,
Date = DateTime.Now
};
return Task.FromResult(result);
}
}
}
Repare que a assinatura do Handle mudou para seguir o contrato da interface. Agora ela é assíncrona e recebe um CancellationToken.
O handler da consulta passa pela mesma transformação.
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Shop.Domain.Queries.Requests;
using Shop.Domain.Queries.Responses;
using Shop.Domain.Repositories;
namespace Shop.Domain.Handlers
{
public class FindCustomerByIdHandler : IRequestHandler<FindCustomerByIdRequest, FindCustomerByIdResponse>
{
private readonly ICustomerRepository _repository;
public FindCustomerByIdHandler(ICustomerRepository repository)
{
_repository = repository;
}
public Task<FindCustomerByIdResponse> Handle(FindCustomerByIdRequest request, CancellationToken cancellationToken)
{
// TODO: lógica de leitura, se houver
// Retorna o resultado
var result = _repository.GetCustomerById(request.Id);
return Task.FromResult(result);
}
}
}
Registrando o MediatR no Startup
Voltando ao Startup.cs, podemos remover os registros manuais dos handlers. O MediatR passa a cuidar dessa parte.
using System.Reflection;
using MediatR;
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddMediatR(Assembly.GetExecutingAssembly());
}
O método AddMediatR varre o assembly informado e registra todos os handlers encontrados. Dessa forma, cada novo comando funciona sem mexer no Startup.cs.
Controlador enxuto com CQRS e Mediator
Nos controladores, basta pedir uma instância de IMediator e chamar o método Send. Como Send retorna uma Task, a action precisa ser assíncrona e aguardar o resultado com await.
[HttpPost]
[Route("")]
public async Task<IActionResult> Create(
[FromServices] IMediator mediator,
[FromBody] CreateCustomerRequest command)
{
var response = await mediator.Send(command);
return Ok(response);
}
O mesmo vale para a consulta.
[HttpGet]
[Route("")]
public async Task<IActionResult> GetById(
[FromServices] IMediator mediator,
[FromQuery] FindCustomerByIdRequest query)
{
var result = await mediator.Send(query);
return Ok(result);
}
Esse detalhe do await é importante. Sem ele, o Ok serializaria a própria Task, e não a resposta do handler.
Com CQRS e Mediator, o controlador conhece apenas o IMediator. Portanto, fica mais fácil criar um controlador base que padroniza as respostas da API.
Prós e contras de CQRS e Mediator
Antes de adotar CQRS e Mediator no seu projeto, vale colocar os dois lados na balança.
Do lado positivo, o mediador abstrai as dependências e simplifica o registro no container. Além disso, o MediatR oferece pipeline behaviors, que aplicam logs, validação e auditoria a todos os comandos de forma centralizada.
Do lado negativo, as dependências deixam de aparecer na assinatura do controlador. Por isso, um handler ausente ou mal registrado só aparece em tempo de execução.
Também existe uma camada extra de indireção. Em outras palavras, navegar do controlador até o handler exige procurar pelo tipo da requisição, e não por uma chamada direta.
O próprio CQRS tem custos, como lembra a Microsoft. Em domínios simples, com telas de CRUD puro, o padrão pode acrescentar complexidade sem retorno proporcional.
Versões e licença do MediatR hoje
Este tutorial usa o ASP.NET Core 3 e a geração do MediatR daquela época. Desde então, algumas mudanças relevantes aconteceram na biblioteca.
A partir da versão 12, o pacote MediatR.Extensions.Microsoft.DependencyInjection foi descontinuado, e a integração com o container passou para o pacote principal. Portanto, em versões novas, o registro usa AddMediatR com RegisterServicesFromAssembly.
Além disso, em julho de 2025 o MediatR 13 passou a ter licença dupla, com uma opção comercial. As versões 12.5 e anteriores continuam sob a licença Apache, e existe uma edição Community gratuita para alguns perfis de uso.
Assim, ao escolher entre CQRS e Mediator com MediatR ou um mediador próprio, considere também a licença. Muitos times implementam um despachante simples quando precisam apenas do roteamento de comandos.
Conclusão sobre CQRS e Mediator
Não precisamos usar o padrão Mediator junto com o CQRS. Os prós do mediador são a abstração das dependências e algumas facilidades no código.
Os contras, por outro lado, envolvem problemas causados pela falta de dependências explícitas, que muitas vezes só surgem em tempo de execução.
Se quiser aprofundar os fundamentos que sustentam esse desenho, revise os princípios SOLID. Com eles claros, decidir quando usar CQRS e Mediator fica muito mais simples.
FAQ
CQRS exige dois bancos de dados?
Não. A forma mais simples de CQRS usa o mesmo banco e separa apenas os modelos e os fluxos de leitura e escrita.
Posso usar CQRS sem MediatR?
Sim. Basta criar handlers e registrá-los na injeção de dependência, como fizemos na primeira parte do artigo.
Qual a diferença entre comando e consulta?
O comando altera o estado do sistema, enquanto a consulta apenas lê dados. Uma consulta nunca deve gerar efeitos colaterais.
Por que o controlador precisa usar await com o MediatR?
Porque o método Send retorna uma Task. Sem o await, a action devolveria a tarefa em vez da resposta do handler.
O MediatR ainda é gratuito?
As versões 12.5 e anteriores seguem sob a licença Apache. A partir da versão 13, existe licença dupla, com uma edição Community gratuita para casos específicos.