ASP .NET Core :  CQRS usando MediatR (FluentValidation) - II


  Neste artigo veremos o que é e como usar o MediatR Pipeline Behaviour na ASP.NET Core para realizar validações.

Continuando o artigo anterior onde implementamos o CQRS usando o MediatR e fizemos a validação com FluentValidation veremos como simplificar o processo de validação.



Vamos iniciar apresentando uma visão geral sobre os pipelines.

Pipelines

No contexto da ASP.NET Core, um pipeline refere-se a uma sequência de middlewares que processam uma requisição HTTP. Cada middleware no pipeline tem a oportunidade de manipular a requisição antes de passá-la para o próximo middleware na sequência.

Os middlewares são componentes reutilizáveis que executam uma tarefa específica, como autenticação, autorização, cache, registro de logs e manipulação de erros. Cada middleware pode adicionar informações ou modificar a requisição ou a resposta. Os middlewares também podem chamar o próximo middleware na sequência ou encerrar a cadeia de processamento e retornar uma resposta.

Quando uma requisição HTTP chega à aplicação ASP.NET Core, ela é passada através de um pipeline de middlewares que implementa as funcionalidades necessárias para processar a requisição. Cada middleware no pipeline tem a oportunidade de manipular a requisição antes de passá-la para o próximo middleware.

No pipeline, os middlewares são organizados em uma sequência específica, em que a ordem importa. Por exemplo, um middleware de autenticação deve ser colocado antes de um middleware de autorização, uma vez que a autenticação deve ser executada antes da autorização. A ordem dos middlewares pode ser configurada no arquivo Startup.cs da aplicação.

Neste contexto, quando você envia um request, vai obter uma resposta, e, esses requests e responses viajam através dos pipelines na ASP.NET Core.

Assim, quando você envia uma requisição, a mensagem de requisição passa do usuário por um pipeline até o aplicativo, onde você executa a operação solicitada com a mensagem de requisição. Depois de concluído, o aplicativo envia de volta a mensagem como uma resposta por meio do pipeline em direção ao usuário final.

Assim, esses pipelines estão completamente cientes de qual é o request ou response.

A imagem abaixo ilustra o conceito do funcionamento de um pipeline:

Agora veremos como aplicar este conceito a validação usando o MediatR.

Digamos que eu queira validar o objeto de solicitação. Como você faria ?

Basicamente, você escreveria as lógicas de validação que são executadas após a solicitação atingir o final do pipeline em direção ao aplicativo. Isso significa que você está validando a solicitação somente depois que ela chegar no aplicativo. Embora esta seja uma boa abordagem, vamos pensar sobre isso.

Por que você precisa anexar as lógicas de validação ao aplicativo, quando já pode validar as solicitações recebidas antes mesmo de atingir qualquer uma das lógicas do aplicativo ?  Percebeu !!!

Uma abordagem melhor seria, de alguma forma, conectar as lógicas de validação dentro do pipeline, para que o fluxo se torne como o usuário enviando uma solicitação por meio do pipeline (contendo a lógica de validação), se a solicitação for válida, você acessa as lógicas do aplicativo, caso contrário, gera uma exceção de validação.

Isso faz muito sentido em termos de eficiência, certo ?

Este conceito e sua aplicação pode ser usado não apenas para realizar a validação, mas para várias outras operações, como registro, rastreamento de desempenho e muito mais.

MediatR Pipeline Behaviour

Vamos agora apresentar o recurso Behaviours da biblioteca MediatR. MediatR Pipeline Behavior que surgiu na versão 3 desta biblioteca.

Sabemos que essas solicitações ou comandos do MediatR são como o primeiro contato dentro de nosso aplicativo, então por que não anexar alguns serviços em seu Pipleline?

Fazendo isso, seremos capazes de executar serviços/lógicas como validações antes mesmo que o Command ou Query Handlers saibam disso. Desta forma, estaremos enviando apenas requisições válidas necessárias para a Implementação do CQRS.

Para mostrar isso na prática vamos partir da nossa aplicação criada no artigo anterior onde já implementamos o CQRS e validação. Nosso objetivo é agora usar o recurso Behavior e simplificar o código da aplicação para as validações.

Portanto já criamos os endpoints da nossa API e já definimos as validações usando FluentValidation onde já criamos validadores para os comandos e as consultas.

Agora a primeira coisa a fazer é registrar os validators criados na classe Program:

 ...
 builder.Services.AddValidatorsFromAssembly(
typeof(Program).Assembly);
 ...

Este código basicamente registra todos os validadores que estão disponíveis no Assembly que também contém o Program.

Agora que temos nosso validador configurado, vamos adicionar a validação ao comportamento do pipeline.

Vamos criar uma pasta PipelineBehaviours no projeto e criar nesta pasta a classe ValidationBehvaviour onde vamos definir o recurso Behaviors a biblioteca MediatR usando o código abaixo:

using FluentValidation;
using
MediatR;

namespace AppMediatR.PipelineBehaviors;

   public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest,
                                 TResponse>
where TRequest : IRequest<TResponse>
   {
      
private readonly IEnumerable<IValidator<TRequest>> _validators;
      
public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators)
       {
         _validators = validators;
       }

       public async Task<TResponse> Handle(TRequest request,
                    RequestHandlerDelegate<TResponse> next,
                    CancellationToken cancellationToken)
       {
           
if (_validators.Any())
            {
               
var context = new ValidationContext<TRequest>(request);
               
var validationResults = await Task.WhenAll(_validators.Select(v =>
                                        v.ValidateAsync(context, cancellationToken)));

                 var failures = validationResults.SelectMany(r => r.Errors)
                                                 .Where(f => f !=
null).ToList();

                 if (failures.Count != 0)
                     
throw new FluentValidation.ValidationException(failures);
            }
           
return await next();
       }
   }

Neste código  obtemos uma lista de todos os validadores registrados, a seguir inicializamos o validador,  também validamos a requisição no método Handle, e, se algum erro de validação for encontrado, extraimos todas as mensagens e retornamos os erros como uma resposta.

Para concluir vamos registar este Pipleine Behaviour no container da ASP.NET Core na classe Program:

...
builder.Services.AddTransient(
typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
...

Agora podemos simplificar o código do controlador removendo o código usado para realizar a validação:

using AppMediatR.Alunos.Commands;
using AppMediatR.Alunos.Queries;
using AppMediatR.Entities;
using MediatR;
using Microsoft.AspNetCore.Mvc;

namespace AppMediatR.Controllers;

[ApiController]
[Route("[controller]")]
public class AlunosController : ControllerBase
{
    private readonly IMediator _mediator;

    public AlunosController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpGet]
    public async Task<ActionResult<IEnumerable<Aluno>>> GetAlunos()
    {
        var query = new GetAlunosQuery();
        var alunos = await _mediator.Send(query);

        if (alunos == null)
        {
            return NotFound();
        }

        return Ok(alunos);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Aluno>> GetById(int id)
    {

        var query = new GetAlunoByIdQuery { Id = id };
        var aluno = await _mediator.Send(query);

        if (aluno == null)
        {
            return NotFound();
        }

        return Ok(aluno);
    }

    [HttpPost]
    public async Task<ActionResult<int>> Create(CriarAlunoCommand command)
    {
        var aluno = await _mediator.Send(command);
        return StatusCode(201);
    }

    [HttpPut("{id}")]
    public async Task<ActionResult<int>> Update(int id, AtualizarAlunoCommand command)
    {
        if (id != command.Id)
        {
            return BadRequest();
        }
      
        var result = await _mediator.Send(command);
        return Ok(new { Id = result });
    }

    [HttpDelete("{id}")]
    public async Task<ActionResult<int>> Delete(int id)
    {
        var command = new ExcluirAlunoCommand { Id = id };
        var result = await _mediator.Send(command);
        return Ok(result);
    }
}

 

Executando o projeto teremos o resultado:

Ao tentar incluir um aluno usando informações inválidas :

teremos o resultado a seguir:

Naturalmente podemos tratar essa resposta mapeando-a para um objeto de resposta diferente usando um middleware de tratamento de erro personalizado.

O ponto a ser observado é que a solicitação chega ao método Handler do comando CriarAlunoCommand somente se for válido.

Isso demonstra claramente o comportamento do pipeline do MediatR.

Pegue o projeto aqui:   AppMediatR2.zip   ...

"Disse-lhes, pois, Jesus: Quando levantardes o Filho do homem, então conhecereis que EU SOU, e que nada faço por mim mesmo; mas isto falo como meu Pai me ensinou."
João 8:28

Referências:


José Carlos Macoratti