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


  Hoje vamos recordar como usar o MediatR para implementar o padrão CQRS - Command Query Responsibility Segregation.

A sigla CQRS é uma abreviação de Segregação de Responsabilidade de Consulta de Comando, numa tradução livre, e, o CQRS é um padrão de design que permite separ facilmente o comando e a lógica de consulta.



Já o MediatR é uma biblioteca que ajuda na implementação do CQRS.

Ao adicionar o MediatR ao nosso aplicativo, podemos reduzir as dependências entre nossos objetos, tornando o aplicativo menos acoplado = mais fácil de manter.  A biblioteca MediatR permite mensagens em processo (não permitindo comunicação direta entre objetos) onde cada solicitação contém um manipulador ou handler que é o responsável por processar a requisição enviada pelo serviço que chama o MediatR. Cada solicitação enviada ao MediatR recebe seu próprio manipulador.

Lógica de consulta e comando

Antes de nos aprofundarmos na implementação real, gostaria de ter certeza de que estamos do mesmo lado ao falar sobre consultas e comandos no contexto de uma aplicação.

Consultas

Quando fazemos uma consulta em um banco de dados, gostaríamos de recuperar um conjunto de dados com os quais podemos trabalhar. Uma consulta de seleção padrão se parece com o seguinte:  SELECT Nome, Idade From Users

Comandos

Ao lidar com o padrão CQRS, os comandos padrão são os comandos Inserir, Atualizar e Excluir. Esse tipo de lógica é colocada no nível de negócio da aplicação separando-a das demais camadas. Por serem operações comerciais, não retornarão nenhum dado no aplicativo. 

Vantagens do CQRS

Se você executa um aplicativo com muitas transações de banco de dados, vai desejar garantir que essas transações sejam executadas o mais rápido possível. Quando você implementa o CQRS em seu aplicativo, cada uma das consultas e comandos recebe seu próprio manipulador.

O CQRS é útil, pois cada instrução é mais fácil de otimizar à medida que separamos cada consulta (obter) e comando (inserir, atualizar, excluir).

O CQRS torna o código mais fácil de escrever, pois elimina a complexidade porque podemos separar os manipuladores, resultando em junções e operações de negócios menos complexas.

Se você tiver um aplicativo enorme e o modelo de negócios for complexo, poderá designar os desenvolvedores para separar partes da lógica de negócios. Isso significaria que um conjunto de desenvolvedores trabalharia apenas nas consultas e outro conjunto de desenvolvedores trabalharia apenas nos comandos INSERT, UPDATE ou DELETE.

Agora se o seu projeto for uma aplicação pequena e simples, pode ser um exagero usar CQRS, pois vocÇe tera um aumento na complexidade do aplicativo. Além disso, quando a lógica de negócios não é complexa, o CQRS pode ser ignorado.

Vantagens do MediatR

Seu aplicativo será mais fácil de manter e novos desenvolvedores serão mais fáceis de integrar, pois a integração exigiria menos tempo e recursos.

Já teve/manteve um aplicativo com uma tonelada de injeção de dependência porque você conseguiu um acoplamento pesado ?  O MediatR possibilita apenas referenciar a classe MediatR e reduzirá o acoplamento entre o controlador e os serviços que eles invocam.

Se precisar, você pode aplicar facilmente configurações novas ou diferentes em seu projeto sem ter que alterar muito código, o que pode consumir muito tempo.

Implementar CQRS usando MediatR em uma API da Web ASP.NET Core

Vamos iniciar criando um projeto no VS 2022 Community usando o template ASP.NET Core Web API com o nome AppMediatR.

A princípio vamos criar uma aplicação para fazer o CRUD básico em uma entidade Aluno contendo as propriedades Id, Nome, Idade Email. Para isso vamos usar o banco de dados SQL Server o EF Core 7 na abordagem Code-First e vamos fazer a validação do modelo usando a FluentValidation.

Após a criação do projeto vamos incluir os seguintes pacotes :

  1. Microsoft.EntityFrameworkCore.SqlServer
  2. Microsoft.EntityFrameworkCore.Tools
  3. FluentValidation.AspNetCore
  4. Microsoft.AspNetCore.OpenApi
  5. Swashbuckle.AspNetCore

Vamos criar uma pasta Entities no projeto e nesta pasta criar a classe Aluno:

public class Aluno
{
 
public int Id { get; set; }
 
public string? Nome { get; set; }
 
public int Idade { get; set; }
 
public string? Email { get; set; }
 
public string? Sexo { get; set; }
}

A seguir vamos criar a classe de contexto AppDbContext que herda de DbContext onde definimos o mapeamento ORM:

public class AppDbContext : DbContext
{
  
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
   {}
  
public DbSet<Aluno> Alunos { get; set; }
}

No arquivo appsettings.json vamos definir a string de conexão:

"ConnectionStrings": {
  
"DefaultConnection": "Data Source=.;Initial Catalog=Cadastro;Integrated Security=True;TrustServerCertificate=True;"
},
...

E na classe Program vamos registrar o serviço do contexto e configurar as opções, vamos registrar o serviço do MediatR e configurar a utilização do Swagger:

using AppMediatR.Context;
using
Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var connection = builder.Configuration.GetConnectionString("DefaultConnection");

builder.Services.AddDbContext<AppDbContext>(options =>
     options.UseSqlServer(connection));

builder.Services.AddMediatR(cfg =>
   cfg.RegisterServicesFromAssembly(
typeof(Program).Assembly));

builder.Services.AddEndpointsApiExplorer();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
   app.UseSwagger();
   app.UseSwaggerUI();
}

app.MapControllers();

app.Run();

Agora podemos aplicar o Migrations usando os seguintes comandos em sequência:

add-migration Inicial
update-database

Com isso teremos o banco de dados Cadastro e a tabela Alunos criados no SQL Server.

Criando os comandos e consultas

Chegou o momento de definirmos os comandos e as consultas e neste projeto eu vou criar uma pasta Alunos e nesta pasta vou criar as seguintes pastas:

  1. Commands  - Aqui vou definir os comandos para incluir, excluir e deletar alunos
  2. Queries - Aqui vou definir os comandos para consultar um aluno e obter todos os alunos
  3. Handlers - Aqui vou definir os handlers dos comandos

Para implementar os comandos e consultas vamos usar o padrão mediator usando a biblioteca MediatR. Essa implementação utiliza quatro elementos principais: IRequest, Handler, Command e IRequestHandler. Vejamos o papel de cada um deles:

IRequest: - A classe IRequest é uma classe de marcação que representa uma solicitação (request) que é enviada ao Mediator. Uma solicitação pode ser qualquer coisa que você queira que o Mediator processe, como por exemplo, a criação de um novo objeto ou a busca de informações em um banco de dados.

Handler: - A classe Handler é uma classe abstrata que define a estrutura básica de um manipulador (handler) para uma solicitação. Um manipulador é um objeto que processa a solicitação recebida pelo Mediator. A classe Handler possui um método abstrato chamado "Handle", que deve ser implementado pelas classes concretas de manipuladores.

Command: - A classe Command é uma classe que representa uma solicitação específica enviada ao Mediator. Ela geralmente contém informações específicas que o manipulador precisa para processar a solicitação. Por exemplo, uma solicitação de criação de um novo objeto pode incluir o nome, a descrição e outros detalhes necessários para criar o objeto.

IRequestHandler: - A interface IRequestHandler é uma interface genérica que define a estrutura básica de um manipulador concreto para uma solicitação específica. Ele herda da classe Handler e especifica o tipo de solicitação que ele é capaz de manipular. Cada manipulador concreto implementa essa interface e é responsável por processar a solicitação correspondente.

Assim vamos criar as classes para as consultas e os comandos que são as mensagens que serão enviadas entre componentes. Por exemplo, você pode criar uma classe "CriarUsuarioCommand" que contém as informações necessárias para criar um novo usuário no sistema.

A seguir vamos criar uma classe para o manipulador da mensagem, onde essa classe é responsável por processar a mensagem. Por exemplo, você pode criar uma classe "CriarUsuarioCommandHandler" que contém a lógica para criar um novo usuário no sistema.

Após configurar e registrar o MediatR podemos enviar as mensagens e obtendo o resultado.

Comandos - Criados na pasta Commands

1- CriarAlunoCommand

 public class CriarAlunoCommand : IRequest<Aluno>
{
 
public string? Nome { get; set; }
 
public int Idade { get; set; }
 
public string? Email { get; set; }
 
public string? Sexo { get; set; }
}

IRequest<Aluno> é uma interface genérica da biblioteca MediatR, que é comumente usada no padrão CQRS (Command Query Responsibility Segregation) para representar um comando que deve ser executado em resposta a uma solicitação do usuário.

O tipo genérico "Aluno" representa o resultado esperado após a execução do comando. Ou seja, neste caso, o comando CriarAlunoCommand espera criar um novo objeto do tipo "Aluno" e retorná-lo após a conclusão do comando.

2- AtualizarAlunoCommand

public class AtualizarAlunoCommand : IRequest<int>
{
  
public int Id { get; set; }
  
public string? Nome { get; set; }
  
public int Idade { get; set; }
  
public string? Email { get; set; }
  
public string? Sexo { get; set; }
}

3- ExcluirAlunoCommand

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

Consultas - Criadas na pasta Queries

1 - GetAlunoByIdQuery

public class GetAlunoByIdQuery : IRequest<Aluno>
{
  
public int Id { get; set; }
}

2- GetAlunosQuery

public class GetAlunosQuery : IRequest<IEnumerable<Aluno>>
{
}

Manipuladores (Handlers)

Aqui vamos criar os Handlers injetando uma instância do contexto e definir a validação usando a FluentValidation.

1- CriarAlunoHandler

using AppMediatR.Alunos.Commands;
using
AppMediatR.Context;
using
AppMediatR.Entities;
using
FluentValidation;
using
MediatR;

namespace AppMediatR.Alunos.Handlers;

public class CriarAlunoHandler : IRequestHandler<CriarAlunoCommand, Aluno>
{
  
private readonly AppDbContext _dbContext;
  
public CriarAlunoHandler(AppDbContext dbContext)
   {
     _dbContext = dbContext;
   }

   public async Task<Aluno> Handle(CriarAlunoCommand request,
                                  CancellationToken cancellationToken)
   {

     var
aluno = new Aluno
     {
        Nome = request.Nome,
        Idade = request.Idade,
        Email = request.Email,
        Sexo = request.Sexo
     };
    _dbContext.Alunos.Add(aluno);

     await _dbContext.SaveChangesAsync();
    
return aluno;
   }

   public class CreateAlunoCommandValidator : AbstractValidator<CriarAlunoCommand>
   {
     
public CreateAlunoCommandValidator()
      {
        RuleFor(x => x.Nome).NotEmpty();
        RuleFor(x => x.Email).NotEmpty().EmailAddress();
        RuleFor(x => x.Idade).InclusiveBetween(0, 120);
        RuleFor(x => x.Sexo).NotEmpty();
      }
   }
}

2- AtualizaAlunoHandler

public class AtualizaAlunoHandler : IRequestHandler<AtualizarAlunoCommand, int>
{
     private readonly AppDbContext _context;
     public AtualizaAlunoHandler(AppDbContext context)
     {
            _context = context;
     }
      public async Task<int> Handle(AtualizarAlunoCommand command, 
                                                       CancellationToken cancellationToken)
      {
          var aluno = await _context.Alunos.FindAsync(command.Id);
           aluno.Nome = command.Nome;
           aluno.Idade = command.Idade;
           aluno.Email = command.Email;
           aluno.Sexo = command.Sexo;
           await _context.SaveChangesAsync();
           return aluno.Id;
      }
}
public class AtualizarAlunoCommandValidator : AbstractValidator<AtualizarAlunoCommand>
{
     public AtualizarAlunoCommandValidator()
     {
	RuleFor(x => x.Nome).NotEmpty();
	RuleFor(x => x.Email).NotEmpty().EmailAddress();
	RuleFor(x => x.Idade).InclusiveBetween(0, 120);
	RuleFor(x => x.Sexo).NotEmpty().IsInEnum();
     }
}

3- ExcluirAlunoHandler

public class ExcluirAlunoHandler : IRequestHandler<ExcluirAlunoCommand, int>
{
    private readonly AppDbContext _context;

    public ExcluirAlunoHandler(AppDbContext context)
    {
        _context = context;
    }

    public async Task<int> Handle(ExcluirAlunoCommand command, CancellationToken cancellationToken)
    {
        var aluno = await _context.Alunos.FindAsync(command.Id);

        _context.Alunos.Remove(aluno);
        await _context.SaveChangesAsync();

        return aluno.Id;
    }
}
public class ExcluirAlunoCommandValidator : AbstractValidator<ExcluirAlunoCommand>
{
    public ExcluirAlunoCommandValidator()
    {
        RuleFor(x => x.Id).NotEmpty();
    }
}

4- GetAlunoByIdHandler

public class GetAlunoByIdHandler : IRequestHandler<GetAlunoByIdQuery, Aluno>
{
    private readonly AppDbContext _context;
    public GetAlunoByIdHandler(AppDbContext context)
    {
        _context = context;
    }
    public async Task<Aluno> Handle(GetAlunoByIdQuery query, CancellationToken cancellationToken)
    {
        return await _context.Alunos.FindAsync(query.Id);
    }
}
public class GetAlunoByIdQueryValidator : AbstractValidator<GetAlunoByIdQuery>
{
    public GetAlunoByIdQueryValidator()
    {
        RuleFor(x => x.Id).NotEmpty();
    }
}

5- GetAlunosHandler

public class GetAlunosHandler : IRequestHandler<GetAlunosQuery, IEnumerable<Aluno>>
{
    private readonly AppDbContext _context;
    public GetAlunosHandler(AppDbContext context)
    {
        _context = context;
    }
    public async Task<IEnumerable<Aluno>> Handle(GetAlunosQuery request, 
        CancellationToken cancellationToken)
    {
        return await _context.Alunos.ToListAsync();
    }
}

Para concluir vamos criar o controlador AlunosController contendo os métodos Action onde vamos usar a instância de Mediator e os comandos de consultas implementados:

using AppMediatR.Alunos.Commands;
using AppMediatR.Alunos.Handlers;
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 validationResult = await new GetAlunoByIdQueryValidator()
					 .ValidateAsync(new GetAlunoByIdQuery { Id = id });
		if (!validationResult.IsValid)
		{
			var errors = validationResult.Errors.Select(x => x.ErrorMessage);
			return BadRequest(new { Errors = errors });
		}
		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 validationResult = await new CriarAlunoCommandValidator()
						 .ValidateAsync(command);
		if (!validationResult.IsValid)
		{
			var errors = validationResult.Errors.Select(x => x.ErrorMessage);
			return BadRequest(new { Errors = errors });
		}
		try
		{
			var aluno = await _mediator.Send(command);
			return StatusCode(201);
		}
		catch (Exception ex)
		{
			return BadRequest(new { Error = ex.Message });
		}
	}
	[HttpPut("{id}")]
	public async Task<ActionResult<int>> Update(int id, AtualizarAlunoCommand command)
	{
		if (id != command.Id)
		{
			return BadRequest();
		}
		//command.Id = id;
		var validationResult = await new AtualizarAlunoCommandValidator()
						  .ValidateAsync(command);
		if (!validationResult.IsValid)
		{
			var errors = validationResult.Errors.Select(x => x.ErrorMessage);
			return BadRequest(new { Errors = errors });
		}
		try
		{
			var result = await _mediator.Send(command);
			return Ok(new { Id = result });
		}
		catch (Exception ex)
		{
			return BadRequest(new { Error = ex.Message });
		}
	}
	[HttpDelete("{id}")]
	public async Task<ActionResult<int>> Delete(int id)
	{
	            var validationResult = await new ExcluirAlunoCommandValidator()
					 .ValidateAsync(new ExcluirAlunoCommand { Id = id });
		if (!validationResult.IsValid)
		{
			var errors = validationResult.Errors.Select(x => x.ErrorMessage);
			return BadRequest(new { Errors = errors });
		}
		try
		{
			var command = new ExcluirAlunoCommand { Id = id };
			var result = await _mediator.Send(command);
			return Ok(result);
		}
		catch (Exception ex)
		{
			return BadRequest(new { Error = ex.Message });
		}
	}
}

Executando o projeto teremos o resultado:

Agora podemos validar todos os endpoints e na próxima parte do artigo veremos como simplificar o processo de validação usando o Pipeline Behaviour.

Pegue o projeto aqui:   AppMediatR.zip   ...

"Porque toda a criatura de Deus é boa, e não há nada que rejeitar, sendo recebido com ações de graças. Porque pela palavra de Deus e pela oração é santificada."
1 Timóteo 4:4-5

Referências:


José Carlos Macoratti