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 :
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(); var connection = builder.Configuration.GetConnectionString("DefaultConnection");builder.Services.AddDbContext<AppDbContext>(options => builder.Services.AddMediatR(cfg => 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:
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> |
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)
public async Task<int> Handle(ExcluirAlunoCommand command,
CancellationToken cancellationToken)
_context.Alunos.Remove(aluno);
return aluno.Id; |
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:
NET - Unit of Work - Padrão Unidade de ...