ASP .NET Core 3.1  - Usando CQRS com MediatR


Neste artigo vamos tratar sobre o padrão CQRS na ASP .NET Core 3.1  e sua implementação com o MediatR,  usando o EF Core na abordagem Code First.

O CQRS tem ganhado notoriedade ultimamente e também ajuda a arquitetar a solução ASP .NET para acomodar a arquitetura Onion.

Hoje vamos realizar a implementação do padrão CQRS em uma Web API, e também vamos usar o padrão Mediator.

O padrão Mediator cuida das interações entre diferentes objetos, fornecendo uma classe mediadora que coordena todas as interações entre os objetos, com o objetivo de diminuir o acoplamento e a dependência entre eles e facilitando as manutenções. Assim, usando o padrão nenhum objeto conversa diretamente com outro, sempre um objeto utilizará a classe mediadora para conversar indiretamente com outros objetos.

O CQRS - Command Query Responsibility Segregation, é 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 refere-se a um comando de banco de dados, que pode ser uma operação Inserir/Atualizar ou Excluir;
  2. Query significa Consultar dados de uma fonte.;

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 :

"Ele afirma que 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."

O problema com os padrões arquitetônicos tradicionais é que o mesmo modelo de dados ou DTO é usado para consultar e atualizar uma fonte de dados. Essa pode ser a abordagem ideal quando seu aplicativo está relacionado apenas a operações CRUD e nada mais. Mas quando seus requisitos começam a ficar complexos, essa abordagem básica pode ser um desastre.

Em aplicações práticas, sempre há uma incompatibilidade entre as formas de leitura e gravação de dados, como as propriedades extras que você pode precisar para atualizar. As operações paralelas podem até levar à perda de dados nos piores casos. Isso significa que você ficará preso a apenas um Objeto de Transferência de Dados por todo o tempo de vida do aplicativo, a menos que opte por introduzir outro DTO, o que, por sua vez, pode quebrar a arquitetura do aplicativo.

A ideia do CQRS é permitir que um aplicativo funcione com diferentes modelos. Resumindo, você tem um modelo que tem os dados necessários para atualizar um registro, outro modelo para inserir um registro e outro para consultar um registro. Isso oferece flexibilidade com cenários variados e complexos. Você não precisa depender de apenas um DTO para todas as operações CRUD implementando o CQRS.

Para uma visão geral sobre o CQRS leia o meu artigo: .NET - Entendendo o padrão CQRS

recursos usados:

Implementando o padrão CQRS

Vamos criar um WebApi ASP .NET Core 3.1 para mostrar a implementação e entender melhor o Padrão CQRS.

Vamos construir um endpoint de API que faça operações CRUD para uma entidade Produto, ou seja, Criar/Excluir/ Atualizar o registro de um produto no Banco de Dados. Vamos usar Entity Framework Core como o ORM para acessar os dados no banco de dados local.

Embora não vamos aplicar uma arquitetura avançada vamos tentar ao máximo ficar aderentes ao Clean Code.

Abra o VS 2019 Community e crie um novo projeto via menu File-> New Project;

Selecione o template ASP .NET Core Web Application, e, Informe o nome da solução Api_CQRS (ou outro nome a seu gosto).

A seguir selecione .NET Core e ASP .NET Core 3.1 e marque o template API e as configurações conforme figura abaixo:

Depois que o projeto foi criado, precisamos adicionar a referência aos seguintes pacotes:

Nota:  Para instalar use o comandoInstall-Package <nome> --version X.X.X

Estamos instalando o pacote MediatR pois vamos aplicar o padrão Mediator e com ela não precisamos nos preocupar em desenvolver a classe mediadora de comunicação entre os objetos, pois ela fornece interfaces prontas para uso, diminuindo a complexidade do código.

Ao final, verificando os pacotes instalados no projeto deveremos obter o resultado abaixo:

Criando a Entidade Produto

Crie uma pasta Models no projeto e nesta pasta inclua a classe Produto que representa a nossa entidade do modelo de domínio:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Api_CQRS.Models
{
    public class Produto
    {
        public int Id { get; set; }
        [StringLength(80, MinimumLength= 4)]
        public string Nome { get; set; }
        public string CodigoBarras { get; set; }
        public bool Ativo { get; set; } = true;
        [StringLength(80, MinimumLength = 4)]
        public string Descricao { get; set; }
        [Column(TypeName = "decimal(10,2)")]
        public decimal Taxa { get; set; }        
        [Column(TypeName = "decimal(10,2)")]
        public decimal Preco { get; set; }
    }
}

Temos aqui um modelo anêmico onde estamos definindo atributos Data Annotations para que quando da aplicação do Migrations as colunas nvarchar tenham tamanho definidos e as colunas decimal estejam definidas com a precisão correta.

Nota: Deixamos essa entidade sem comportamentos para simplificar o código.

A seguir vamos registrar o serviço no método ConfigureServices da classe Startup :

public void ConfigureServices(IServiceCollection services)
{
            services.AddControllers();
            services.AddDbContext<AppDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection"),
                           b => b.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName)));
}

Aqui passamos a informação do contexto, do provedor usado e da string de conexão com o banco de dados SQL Server, e, usamos o método MigrationsAssembly que configura o assembly onde as migrações serão mantidas para este contexto.

Precisamos agora definir a string de conexão com o nome DefaultConnection no arquivo appsettings.json:

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=****\\sqlexpress;Initial Catalog=ApiCQRSDB;Integrated Security=True"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

Aqui você deve informar a string de conexão da instância do seu SQL Server local.

Para gerar o banco de dados ApiCQRSDB e a tabela Produtos vamos usar os seguintes comandos do Migrations:

1- add-migration "Inicial"

Cria o script de migração definindo os métodos Up() e Down() contendo os comandos SQL;

2- update-database

Aplica o script de migração ao banco de dados gerando o banco e as tabelas

Após podemos confirmar a criação do banco de dados e da tabela no Server Explorer do VS 2019:

Aplicando o padrão Mediator

O padrão Mediator reduz drasticamente o acoplamento entre vários componentes de um aplicativo, fazendo com que eles se comuniquem indiretamente, geralmente por meio de um objeto mediador especial.

Assim, vamos aplicar padrão Mediator para reduzir o acoplamento em nosso projeto visto que queremos manter os controladores o mais enxuto possível, e para isso vamos usar a biblioteca MediatR.

A biblioteca MediatR ajuda a implementar o padrão Mediator, e por isso já instalamos os pacotes dessa biblioteca em nosso projeto, agora para registrar a biblioteca, vamos incluir o código abaixo destacado em azul no método ConfigureServices :

public void ConfigureServices(IServiceCollection services)
{
            services.AddControllers();
            services.AddDbContext<AppDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection"),
                    b => b.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName)));
            services.AddMediatR(Assembly.GetExecutingAssembly());
}

Definindo o CRUD via CQRS com MediatR

Vamos agora implementar as operações CRUD usando uma abordagem CQRS.

Aqui eu vou fazer uma abordagem bem simples implementando as operações CRUD com os comandos e as consultas usando o MediatR.

Vamos criar uma pasta chamada Recursos no diretório raiz do projeto e nesta pasta criar as subpastas Commands e Queries.

1- Definindo as consultas

Vamos iniciar criando as consultas na pasta Queries:

a- Consultando um produto pelo Id

using Api_CQRS.Models;
using MediatR;
namespace Api_CQRS.Recursos.Queries
{
    public class GetProdutoPorIdQuery : IRequest<Produto>
    {
        public int Id { get; set; }
    }
}

Aqui definimos a consulta GetProdutoPorIdQuery que herda de IRequest e esta retornando como resposta um objeto Produto.  Estamos enviando um request para o Mediator que vai chamar a classe que vai executar o comando; tudo de forma transparente.

Agora vamos criar a classe GetProdutoPorIdQueryHandler  que vai executar o comando e retornar um Produto.

using Api_CQRS.Context;
using Api_CQRS.Models;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Threading;
using System.Threading.Tasks;
namespace Api_CQRS.Recursos.Queries
{
    public class GetProdutoPorIdQueryHandler : IRequestHandler<GetProdutoPorIdQuery, Produto>
    {
        private readonly AppDbContext _context;
        public GetProdutoPorIdQueryHandler(AppDbContext context)
        {
            _context = context;
        }
        public async Task<Produto> Handle(GetProdutoPorIdQuery request, CancellationToken cancellationToken)
        {
            return await _context.Produtos.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);
        }
    }
}

Esse código vai executar o comando e retornar um Produto pelo Id enviado no request que virá do Controller.

b- Retornando todos os produtos

using Api_CQRS.Models;
using MediatR;
using System.Collections.Generic;
namespace Api_CQRS.Recursos.Queries
{
    public class GetTodosProdutosQuery : IRequest<IEnumerable<Produto>>
    {
    }
}

Agora temos a classe que processa o comando e retorna uma lista de produtos (IEnumerable<Produto>):

using Api_CQRS.Context;
using Api_CQRS.Models;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Api_CQRS.Recursos.Queries
{
    public class GetTodosProdutosQueryHandler : IRequestHandler<GetTodosProdutosQuery, IEnumerable<Produto>>
    {
        private readonly AppDbContext _context;
        public GetTodosProdutosQueryHandler(AppDbContext context)
        {
            _context = context;
        }
      public async Task<IEnumerable<Produto>> Handle(GetTodosProdutosQuery request, CancellationToken cancellationToken)
      {
            return await _context.Produtos.ToListAsync();
      }
    }
}

Observe que eu não criei um repositório e estou usando o contexto do EF Core para acessar o banco de dados.

2- Definindo os comandos

Vamos agora definir os comandos na pasta Commands.

É aqui que temos a utilização do CQRS pela implementação do padrão Command composto de dois objetos:

  1. Command - Define ações que irão alterar o estado dos dados e os objetos;
  2. Command Handler - São responsáveis por executar as ações definidas pelos objetos Command;

Todas as classes implementam IRequest<T> onde especificamos o tipo de dados que será retornado quando o comando for processado, e, também, através da qual vinculamos os comandos com as classes Command Handlers. É assim que a  MediatR sabe qual objeto deve ser invocado quando um request for gerado.

a- ProdutoCreateCommand

public class ProdutoCreateCommand : IRequest<Produto>
{
        public string Nome { get; set; }
        public string CodigoBarras { get; set; }
        public bool Ativo { get; set; } = true;
        public string Descricao { get; set; }
        public decimal Taxa { get; set; }
        public decimal Preco { get; set; }
}

Note que esta classe implementa a interface IRequest que é a interface do MediatR usada para indicar que esse é um comando usado pelas classes Handlers que iremos criar para cada Command.

b- ProdutoCreateCommandHandler - Processa o ProdutoCreateCommand e retorna o Produto que foi criado:

using Api_CQRS.Context;
using Api_CQRS.Models;
using MediatR;
using System.Threading;
using System.Threading.Tasks;
namespace Api_CQRS.Recursos.Commands
{
    public class ProdutoCreateCommandHandler : IRequestHandler<ProdutoCreateCommand, Produto>
    {
        private readonly AppDbContext _context;
        public ProdutoCreateCommandHandler(AppDbContext context)
        {
            _context = context;
        }
        public async Task<Produto> Handle(ProdutoCreateCommand request, CancellationToken cancellationToken)
        {
            var produto = new Produto();
            produto.CodigoBarras = request.CodigoBarras;
            produto.Nome = request.Nome;
            produto.Preco = request.Preco;
            produto.Taxa = request.Taxa;
            produto.Descricao = request.Descricao;

            _context.Produtos.Add(produto);
            await _context.SaveChangesAsync();

            return produto;
        }
    }
}

c- ProdutoUpdateCommand

public class ProdutoUpdateCommand : IRequest<Produto>
{
        public int Id { get; set; }
        public string Nome { get; set; }
        public string CodigoBarras { get; set; }
        public bool Ativo { get; set; } = true;
        public string Descricao { get; set; }
        public decimal Taxa { get; set; }
        public decimal Preco { get; set; }
}

d- ProdutUpdateCommandHandler - Processa o ProdutoUpdateCommand e retorna o Produto que foi atualizado:

using Api_CQRS.Context;
using Api_CQRS.Models;
using MediatR;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Api_CQRS.Recursos.Commands
{
    public class ProdutoUpdateCommandHandler : IRequestHandler<ProdutoUpdateCommand, Produto>
    {
        private readonly AppDbContext _context;
        public ProdutoUpdateCommandHandler(AppDbContext context)
        {
            _context = context;
        }
        public async Task<Produto> Handle(ProdutoUpdateCommand request, CancellationToken cancellationToken)
        {
            var produto = _context.Produtos.Where(a => a.Id == request.Id).FirstOrDefault();
            if (produto == null)
            {
                return default;
            }
            else
            {
                produto.CodigoBarras = request.CodigoBarras;
                produto.Nome = request.Nome;
                produto.Preco = request.Preco;
                produto.Taxa = request.Taxa;
                produto.Descricao = request.Descricao;

                await _context.SaveChangesAsync();
                return produto;
            }
        }
    }
}

e- ProdutoDeleteCommand

public class ProdutoDeleteCommand : IRequest<Produto>
{
        public int Id { get; set; }     
}

f- ProdutDeleteCommandHandler - Processa o ProdutoDeleteCommand e retorna o Produto que foi deletado:

using Api_CQRS.Context;
using Api_CQRS.Models;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Api_CQRS.Recursos.Commands
{
    public class ProdutoDeleteCommandHandler : IRequestHandler<ProdutoDeleteCommand, Produto>
    {
        private readonly AppDbContext _context;
        public ProdutoDeleteCommandHandler(AppDbContext context)
        {
            _context = context;
        }
        public async Task<Produto> Handle(ProdutoDeleteCommand request, CancellationToken cancellationToken)
        {
            var produto = await _context.Produtos.Where(a => a.Id == request.Id).FirstOrDefaultAsync();
            _context.Remove(produto);

            await _context.SaveChangesAsync();
            return produto;
        }
    }
}

Assim para cada Command criamos Command Handler, embora possamos implementar um único objeto CommandHandler para tratar todos os Commands criados na aplicação.

Criando o controlador

Agora vamos criar o controlador da nossa aplicação.

Clique com o botão direito do mouse sobre a pasta Controllers e a seguir clique em Add->Controller;

A seguir escolha a opção API Controller - Empty e clique em Add;

Informe o nome ProdutosController e clique em Add.

using Microsoft.AspNetCore.Mvc;
namespace Api_CQRS.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProdutosController : ControllerBase
    {
    }
}

Ao final será criado o controlador acima onde vamos incluir o código abaixo:

using Api_CQRS.Context;
using Api_CQRS.Models;
using Api_CQRS.Recursos.Commands;
using Api_CQRS.Recursos.Queries;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Api_CQRS.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProdutosController : ControllerBase
    {
        private readonly IMediator _mediator;
        public ProdutosController(AppDbContext context, IMediator mediator)
        {
            _mediator = mediator;
        }
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Produto>>> GetProdutos()
        {
            try
            {
                var command = new GetTodosProdutosQuery();
                var response = await _mediator.Send(command);
                return Ok(response);
            }
            catch (Exception ex)
            {
                return BadRequest(ex.Message);
            }
        }
        [HttpGet("{id:int}")]
        public async Task<ActionResult<Produto>> GetProdutos(int id)
        {
            try
            {
                var command = new GetProdutoPorIdQuery { Id = id};
                var response = await _mediator.Send(command);
                return Ok(response);
            }
            catch (Exception ex)
            {
                return BadRequest(ex.Message);
            }
        }
        [HttpPost]
        public async Task<ActionResult<Produto>> PostProduto([FromBody] ProdutoCreateCommand command)
        {
            try
            {
                var response = await _mediator.Send(command);
                return Ok(response);
            }
            catch (Exception ex)
            {
                return BadRequest(ex.Message);
            }
        }
        [HttpDelete("{id:int}")]
        public async Task<ActionResult<Produto>> DeleteProduto(int id)
        {
            try
            {
                var command = new ProdutoDeleteCommand { Id = id };
                var response = await _mediator.Send(command);
                return Ok(response);
            }
            catch (Exception ex)
            {
                return BadRequest(ex.Message);
            }
        }
        [HttpPut]
        public async Task<ActionResult<Produto>> PutProduto([FromBody] ProdutoUpdateCommand command)
        {
            try
            {
                var response = await _mediator.Send(command);
                return Ok(response);
            }
            catch (Exception ex)
            {
                return BadRequest(ex.Message);
            }
        }
    }
}

Neste código Injetamos no construtor do Controller uma instância da interface IMediator pois precisamos enviar as requisições dos nosso objetos Command usando o método Send.

Aqui a interface IMediator faz o papel da classe mediadora que usa o método Send para chamar os comand handlers definidos.

Dessa forma quando recebemos no método Post o comando ProdutoCreateCommand será usado e nele passamos o objeto para o método Send, que por sua vez vai procurar alguma classe que herda de IRequestHandler e vai invocar o método Handler com base no objeto passado, e com isso, o método Send encontra a classe ProdutoCreateCommandHandler invocando o respectivo método.

Para testar podemos podemos alterar o arquivo launchSettings.json definindo a url inicial de acesso para : api/produtos

...
"profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "api/produtos",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "AspNet_MediatR1": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "api/produtos",
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
...

Agora é só alegria.

Executando o projeto, podemos usar o Postman e testar nossa implementação para as requisições.

1- GET
2- POST
3- PUT
4- DELETE
5- GET(int Id)

Pegue o projeto completo aqui:   Api_CQRS.zip  (sem as referências)

"Dando graças ao Pai que nos fez idôneos para participar da herança dos santos na luz;
O qual nos tirou da potestade das trevas, e nos transportou para o reino do Filho do seu amor;"
Colossenses 1:12,13

Referências:


José Carlos Macoratti