NET 7 -  Usando filtros em Mininal APIs


   Neste artigo vou mostrar como usar o novo recursos dos filtros das Minimal APIs na ASP.NET Core 7 para fazer a validação com Fluent Validation.

Eu já apresentei o novo recursos dos filtros das minimal APIs neste artigo : NET 7 -  Mininal APIs : Apresentando filtros

Hoje vamos criar uma minimal API no ambiente do .NET 7 e mostrar como usar os filtros para realizar a validação usando a Fluent Validation.

Recursos usados :

Criando o projeto no VS 2022 e configurando o ambiente

Abra o VS 2022 preview e clique em Create a New Project selecionando o template ASP.NET Core Web API” :

Informe o nome ApiProdutos;

A seguir defina as demais configurações conforme mostrada na imagem a seguir:

Ao clicar em Create teremos a minimal API criada.

Vamos alterar o código gerado pelo template limpando o código da classe Program.

A seguir vamos incluir os seguintes pacotes no projeto:

Crie uma pasta Models no projeto e a seguir inclua nesta pasta a classe Produto:

public class Produto
{
 
public int Id { get; set; }

  [MaxLength(100)]
 
public string? Nome { get; set; }

  [Column(TypeName =
"decimal(10,2)")]
 
public decimal Preco { get; set; }
 
 
public int Estoque { get; set; }
}

Crie outra pasta Context no projeto e inclua nesta pasta o arquivo AppDbContext:

using Microsoft.EntityFrameworkCore;

namespace ApiProdutos.Data;

public class AppDbContext : DbContext
{
  
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
   {}

  
public DbSet<Produto> Produtos { get; set; }
}

Inclua no arquivo appsettings.json a string de conexão:

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

Na classe Program vamos registrar o contexto:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

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

builder.Services.AddDbContext<AppDbContext>(options =>
       options.UseSqlServer(builder.Configuration
                            .GetConnectionString(
"DefaultConnection")));
...
 

A seguir aplique o migrations usando os comandos:

Add-Migration Inicial -c AppDbContext -o Context/Migrations

E a seguir :

Update-Database Inicial

Com isso teremos o banco de dados ProdutosDB criado a tabela Produtos.

Configurando a Fluent Validation

Crie uma pasta Validators no projeto a  seguir inclua nesta pasta a classe ProdutoValidator:

using ApiProdutos.Data;
using
FluentValidation;
namespace
ApiProdutos.Validators;

public
class ProdutoValidator : AbstractValidator<Produto>
{  
 
public ProdutoValidator()
  {
    RuleFor(o => o.Nome).NotNull().NotEmpty().MinimumLength(3);
    RuleFor(o => o.Preco).NotNull().NotEmpty().NotEqual(0);
    RuleFor(o => o.Estoque).NotNull().NotEmpty().NotEqual(0);
  }
}

Como você pode ver, uma única invocação de método pode resultar em vários erros, portanto, vamos criar um método de extensão para combinar a saída em uma única mensagem de erro. Usaremos isso ao invocar a validação.

Assim crie a pasta Extensions e nesta pasta a classe ValidationErrorExtensions.cs:

using FluentValidation.Results;

namespace ApiProdutos.Extensions;

public static class ValidationErrorExtensions
{
  
public static string GetErrors(this List<ValidationFailure> errors)
   {
    
var errorMessages = "";
     errors.ForEach(err => errorMessages += err.ErrorMessage +
"");
    
return errorMessages;
    }
}

Criando o filtro

Vamos usar agora o recurso dos filtros das minimal APIs criando a classe ValidationFilter genérica que deriva de IEndpointFilter.

using ApiProdutos.Extensions;

using FluentValidation;

namespace ApiProdutos.Filters;

public class ValidationFilter<T> : IEndpointFilter where T : class
{
  
private readonly IValidator<T> _validator;
  
public ValidationFilter(IValidator<T> validator)
   {
     _validator = validator;
    }

   public async ValueTask<object> InvokeAsync(EndpointFilterInvocationContext context,
                 EndpointFilterDelegate next)
   {
     
var parameter = context.Arguments.SingleOrDefault(p => p.GetType() == typeof(T));

      if (parameter is null) return Results.BadRequest("O parametro é inválido.");

       var result = await _validator.ValidateAsync((T)parameter);

      if (!result.IsValid)
      {
      
 var errors = result.Errors.GetErrors();
       
return Results.Problem(errors);
      }
      
// now the actual endpoint execution
   
  return await next(context);
  }
}

O tipo T na classe ValidationFilter será definida por nossa entidade Produto. Observe que a lógica do filtro ocorre antes de retornarmos await next(context). Até esse ponto, se algo der errado, ou seja: a validação falhar, o processo entra em curto-circuito ou seja, não é executado.

Criando os endpoints

Vamos criar agora uma classe separada para os endpoints de nossos produtos e assim não vamos incluir  todos os endpoints em Program.cs, mas em um arquivo separado na pasta Endpoints na classe ProdutosEndpoints.cs. que uma classe de extensão de WebApplication.

Assim crie a pasta Endpoints e nesta pasta a classe estática ProdutosEndpoints:

using ApiProdutos.Data;
using ApiProdutos.Extensions;
using ApiProdutos.Filters;
using ApiProdutos.Validators;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using System;
using static Microsoft.AspNetCore.Http.Results;

namespace ApiProdutos.Endpoints;

public static class ProdutosEndpoints
{
    public static void MapProdutosEndpoints(this WebApplication app)
    {
        app.MapGet("/produtos", List);
        app.MapGet("/produtos/{id}", Get);
        app.MapPost("/produtos", Create).AddEndpointFilter<ValidationFilter<Produto>>();
        app.MapPut("/produtos", Update).AddEndpointFilter<ValidationFilter<Produto>>();
        app.MapDelete("/produtos/{id}", Delete);
    }

    public static async Task<IResult> List(AppDbContext db)
    {
        var result = await db.Produtos.ToListAsync();
        return Results.Ok(result);
    }

    public static async Task<IResult> Get(AppDbContext db, int id)
    {
        return await db.Produtos.FindAsync(id) is Produto produto
            ? Results.Ok(produto)
            : Results.NotFound();
    }

    public static async Task<IResult> Create(AppDbContext db,
                           IValidator<Produto> validator, Produto produto)

    {
        db.Produtos.Add(produto);
        await db.SaveChangesAsync();

        return Results.Created($"/produtos/{produto.Id}", produto);
    }

    public static async Task<IResult> Update(AppDbContext db,
                            IValidator<Produto> validator, Produto produtoAtualizado)

    {
        var produto = await db.Produtos.FindAsync(produtoAtualizado.Id);

        if (produto is null) return Results.NotFound();

        produto.Nome = produtoAtualizado.Nome;
        produto.Preco = produtoAtualizado.Preco;
        produto.Estoque = produtoAtualizado.Estoque;

        await db.SaveChangesAsync();
        return Results.NoContent();
    }

    public static async Task<IResult> Delete(AppDbContext db, IValidator<Produto> validator, int id)
    {
        if (await db.Produtos.FindAsync(id) is Produto produto)
        {
            db.Produtos.Remove(produto);
            await db.SaveChangesAsync();
            return Results.Ok(produto);
        }
        return Results.NotFound();
    }
}

Na primeira parte temos um método estático chamado MapProdutosEndpoints que contém o app.MapGet, app.MapPost, etc. Cada uma dessas funções recebe um delegate (a funcionalidade real do endpoint). Todas essas funções, List, Get, Create, ficam abaixo desse método.

Agora vamos registrar a validador e os endpoints na classe Program:

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

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

builder.Services.AddDbContext<AppDbContext>(options =>options.UseSqlServer 
     (builder.Configuration.GetConnectionString(
"DefaultConnection")));

builder.Services.AddScoped<IValidator<Produto>, ProdutoValidator>();

var app = builder.Build();

// Configure the HTTP request pipeline.

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

app.MapProdutosEndpoints();

app.UseHttpsRedirection();
app.Run();
 

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

Vamos testar a validação acionando o endpoint Post /produtos e tentar criar um produto sem os dados válidos:

Acionando o botão Execute teremos o seguinte response:

Observe as mensagens de validação indicando que o filtro esta funcionando.

E estamos conversados...

Pegue o projeto aqui :  ApiProdutos.zip

"como está escrito: Não há justo, nem um sequer, não há quem entenda, não há quem busque a Deus;
todos se extraviaram, à uma se fizeram inúteis; não há quem faça o bem, não há nem um sequer."
Romanos 3:10-12

Referências:


José Carlos Macoratti