ASP .NET Core -  CQRS e Mediator - I


Hoje vamos fazer uma breve revisão do CQRS - Command Query Responsibility Segregation e mostrar como a biblioteca MediatR pode nos ajudar quando tratamos com o CQRS.

Eu já apresentei o CQRS neste artigo - ASP .NET Core 3.1 - Entendendo o CQRS e também já abordei o padrão Mediator neste artigo: O padrão de projeto Mediator.

CQRS e o padrão Mediator

A biblioteca MediatR foi criada para facilitar a implementação de dois padrões de projeto : CQRS e Mediator.

Vejamos um breve resumo de cada um destes padrões.

O padrão CQRS

CQRS significa "Command Query Responsibility Segregation” ou numa tradução livre “Segregação de Responsabilidade de Consulta de Comando”. Como o acrônimo sugere, trata-se de dividir a responsabilidade de comandos (escrita) e consultas (leituras) em diferentes modelos.

Se pensarmos no padrão CRUD (Create-Read-Update-Delete) que é muito usado normalmente temos a interface do usuário interagindo com um armazenamento de dados responsável por todas as quatro operações.

Em vez disso, o CQRS nos faria dividir essas operações em dois modelos, um para as consultas (O Read ou a letra "R" do CRUD) e outro para os comandos (o “CUD”).

Usando o CQRS o aplicativo separa os modelos de consulta e comando. Dessa forma o padrão CQRS não faz requisitos formais de como essa separação ocorre e a implementação pode ser tão simples quanto uma classe separada no mesmo aplicativo e pode ser usado até aplicativos físicos separados em servidores diferentes. Essa decisão seria baseada em fatores como requisitos de dimensionamento e infraestrutura.

O ponto chave é que, para criar um sistema com o padrão CQRS, precisamos apenas dividir as leituras das gravações.

Simples assim...

Qual é o problema que o CQRS se propõe a resolver ?

Para aplicações com uma grande quantidade de usuários acessando uma ou mais base de dados com uma grande quantidade de informações as premissas de desempenho no acesso aos dados, a escalabilidade da aplicação e a sua disponibilidade acabam sendo de suma importância para o sucesso do projeto.

Uma aplicação de grande porte que faz uso intenso de acesso a dados pode enfrentar problemas de lentidão, deadlocks e timouts afetando a escalabilidade, a disponibilidade e o desempenho. Este cenário seria o pior possível para qualquer tipo de aplicação.

Esses problemas podem estar ocorrendo devido ao tempo gasto na consulta e na persistência das informações principalmente quando não temos uma divisão clara dessas responsabilidade e de como elas podem estar afetando a aplicação. Afinal seria melhor dar prioridade para a leitura de dos dados ou priorizar a gravação dos dados como uma forma alavancar a aplicação ?

O CQRS nos permite “nos libertar” dessas considerações e dar a cada abordagem, leitura e/ou gravação, o design e a consideração iguais que elas merecem, sem nos preocupar com o impacto que elas podem causar, e, isso traz enormes benefícios no desempenho e na agilidade, especialmente se houver equipes separadas trabalhando nessas abordagens.

Dessa forma o CQRS nos orienta a dividir a responsabilidade de gravação e escrita onde podemos ter meios distintos para realizar a gravação e realizar as consultas e também podemos fazer isso em banco de dados diferentes.

O padrão Mediator

O padrão Mediator lida com o desacoplamento, colocando uma camada intermediária entre os componentes, chamada de "Mediador", que será sua única fonte de dependências.

Assim este padrão define um objeto que encapsula como os objetos interagem uns com os outros. Em vez de dois ou mais objetos dependerem diretamente um do outro, eles interagem com um "mediador", que é responsável por enviar essas interações para a outra parte:

Os componentes delegam suas chamadas de função ao Mediador e este decide qual componente precisa ser chamado e para quê. Isso garante que os componentes fiquem "fracamente acoplados" uns aos outros e não chamem uns aos outros diretamente. Isso também cria a oportunidade de um componente ser substituído por outra implementação, se necessário, e o sistema não será afetado.

Como a biblioteca MediatR ajuda na implementação do CQRS e do Mediator

Podemos pensar na biblioteca MediatR como uma implementação do Mediator que nos ajuda a construir sistemas usando CQRS. Toda a comunicação entre a interface do usuário e o armazenamento de dados ocorre via MediatR.

Como no CQRS, as responsabilidades de Consulta e Comandos são assumidas por seus respectivos Handlers e torna-se muito difícil fazer a conexão manualmente entre um Comando com seu Handler e manter os mapeamentos.

É aqui que o padrão Mediator entra em cena: ele encapsula essas interações e cuida do mapeamento para nós.

E na plataforma .NET podemos usar a biblioteca MediatR que traz a implementação do Mediator que usamos na implementação do CQRS.

CQRS - Implementação com o padrão Mediator usando o MediatR

Vamos configurar um projeto ASP .NET Core do tipo Web API bem simples para mostrar a utilização da biblioteca MediatR.

Nota: Como nosso foco será mostrar o uso do MediatR com CQRS eu não vou me preocupar com a arquitetura da aplicação visando a separação das responsabilidades segundo o padrão da clean architecture. Em outro artigo eu vou mostrar como fazer essa implementação usando a Clean Architecture.

recursos usados:

Criando o projeto API no VS 2019

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 DemoCQRS (ou outro nome a seu gosto).

A seguir selecione .NET Core e ASP .NET Core 5.0 e marque o template ASP .NET Core Web 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

Vamos aproveitar e já configurar o serviço do MediatR no projeto API incluindo no método ConfigureServices da classe Startup o código destacado em azul :

 public void ConfigureServices(IServiceCollection services)
 {
          services.AddControllers();
          services.AddMediatR(Assembly.GetExecutingAssembly());
  
          services.AddSwaggerGen(c =>
          {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "DemoMediator.API", Version = "v1" });
          });
 }

Para concluir vamos remover do projeto controlador WeatherForecastController.cs e a classe WeatherForecast.cs.

Criando o controlador TestesController

Vamos criar um novo controlador na pasta Controllers chamado TestesController com o seguinte código:

using MediatR;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace DemoCQRS.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TestesController : ControllerBase
    {
        private readonly IMediator _mediator;
        public TestesController(IMediator mediator)
        {
            _mediator = mediator;
        }
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "cqrs", "mediator" };
        }        
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "mediatr";
        }        
        [HttpPost]
        public void Post([FromBody] string value)
        {
        }        
        [HttpPut("{id}")]
        public void Put(int id, [FromBody] string value)
        {
        }        
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

Neste código criamos um controlador com os métodos GET, POST, PUT e DELETE e no construtor do controlador injetamos uma instância do MediatR.

Para tornar o artigo mais simples e focar no CQRS e no MediatR não vamos usar um banco de dados relacional. Vamos criar uma classe que encapsula esta responsabilidade.

Para isso vamos criar uma pasta Data no projeto e nesta pasta vamos criar uma classe FakeData.

using System.Collections.Generic;
namespace DemoCQRS.Data
{
    public class FakeData
    {
        private static List<string> _values;
        public FakeData()
        {
            _values = new List<string>
        {
            "cqrs",
            "mediatr",
            "mediator"
        };
        }
        public void AddValue(string value)
        {
            _values.Add(value);
        }
        public IEnumerable<string> GetAllValues()
        {
            return _values;
        }
    }
}

A seguir vamos registrar o serviço para configurar o nosso FakeData como um Singleton incluindo o código a seguir no método ConfigureServices da classe Startup:

       public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddSingleton<FakeData>();
            services.AddMediatR(Assembly.GetExecutingAssembly());
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "DemoCQRS", Version = "v1" });
            });
        }

 

Iniciando com o CQRS - Separando os comandos e as consultas

Já vimos que o padrão CQRS divide as leituras das gravações e vamos fazer isso agora.

Para organizar melhor o projeto vamos criar uma pasta Services e dentro desta pasta vamos criar duas pastas no projeto :

Usaremos essas pastas para separar nossos modelos de leitura e escrita. Lembrando que essa abordagem foi feita de forma bem simples para facilitar o entendimento do assunto principal deste artigo.

Fazendo requests com o MediatR

Os requests MediatR são mensagens no estilo Request/Response muito simples, em que um único Request é tratado por um único Handler ou manipulador de maneira síncrona.



Existem dois tipos de Request no MediatR :

  1. Um Request que retorna um valor (Leitura)
  2. Um Request que não retorna um valor (Gravações)

Usaremos a nossa fonte de dados  FakeData que criamos anteriormente para implementar alguns Requests MediatR.

Primeiro, vamos criar uma solicitação que retorna todos os valores de nosso FakeData.

Vamos criar na pasta Services/Queries a classe GetValuesQuery conforme o código a seguir:

using DemoCQRS.Data;
using MediatR;
using System.Collections.Generic;
namespace DemoCQRS.Services.Queries
{
    public class GetValuesQuery
    {
        public class Query : IRequest<IEnumerable<string>> { }

        public class Handler : RequestHandler<Query, IEnumerable<string>>
        {
            private readonly FakeData _db;
            public Handler(FakeData db)
            {
                _db = db;
            }
            protected override IEnumerable<string> Handle(Query request)
            {
                return _db.GetAllValues();
            }
        }
    }
}

Vamos entender o código :

Primeiro, criamos uma classe interna ou inner class chamada Query, que implementa IRequest<IEnumerable<string>>. Isso significa que nosso Request vai retornar uma lista de strings.

Em seguida, criamos outra classe interna chamada Handler, que herda de RequestHandler<Query,IEnumerable<string>>. Isso significa que essa classe tratará a Query ou Consulta, neste caso, retornando a lista de strings.

No construtor desta classe injetamos uma instância do nosso serviço de dados FakeData e a seguir implementamos um único método chamado Handle, que retorna os valores de nosso FakeData.

Observe que colocamos a consulta e o manipulador(handler) na mesma classe usando o recurso das classes internas. Mas, podemos colocá-los em classes ou projetos separados, se preferirmos. No entanto, colocando nas mesmas classes fica mais fácil a localização e a manutenção do código.

Estamos usando uma abordagem síncrona para simplificar e devido à forma como estamos implementando uma lista de valores na memória. No entanto, a biblioteca MediatR suporta o assincronismo com async/await.

Testando o Request MediatR

Para chamar nosso Request precisamos apenas modificar o método Get() em nosso TestesController:

using MediatR;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace DemoCQRS.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TestesController : ControllerBase
    {
        private readonly IMediator _mediator;
        public TestesController(IMediator mediator)
        {
            _mediator = mediator;
        }
        [HttpGet]
        public async System.Threading.Tasks.Task<IEnumerable<string>> GetAsync()
        {
            return await _mediator.Send(new Services.Queries.GetValuesQuery.Query());
        }
        ...
}

Veja como é simples enviar um Request ao MediatR.

Note que não estamos considerando uma dependência de FakeData e nem temos alguma ideia de como a consulta é tratada. Este é um dos princípios do padrão Mediator.

Agora vamos verificar se tudo está funcionando conforme o esperado.

Primeiro, vamos pressionar CTRL + F5 para construir e executar nosso aplicativo, e devemos a interface do Swagger exibindo os endpoints definidos em nosso controlador TestesController:

Vamos acionar o endpoint GET /api/Testes que invocar o método GetValuesQuery

Obtemos os valores conforme o esperado o que prova que o MediatR está funcionando corretamente, pois os valores que vemos são aqueles inicializados pelo nosso FakeData. Acabamos de implementar nossa primeira "Consulta" no CQRS.

Na próxima parte do artigo veremos o outro tipo de Request MediatR, que não retorna um valor, ou seja, um “Command” ou Comando.

"Os que confiam no SENHOR serão como o monte de Sião, que não se abala, mas permanece para sempre."
Salmos 125:1

Referências:


José Carlos Macoratti