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

Continuando  o artigo anterior vamos agora implementar os recursos nos projetos
Application e Products.

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.

  1. Command - Representa tudo o que altera o estado de uma entidade (insert, update, delete).
  2. Query  - Não modifica o estado das entidades e retorna um DTO que não encapsula nenhum conhecimento de domínio;

                                                   

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 comandoInstall-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:

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:


José Carlos Macoratti