1. Início
  2. Back-end
  3. Usando o MediatR com ASP.NET Core

Usando o MediatR com ASP.NET Core

tres telas de computador com codigos de programacao

Neste artigo iremos abordar uma biblioteca que utiliza o designer ​Pattern ​Mediator​. 

Pretendo fazer um contraponto dela com outras opções que, em minha opinião, não valem tanto a pena — por serem mais custosas.

O mediatR funciona como um “garçom”, fazendo o controle das requisições, seja para inclusões, alterações, exclusões ou consultas, deixando numa fila, como se fosse um “pedido de pizza” no mundo real.

Entraremos mais a fundo nisso, vamos lá?

Domain Driver Design

grafico do paradigma domain driven design

A maioria dos grandes sistemas utiliza ​o ​DDD (Domain Driven Design).

Esse é um paradigma para desenvolvimento de software que facilita a implementação de processos complexos e regras, realizando uma divisão por camadas.

Vou explicar brevemente a função de cada camada, apenas para clarear o entendimento daqueles que não conhecem muito como o DDD funciona.

Camada de aplicação

Aqui os serviços da API serão desenvolvidos. Sua função é repassar e direcionar qualquer requisição de modo que uma ação seja executada.

Camada de domínio

Aqui os modelos são implementados e posteriormente mapeados para o database.

Camada de serviço

Todas as validações e regras são criadas aqui, antes de serem mandadas ao banco de dados.

Camada de infraestrutura

Essa camada pode ser dividida em sub-camadas: data e cross-cutting, responsáveis respectivamente por realizar a persistência com o banco de dados e cruzar a hierarquia.

Essa fórmula funciona bem quando não temos milhares de requisições simultâneas. Para resolver os problemas que muitas requisições podem causar, a infraestrutura é reformulada.

“Vamos deixar nossas bases de dados no Azure ou AWS”, é a solução comum pensada, e na minha opinião, isso só gera mais custos.

Para resolver esses “gastos”, utiliza-se o CQRS (Segregação de Responsabilidade de Comando e Consulta).

CQRS (Command Query Responsibility Segregation)

homem em um notebook programando

Esse é um padrão de design, ou uma prática de desenvolvimento, onde você divide seu domínio em duas partes: o modelo de gravação e leitura.

O modelo de gravação (comandos)

É aqui que ocorre toda a atualização, inserção e exclusão. Um comando altera o estado do sistema, causando um impacto duradouro.

Na maioria das vezes essa parte é a mais complicada, pois nela está a maior parte da lógica de negócios e, portanto, as classes serão as mais complexas. 

Quando você envia um comando, não espera nada de volta. Por quê? Porque o modelo de leitura que foi usado antes — para mostrar o formulário — proíbe o usuário de emitir um comando unsuccesfull.

Em um aplicativo da Web/rpc via http api, ou rest api, o comando é iniciado por um POST, embora exclua a linha no banco de dados. 

No CQRS os comandos não são digitados como no mundo real.

Por exemplo, um usuário não deseja excluir registros de reservas, ele quer cancelar uma reserva (o que significa excluir um registro, mas também enviar e-mails, emitir reembolso, alertar a administração, etc)

>>Leitura Recomendada:
Validação de dados usando Fluent Validation

O modelo de leitura (consultas)

É aqui que ocorre todo o select. 

Uma consulta é uma maneira de exibir uma parte do estado do sistema para o usuário. Se você apenas emitir consultas, o estado do sistema não será alterado. 

É aqui que todo o trabalho de desempenho é feito, pois é essa a parte mais usada do sistema.

Por exemplo, quantos hotéis você vê antes de reservar uma estadia?

Em um aplicativo da web/rpc via api http/api rest, o comando é iniciado por um GET. 

A única exceção é por motivo técnico (solicitação muito grande). 

Usa-se GET para que o usuário consiga obter esse resultado de consulta sempre que quiser.

Normalmente essa parte é dividida em algumas outras etapas:

Dissociação

Dividindo seu domínio em duas partes, você pode torná-las muito mais independentes.

O modelo de leitura sempre será acoplado ao modelo de gravação, pelo menos quando você for atualizá-lo, mas o modelo de gravação não se importará com a maneira como você lê os dados e tentará ser o mais coerente possível.

O aumento do evento de domínio fará com que o modelo de gravação ignore totalmente o modelo de leitura.

Domínio seguinte

É muito importante ter uma base de código tão próxima quanto possível do que o especialista em domínio pensa.

O especialista de domínio não confia apenas nas linhas de banco de dados (CRUD), ele acha ações (comando), telas de usuário final (consultas) e evento/gatilho.

Desempenho

Se você armazenar o domínio em um armazenamento de dados otimizado para ele, poderá obter um desempenho melhor.

Por exemplo, você armazena o modelo de gravação em um RDMS para ter certeza de que o estado do sistema é coerente e pode armazenar o modelo de leitura em um banco de dados NoSQL (como Redis), porque não deseja executar 150 associações quando precisar exibir alguns dados para o usuário.

Na maioria das empresas em que passei é utilizado um banco de dados NoSQL para atuar apenas nas consultas, deixando isolado um SqlServer ou Oracle para as inserções, alterações e deleções.

Essa prática, em minha opinião, funciona, mas fica dispendioso no material intelectual humano precisar ter pessoas especialistas em acompanhar o processamento, a integridade dos dados e a horizontalização — quando assim for necessário. Significa custo para as Empresas.

Por que usar o Mediatr?

post it com uma lampada desenhada presa num painel de cortiça

Se quiser implementar o CQRS, você terá que criar códigos padronizados para as mensagens de fiação. 

Como alternativa, você pode usar um pacote que faz todo esse trabalho para você.

O Mediatr é um projeto de código aberto .Net criado por Jimmy Bogard

Ele é uma implementação do padrão do Mediador (dissociar a mensagem do manuseio).

Veja uma situação muito comum em nosso dia-a-dia: Um cliente vai à sua agência bancária para sacar dinheiro, efetua uma consulta de saldo, logo a seguir faz um saque em sua conta e finaliza novamente com outra consulta. 

Temos aqui três transações. Esse é um cenário cotidiano e a sobrecarga nas bases de dados é elevadíssima.

>>Leitura Recomendada:
O
que é Kubernetes? Tudo sobre!

Utilizando o MediatR

tela com codigos de programacao coloridos

Imaginem agora ter a mesma arquitetura CQRS utilizando poucas linhas de códigos e classes em um projeto? 

Uso
Instalar

O Mediatr está disponível no nuget para o projeto .net standard 2.0 (também está disponível para projetos de framework .net). 

Você acabou de digitar este comando no console do gerenciador de pacotes:

 Pacote de instalação MediatR 

Para vincular mensagens e manuseio, o MediatR precisa de um contêiner IoC. 

Se você acha que o contêiner incluído no Asp.Net core é suficiente, você deve instalar o pacote para este contêiner:

Install-Package MediatR.Extensions.Microsoft.DependencyInjection

Esse pacote é uma dependência do primeiro, então apenas esse é suficiente.

E você configura o MediatR assim em seu Startup:

 ConfigureService
services.AddMediatR(typeof(Startup)); 

Eu uso o tipo Startup, então o MediatR fará a varredura de todo o meu projeto principal do aspnet para a implementação da interface necessária.

Caso tenha interesse, você pode ver meu código aqui: Startup.cs

Eu também adiciono uma referência ao assembly compartilhado entre meu cliente (blazor app) e o servidor onde obtive as definições dos meus comandos e consultas.

Emitindo Comando e Consulta

Depois que tudo for inicializado, você poderá criar sua primeira mensagem.

Eu criei um comando para entrar no usuário.

Esse caso é estranho porque eu não atualizo o banco de dados, mas na minha cabeça ainda esse ainda é o estado global do sistema.

No Mediatr não há muita diferença entre um comando e uma consulta.

Aqui está o meu comando:

 public class LoginCommand : IRequest<LoginCommandResult>
    {
        [Required]
        public string UserName { get; set; }
 
        [Required]
        [DataType(DataType.Password)]
        public string Password { get; set; }
 
        [Display(Name = "Remember me?")]
        public bool RememberMe { get; set; }
    }

  • É um objeto c# que implementa uma “interface de marcador” genérica, IRequest (não existe um método para implementar).
  • Eu tenho o atributo de validação DataAnnotations para validar esta mensagem
  • O argumento genérico de IRequest é o tipo do resultado retornado pelo manipulador. Aqui está um comando com um resultado, simplesmente porque o usuário pode digitar um login ou senha inválidos. Eu consegui não ter nenhum resultado, pois acredito que fazer dessa forma é mais fácil.

Para lidar com esse comando, você precisa de duas coisas.

A primeira é a instância do mediador IMediatr:

 private readonly IMediator _mediator;
public AccountController(IMediator mediator)
{
    _mediator = mediator;
}

O manuseio é definido assim:

 public class LoginCommandHandler : IRequestHandler<LoginCommand, LoginCommandResult>
{
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly ILogger _logger;
 
    public LoginCommandHandler(SignInManager<ApplicationUser> signInManager, ILogger logger)
    {
        _signInManager = signInManager;
        _logger = logger;
    }
 
    public async Task<LoginCommandResult> Handle(LoginCommand request, CancellationToken cancellationToken)
    {
        var result = await _signInManager.PasswordSignInAsync(request.UserName, request.Password, request.RememberMe, lockoutOnFailure: false);
        if (result.Succeeded)
        {
            _logger.LogInformation("User logged in.");
            return new LoginCommandResult() { IsSuccess = true };
        }
        if (result.RequiresTwoFactor)
        {
            return new LoginCommandResult() { Need2FA = true };
        }
        if (result.IsLockedOut)
        {
            _logger.LogWarning("User account locked out.");
            return new LoginCommandResult() { IsLockout = true };
        }
        else
        {
            return new LoginCommandResult() { IsSuccess = false };
        }
    }
}

 

Esse código é bem simples. Eu só precisava implementar “IRequestHandler <LoginCommand, LoginCommandResult>”.

Então você chama assim:

 [HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginCommand command)
{
    var result = await _mediator.Send(command);
    if (!result.IsSuccess)
    {
        if (result.IsLockout)
            return Redirect("/lockout");
        ModelState.AddModelError("UserName", "Invalid login attempt.");
        return BadRequest(ModelState);
 
    }
    //if (result.Need2FA)
    //{
    //    return RedirectToAction("/loginWith2fa");
    //}
    return Ok();
}

 
  • A primeira linha chama o mediador e obtém o resultado de forma assíncrona (você poderia ter IO sob isso, então é melhor fazer tudo assíncrono)
  • Eu analiso o resultado. A maior parte desse código é do modelo de segurança original para asp.net. Eu comentei a parte 2FA, considerando que eu não o implementei ainda.
  • Eu reutilizei o ModelState para enviar o erro, como é o formato esperado pelo cliente (rpc via http) no caso de uma solicitação incorreta (400). Este é o código mais complicado que eu tenho na minha ação do controlador, na maioria das vezes é apenas como isso:
 var res = await mediator.Send(command);
if (res.IsSucess)
    return new OkResult();
return new BadRequestObjectResult(res.Errors); 

Então, por que ter um controlador em tudo?

O principal motivo é que ele executa algumas ações: roteamento, verbos, redirecionamentos http e, em alguns casos, como a parte de login, algo mais complexo.

>>Leitura Recomendada:
Orquestração de contêineres — com Docker, Swarm e Portainer

Eventos

Eu estou usando o Azure Table para armazenamento de dados.

Esse serviço não fornece indexação pronta para uso, então você precisa fazer sozinho.

Com o evento, posso separar com segurança meu modelo de gravação (inserindo novo elemento) e meu modelo de leitura (consultando o elemento com base em alguns critérios).

Um evento é muito parecido com um comando, mas implementa o INotification e não há resultado aqui, pois o chamador não se importa com o que acontece a seguir:

 public class TossPosted : INotification {
    public string TossId{get;set;}
}

Um lance é uma mensagem no meu aplicativo (como um post ou algo assim).

Então, para manipular o evento e criar o índice, você implementa o INotificationHandler, dessa forma:

 public class TossTagIndexHandler : INotificationHandler<TossPosted> {
    ///missing code : dependency injection, azure table init ...
    public Task Handle(TossPosted notification, CancellationToken cancellationToken) {       
        await mainTable.CreateIfNotExistsAsync();
        
        var toss = await _mediator.Send(new TossContentQuery(notification.TossId);
        
        var tasks = HashTagIndex
            .CreateHashTagIndexes(toss)//this create an entry for each hashtag entered into a toss
            .Select(h => mainTable.ExecuteAsync(TableOperation.Insert(h))
            .ToList();
        await Task.WhenAll(tasks);
    }}

 

Com isso, posso adicionar quantas estratégias de indexação eu quiser, embora seja necessário ter que criar algum processamento em lote ao implantá-lo.

Eu posso usar uma outra implementação para lidar com isso em outro servidor ou serviço de nuvem.

Eu também poderia desacoplá-lo mais e a manipulação de eventos iria apenas empurrar o novo CreateTossTagIndexCommand, mas isso seria muito código para nenhum ganho.

Validação

Você não viu qualquer validação de comando aqui.

É normal eu usar o novo ApiController do aspnet core, que lida com a devolução de um 400 pedido incorreto se um dos meus atributos não for respeitado. E isso é o suficiente para mim agora.

Conclusão

tela de computador com codigos de programacao

O Mediatr realmente ajudou a dissociar todo o meu comando, consulta e manipulação de eventos.

Também ajudou a manter meu controlador pequeno pois é, na minha opinião, um dos maiores problemas em um aplicativo mvc.

Para quem tiver interesse, aqui está um exemplo no Github criado por mim.

Fontes utilizadas para escrever o artigo:

  1. https://lostechies.com/jimmybogard/2015/05/05/cqrs-with-mediatr-and-automapper/
  2. https://github.com/jbogard/MediatR/wiki
  3. https://docs.microsoft.com/pt-br/aspnet/core/release-notes/aspnetcore-2.1?view=aspnetcore-2.1
  4. https://martinfowler.com/bliki/CQRS.html

Categorias

Leituras Recomendadas

Quer receber conteúdos incríveis como esses?

Assine nossa newsletter para ficar por dentro de todas as novidades do universo de TI e carreiras tech.