|
Neste tutorial veremos como criar uma aplicação ASP.NET Core usando a abordagem da Clean Architecture e realizar um CRUD usando o Dapper. |

Vamos implementar no projeto Application o padrão CQRS - Command Query Responsability Segregation- que é um padrão de arquitetura de desenvolvimento de software que permite realizar a separação de leitura e escrita em dois modelos: Query e Command, uma para leitura e outra para escrita de dados, respectivamente.

Assim, o CQRS separa as responsabilidades em termos de leitura e escrita, o que faz muito sentido. Esse padrão foi originado do Princípio Command and Query Separation desenvolvido por Bertrand Meyer, e, esta definido na Wikipedia da seguinte forma :
"... todo método deve ser um comando que executa uma ação ou uma consulta que retorna dados ao chamador, mas não ambos. Em outras palavras, fazer uma pergunta não deve mudar a resposta. [1] Mais formalmente, os métodos devem retornar um valor apenas se forem referencialmente transparentes e, portanto, não apresentarem efeitos colaterais."
A utilização do CQRS não é recomendada quando :
A implementação que iremos fazer vai usar a biblioteca MediatR que funciona da seguinta forma:
Basicamente a
biblioteca MediatR possui dois tipos de mensagens que ele despacha :
Mensagens de Request/Reponse despachada para um único handler;
Mensagens de
Notificação despachada para múltiplos handlers;
Aqui temos dois
componentes principais chamados de Request e Handler.
Request
→ Representa a mensagem a ser processada;
Handler → Faz o
processamento de determinada(s) mensagen(s);
Esses componentes são
implementados usando as interfaces: IRequest e IRequestHandler
Cada
Handler normalmente irá tratar um único Request e assim podemos ter classes
menores e mais simples.
Essas interfaces atuam em cenários de comandos e
consultas primeiro criando a mensagem com IRequest e a seguir
realizando o processamento com IRequestHandler.
E temos
dois dois tipos de requests no MediatR :
-
Aqueles que não retornam um valor representados pela interface IRequest;
- E aqueles que retornam um valor,
representados pela interface IRequest<T>;
Cada interface Request
tem sua própria interface de Handler. Assim temos :
IRequestHandler<T> - Para processamento que não
retornam valor;
IRequestHandler<T, U> - Para processamento que retorna um valor U;
Esses dois componentes não fazem nada sozinhos eles precisam de um
intermediador, que será responsável por receber um Request e invocar o Handler
associado á ele.
Para isso temos um componente chamado Mediator que
implementa a interface IMediator, por onde deveremos interagir com as demais
classes.
Usando a interface IMediator nossas classes não irão saber quem
ou quais componentes irão realizar determinada ação; apenas enviamos a mensagem
para o Mediator e ele irá se encarregar de chamar a classe que irá executar o
que precisamos.
Assim, o MediatR simplifica a implementação do padrão
CQRS ao fornecer uma abstração eficaz para a comunicação entre componentes,
promovendo a separação de responsabilidades entre comandos, consultas e
notificações.
Definindo os recursos no projeto Application
Vamos incluir neste projeto o pacote MediatR.
Nota: Para instalar use o comando: Install-Package <nome> --version X.X.X
A seguir vamos criar a pasta Products e nesta pasta vamos criar as pastas Commands e Queries onde vamos definir os comandos e consultas CQRS.
Implementando os comandos CQRS
Vamos iniciar com os comandos criando o comando para criar um novo produto. Para isso vamos criar a classe de comando e o seu handler separados, sabendo que podemos mesclar essas duas classes para ter um código mais simples.
1- Criando o comando CreateProductCommand
public class CreateProductCommand : IRequest<Product> { public string? Name { get; set; } public decimal Price { get; set; } public string? Description { get; set; } public double Stock { get; set; } }
2- Criando o seu manipulador na classe CreateProductCommandHandler
public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, Product> { private readonly IProductRepository _productRepository; public CreateProductCommandHandler(IProductRepository productRepository) { _productRepository = productRepository; } public async Task<Product> Handle(CreateProductCommand request, CancellationToken cancellationToken) { var product = new Product { Name = request.Name, Price = request.Price, Description = request.Description, Stock = request.Stock }; return await _productRepository.Add(product); } } |
Vou explicar esta primeira implementação com mais detalhes :
CreateProductCommand :
Essa classe define um comando chamado
CreateProductCommand, que implementa a interface
IRequest<Product>. O comando contém os
dados necessários para criar um novo produto, como nome, preço, descrição e
estoque.
CreateProductCommandHandler -
Essa classe é o manipulador do comando
CreateProductCommand. Implementa a
interface IRequestHandler<CreateProductCommand,
Product>, o que significa que é capaz de lidar com solicitações do
tipo CreateProductCommand e
retornar um objeto Product.
O método Handle é chamado quando um comando
CreateProductCommand é recebido. Ele
cria um novo objeto Product com base nos dados fornecidos no
comando e, em seguida, usa o repositório de produtos (_productRepository)
para adicionar o produto ao banco de dados. Finalmente, o método retorna o
produto recém-criado.
Parâmetros do método Handle:
request: O objeto
CreateProductCommand que contém os
dados necessários para criar o produto.cancellationToken: Um
token de cancelamento que pode ser usado para cancelar a execução da
solicitação.Desta forma esse código implementa a separação entre comandos (responsáveis por alterar o estado do sistema) e manipuladores de comandos (responsáveis por executar a lógica associada a um comando específico). Essa separação ajuda a manter o código organizado e permite que diferentes partes do sistema sejam modificadas e testadas de forma independente.
Vejamos a seguir os demais comandos e seus handlers:
3- Comando UpdateProductCommand
public class UpdateProductCommand : IRequest<Product> { public int Id { get; set; } public string? Name { get; set; } public decimal Price { get; set; } public string? Description { get; set; } public double Stock { get; set; } } |
4- UpdateProductCommandHandler
public class UpdateProductCommandHandler : IRequestHandler<UpdateProductCommand, Product> { private readonly IProductRepository _productRepository; public UpdateProductCommandHandler(IProductRepository productRepository) { _productRepository = productRepository; } public async Task<Product> Handle(UpdateProductCommand request, CancellationToken cancellationToken) { var existingProduct = await _productRepository.GetProductById(request.Id); if (existingProduct == null) throw new Exception("Product not found"); existingProduct.Name = request.Name; existingProduct.Price = request.Price; existingProduct.Description = request.Description; existingProduct.Stock = request.Stock; await _productRepository.Update(existingProduct); return existingProduct; } } |
5- DeleteProductCommand
public class DeleteProductCommand : IRequest<Product> { public int Id { get; set; } } |
5- DeleteProductCommandHandler
public class DeleteProductCommandHandler : IRequestHandler<DeleteProductCommand, Product> { private readonly IProductRepository _productRepository; public DeleteProductCommandHandler(IProductRepository productRepository) { _productRepository = productRepository; } public async Task<Product> Handle(DeleteProductCommand request, CancellationToken cancellationToken) { var product = await _productRepository.Delete(request.Id); return product; } } |
Implementando as consultas CQRS
No padrão de arquitetura CQRS (Command Query Responsibility Segregation), há uma distinção clara entre as operações de leitura (queries) e as operações de escrita (commands) em um sistema. As consultas CQRS são responsáveis por lidar com as operações de leitura, ou seja, elas são responsáveis por recuperar dados do sistema, sem modificar o estado do mesmo.
Vamos agora implementar duas consultas na pasta Queries:
1- GetAllProductsQuery
public record GetAllProductsQuery : IRequest<IEnumerable<Product>>; |
2- GetAllProductsQueryHandler
public class GetAllProductsQueryHandler : IRequestHandler<GetAllProductsQuery, IEnumerable<Product>> { private readonly IProductRepository? _productDapperRepository; public GetAllProductsQueryHandler(IProductRepository? productDapperRepository) { _productDapperRepository = productDapperRepository; } public async Task<IEnumerable<Product>> Handle(GetAllProductsQuery request, CancellationToken cancellationToken) { var products = await _productDapperRepository.GetProducts(); return products; } } |
3- GetProductByIdQuery
public record GetProductByIdQuery(int Id) : IRequest<Product>; |
4- GetProductByIdQueryHandler
public class GetProductByIdQueryHandler : IRequestHandler<GetProductByIdQuery, Product> { private readonly IProductRepository? _productDapperRepository; public GetProductByIdQueryHandler(IProductRepository? productDapperRepository) { _productDapperRepository = productDapperRepository; } public async Task<Product> Handle(GetProductByIdQuery request, CancellationToken cancellationToken) { var product = await _productDapperRepository.GetProductById(request.Id); return product; } } |
Com isso temos os comandos e consultas criados e prontos para serem usados.
Definindo os recursos no projeto Products
O projeto Products representa a nossa camada de apresentação e nela vamos criar o controlador ProductsController na pasta Controllers do projeto.
Antes de iniciar a implementação vamos remover o controlador e a classe WeatherForecast criadas no projeto.
A seguir vamos definir o código na classe Program onde vamos invocar o método de extensão AddInfrastructure e também o código que vai criar o banco de dados Sqlite:
var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddInfrastructure(builder.Configuration);
var app = builder.Build(); CreateDatabase(app);
if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); static void CreateDatabase(WebApplication app) { var serviceScope = app.Services.CreateScope(); var dataContext = serviceScope.ServiceProvider.GetService<AppDbContext>(); dataContext?.Database.EnsureCreated(); } |
No arquivo appsettings.json do projeto vamos definir a string de conexão com o SQLite:
{ "ConnectionStrings": { "Sqlite": "Data Source=ProductsDB.db" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" }
A seguir na pasta Controllers vamos criar o controlador ProductsController :
[Route("[controller]")] [ApiController] public class ProductsController : ControllerBase { private readonly IMediator _mediator; public ProductsController(IMediator mediator) { _mediator = mediator; } [HttpGet("Products")] public async Task<IEnumerable<Product>> GetAll() { var query = new GetAllProductsQuery(); var produtos = await _mediator.Send(query); return produtos; } [HttpGet("{id}")] public async Task<Product> GetProduct(int id) { var query = new GetProductByIdQuery(id); var produto = await _mediator.Send(query); return produto; } [HttpPost] public async Task<ActionResult<Product>> Create(CreateProductCommand command) { var product = await _mediator.Send(command); if (product is null) return BadRequest(); return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product); } [HttpPut("{id}")] public async Task<ActionResult<Product>> Update(int id, UpdateProductCommand command) { if (id != command.Id) return BadRequest(); var result = await _mediator.Send(command); //return NoContent(); return result; } [HttpDelete("{id}")] public async Task<ActionResult<Product>> Delete(int id) { var command = new DeleteProductCommand { Id = id }; var result = await _mediator.Send(command); //return NoContent(); if (result is null) return BadRequest(); return result; } } |
Executando o projeto teremos na interface do Swagger a exibição dos endpoints criados para realizar o CRUD de Produtos usando o Dapper:

Com isso concluímos a implementação da nossa aplicação ASP.NET Core usando a Clean Architecture onde implemetamos o padrão CQRS usando o MediatR e o Dapper.
Pegue o projeto neste link:
ApiDapperClean.zip
"Falou-lhes, pois, Jesus outra vez, dizendo: Eu sou
a luz do mundo; quem me segue não andará em trevas, mas terá a luz da vida."
João 8:12
Referências: