ASP.NET Core - Usando Fluent Validation com CQRS


 Neste artigo vou apresentar como usar a Fluent Validation com a CQRS em um projeto ASP.NET Core.

O padrão CQRS (Command Query Responsibility Segregation) e a biblioteca MediatR ganharam popularidade para a construção de aplicativos escalonáveis e de fácil manutenção.

Quando se trata de validar dados de entrada, o Fluent Validation oferece uma abordagem flexível e intuitiva e neste artigo, vamos explorar como integrar Fluent Validation com CQRS e MediatR em um aplicativo ASP.NET Core.

Criando e configurando o projeto

Vamos começar criando um novo projeto usando o template ASP.NET Core Web API com o nome ApiCqrsFluentValidation usando as seguintes configurações:



Depois que o projeto for criado, vamos instalar os seguintes pacotes nugets:

Você pode instalar esses pacotes usando o NuGet Package Manager ou executando o seguinte comando no Package Manager Console:
Install-Package <nome_pacote>

Nota (*):  O pacote FluentValidation.AspNetCore foi descontinuado e não recebe mais suporte.
Essa informação é oficial e pode ser encontrada no repositório do FluentValidation no GitHub e na documentação.
O motivo principal para a descontinuação é que a equipe do FluentValidation recomenda a utilização do pacote principal
FluentValidation com uma abordagem de validação manual no ASP.NET Core.

O FluentValidation ainda é open source e está disponível no

Agora vamos criar uma pasta Entities no projeto e a seguir criar a classe User :

public class User
{
  
public int Id { get; set; }
  
public string? Username { get; set; }
  
public string? Email { get; set; }
  
public string? Password { get; set; }
}

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

{
 
"ConnectionStrings": {
  
"DefaultConnection": "Data Source=<seu_host>;Initial Catalog=ApiCqrsDB;
                         Integrated Security=True;TrustServerCertificate=True"

},
...

Vamos criar a pasta Context no projeto e nesta pasta criar o arquivo de contexto AppDbContext que herda de DbContext:

using ApiCqrsFluentValidation.Entities;
using
Microsoft.EntityFrameworkCore;

namespace ApiCqrsFluentValidation.Context;

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

Finalmente vamos registrar o serviço do contexto na classe Program:

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

builder.Services.AddDbContext<AppDbContext>(options =>
                   options.UseSqlServer(connection, b =>
                   b.MigrationsAssembly(
typeof(AppDbContext).Assembly.FullName)));
...

Podemos agora aplicar o Migrations usando a ferramenta EF Core Tools com os seguintes comandos:

dotnet ef migrations add MigracaoInicial
dotnet ef database update

A seguir vamos configurar e registrar os serviços do MediatR da Fluent Validation no contêiner de injeção de dependência do ASP.NET Core.  Fazemos isso na classe Program:

using FluentValidation.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
// Add services to the container.

builder.Services.AddControllers();

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

builder.Services.AddFluentValidationAutoValidation()
       .AddFluentValidationClientsideAdapters();

builder.Services.AddEndpointsApiExplorer();

builder.Services.AddSwaggerGen();
...
...

Definindo os comandos e consultas : Commands e Queries

O CQRS - Command Query Responsibility Segregation, é um padrão de arquitetura de desenvolvimento de software que permite realizar a separação de leitura e escrita em dois modelos: Query e Command, uma para leitura e outra para escrita de dados, respectivamente.

  1. Command - refere-se a um comando de banco de dados, que pode ser uma operação Inserir/Atualizar ou Excluir;
  2. Query - significa Consultar dados de uma fonte.;

Assim, o CQRS separa as responsabilidades em termos de leitura e escrita, o que faz muito sentido.

Todas as classes implementam IRequest<T> onde especificamos o tipo de dados que será retornado quando o comando for processado, e, também, através da qual vinculamos os comandos com as classes Command Handlers.

Para fazer a implementação temos que criar as classes para os comandos (alterações de estado) e para as consultas (recuperação de dados) e a seguir temos que criar os Handlers ou manipuladores para cada comando e consulta, implementando as interfaces IRequestHandler ou IRequestHandler<TRequest, TResponse> do MediatR.

Indo direto ao que interessa vamos criar no projeto uma pasta CQRS e dentro desta pasta vamos criar a pasta Commands e a pasta Queries.

A seguir na pasta Commands vamos criar a classe CreateUserCommand :

public class CreateUserCommand : IRequest<Guid>
{
  
public string? Username { get; set; }
  
public string? Email { get; set; }
  
public string? Password { get; set; }
}

Na mesma pasta vamos criar a classe CreateUserCommandHandler :

using MediatR;

namespace ApiCqrsFluentValidation.CQRS.Commands;

public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Guid>
{
  
private readonly AppDbContext _context;
   public
CreateUserCommandHandler(AppDbContext context)
   {
     _context = context;
   }

  
public async Task<Guid> Handle(CreateUserCommand request, CancellationToken cancellationToken)
   {
    
  var user = new User();
       user.Username = request.Username;
       user.Email = request.Email;
       user.Password = request.Password;

       _context.Users.Add(user);


       await
_context.SaveChangesAsync();

       return
await Task.FromResult(Guid.NewGuid());
   }
}

A seguir na pasta Queries vamos criar a classe GetUsersQuery:

public class GetUsersQuery : IRequest<IEnumerable<User>>
{

}

A seguir ainda na pasta Queries vamos criar o Handler GetUsersQueryHandler :

using ApiCqrsFluentValidation.Context;
using
ApiCqrsFluentValidation.Entities;
using
MediatR;
using
Microsoft.EntityFrameworkCore;

namespace ApiCqrsFluentValidation.CQRS.Queries;

public class GetUsersQueryHandler : IRequestHandler<GetUsersQuery, IEnumerable<User>>
{
  
private readonly AppDbContext _context;
  
public GetUsersQueryHandler(AppDbContext context)
   {
    _context = context;
   }
  
public async Task<IEnumerable<User>> Handle(GetUsersQuery request,
                                        CancellationToken cancellationToken)
   {
   
return await _context.Users.ToListAsync();
   }
}

Criando o controlador

Agora vamos criar o controlador UsersController  da nossa aplicação.

using ApiCqrsFluentValidation.CQRS.Commands;
using ApiCqrsFluentValidation.CQRS.Queries;
using ApiCqrsFluentValidation.Entities;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace ApiCqrsFluentValidation.Controllers;
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
    IMediator _mediator;
    public UsersController(IMediator mediator)
    {
        _mediator = mediator;
    }
    [HttpPost]
    public async Task<IActionResult> Create(CreateUserCommand command)
    {
        var userId = await _mediator.Send(command);
        return Ok(userId);
    }
    [HttpGet]
    public async Task<ActionResult<IEnumerable<User>>> GetUsers()
    {
        try
        {
            var command = new GetUsersQuery();
            var response = await _mediator.Send(command);
            return Ok(response);
        }
        catch (Exception ex)
        {
            return BadRequest(ex.Message);
        }
    }
}

Fazendo a validação com Fluent Validation

Agora vamos criar uma pasta CQRS/Commands do projeto a classe CreateUserCommandValidator onde vamos definir as regras de validação para os dados da entidade User:

using FluentValidation;

namespace ApiCqrsFluentValidation.CQRS.Commands;

public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
  
public CreateUserCommandValidator()
   {
     RuleFor(x => x.Username).NotEmpty().MaximumLength(50).WithMessage(
"O nome é requerido(tamanho máximo 20).");
     RuleFor(x => x.Email).NotEmpty().WithMessage(
"O Email é obrigatório.");
     RuleFor(x => x.Email).EmailAddress().WithMessage(
"O formato do email é inválido.");
     RuleFor(p => p.Password).NotEmpty().WithMessage(
"A senha não pode ser vazia")
       .MinimumLength(8).WithMessage(
"O Tamanho da senha deve ser de no mínimo 8.")
       .MaximumLength(15).WithMessage(
"O Tamanho da senha deve não pode exceder 15.")
       .Matches(
@"[A-Z]+").WithMessage("A senha deve conter pelo menos um caractere maiúsculo.")
       .Matches(
@"[a-z]+").WithMessage("A senha deve conter pelo menos um carcatere minúsculo.")
       .Matches(
@"[0-9]+").WithMessage("A senha deve conter pelo menos um número.")
       .Matches(
@"[\!\?\#\*\.]+").WithMessage("A senha deve conter pelo menos um caracstere (!?# *.).");
   }
}

A seguir vamos uma pasta chamada Behaviors no projeto e nesta pasta criar a classe ValidationBehavior :

using FluentValidation;
using MediatR;
namespace ApiCqrsFluentValidation.Behaviors;
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> 
                                                  where TRequest : IRequest<TResponse>
{
    private readonly IValidator<TRequest> _validator;
    public ValidationBehavior(IValidator<TRequest> validator)
    {
        _validator = validator;
    }
    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse>
                                                next, CancellationToken cancellationToken)
    {
        var validationResult = await _validator.ValidateAsync(request, cancellationToken);
        if (!validationResult.IsValid)
        {
            throw new ValidationException(validationResult.Errors);
        }
        return await next();
    }
}

Os "Behaviors" (comportamentos) na biblioteca MediatR são uma maneira de inserir lógica adicional nas operações de manipulação de comandos e consultas durante seu processamento. Eles permitem que você adicione tarefas antes e/ou depois do manuseio dos comandos e consultas. Os behaviors podem ser usados para realizar ações como validação, logging, autorização e muito mais.

No código implementado temos um exemplo de um comportamento de validação chamado ValidationBehavior. Este comportamento é responsável por validar os objetos de solicitação (requests) antes de serem manipulados pelo manipulador correspondente.

Vejamos uma explicação detalhada desta classe:

Estamos criando uma classe genérica chamada ValidationBehavior que implementa a interface IPipelineBehavior<TRequest, TResponse>. Isso permite que ela seja usada como um behavior no pipeline de manipulação do MediatR.

  1. A restrição de tipo where TRequest : IRequest<TResponse> especifica que o TRequest deve ser um tipo que implementa a interface IRequest<TResponse>. Isso garante que o comportamento seja aplicável apenas a solicitações (requests).
  2. No construtor da classe, estamos injetando uma instância de IValidator<TRequest>, que é um validador associado ao tipo de solicitação TRequest.
  3. O método Handler é o método principal do comportamento, onde a lógica de validação é implementada. Ele recebe a solicitação, um delegate para o próximo manipulador no pipeline (next), e um token de cancelamento.
  4. O código chama o método ValidateAsync do validador _validator passando a solicitação e o token de cancelamento. Isso retorna um objeto ValidationResult que contém os resultados da validação.
  5. O código verifica se o resultado da validação não é válido (!validationResult.IsValid). Se não for válido, lança uma exceção ValidationException contendo os erros de validação.
  6. Se a validação for bem-sucedida, o código chama o próximo manipulador no pipeline usando o next(). Isso permite que a solicitação continue a ser processada pelos outros behaviors e manipuladores.

 Agora vamos registrar os serviços na classe Program:

...
// registrar validator manualmente
// builder.Services.AddTransient<IValidator<CreateUserCommand>, CreateUserCommandValidator>();

// Para detectar os validators automaticamente

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

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

Pronto ! Agora é só alegria...

Executando o projeto teremos os endpoints exibidos na interface do Swagger:

E tentando criar um usuário que fere as regras de validação teremos o seguinte resultado:

      

Com isso temos a validação feita com a Fluent Validation em nosso comando CQRS.

Pegue o código do projeto aqui:   ApiCqrsFluentValidation.zip

"Esta é a mensagem que dele ouvimos e transmitimos a vocês: Deus é luz; nele não há treva alguma.
Se afirmarmos que temos comunhão com ele, mas andamos nas trevas, mentimos e não praticamos a verdade"
1 João 1:5-6

Referências:


José Carlos Macoratti