ASP.NET Core - Refatorando Minimal APIs com Carter
  Neste artigo veremos como usar os recursos do Carter para refatoração nas minimal APIs da ASP.NET Core.

Se você ainda não conhece o Carter veja o meu artigo neste link :  ASP .NET Core - Criando APIs com o framework Carter.

 

 

O Carter é um framework minimalista para o desenvolvimento de APIs web em C#. Ele foi inspirado pelo Sinatra, um framework similar em Ruby, e é projetado para ser simples, direto e fácil de usar.

 

Aqui estão alguns dos principais recursos e conceitos do Carter:

 

- O Carter permite definir rotas de forma simples e direta, especificando métodos HTTP e padrões de URL.

- Você pode modelar as requisições que sua API aceita e as respostas que ela retorna usando classes simples em C#.

- Assim como outros frameworks, Carter suporta middlewares, que são componentes que podem processar uma requisição antes que ela chegue ao seu endpoint.

- É possível usar serviços e componentes através de injeção de dependência, facilitando a organização e teste de código.

- Carter é altamente extensível, permitindo que você adicione seus próprios manipuladores, middlewares e plugins para estender suas capacidades.

- Com o plugin Carter.OpenApi, é possível gerar automaticamente a documentação da API usando o padrão OpenAPI (anteriormente conhecido como Swagger).

 

Criando uma minimal API

 

Para mostrar seus recursos na prática vamos criar um novo projeto no  VS 2022 usando o template ASP.NET Core Web API com o nome MinApiCarter e vamos selecionar a opção para não usar Controllers e criar uma minimal API.

 

Vamos criar a pasta Entities no projeto e nesta pasta vamos criar a entidade Product:

 

public class Product
{
    public int ProductId { get; set; }
    public string? ProductName { get; set; }
    public decimal Price { get; set; }
    public DateTime CreatedOn { get; set; }
    public bool IsValid()
    {
        return
            ProductId != 0 &&
            !string.IsNullOrWhiteSpace(ProductName) &&
            Price >= 0;
    }
}

 

Nossa entidade possui 4 propriedades e o método IsValid que verifique se ProductId é deferente de zero, se o ProductName não está vazio ou nulo e se o Price é maior ou igual a zero. Este método retorna true se todas as condições forem atendidas e false caso contrário.

 

A seguir vamos criar a pasta Repositories onde vamos criar a interface IProductRepository e a classe concreta ProductRepository:

 

1- IProductRepository

 

public interface IProductRepository
{
    bool CreateProduct(Product product);
    IEnumerable<Product> GetProducts();
    Product GetProductById(int id);
    bool UpdateProduct(int id, Product product);
    bool DeleteProduct(int id);
}

 

2- ProductRepository

 

using Bogus;
using MinApiCarter.Entities;
namespace MinApiCarter.Repositories;
public class ProductRepository : IProductRepository
{
    private List<Product>? products;
    private readonly ILogger<IProductRepository> _logger;
    public ProductRepository(ILogger<IProductRepository> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        GenerateSeedData();
    }
    public bool CreateProduct(Product product)
    {
        var productNameExistsAlready = 
            products.Any(p => p.ProductName == product.ProductName);
        
        var produtoIdExistsAlready = products.Any(p => 
                                  p.ProductId == product.ProductId);
        if (productNameExistsAlready || produtoIdExistsAlready)
        {
            _logger.LogError("Product exists already!");
            return false;
        }
        products.Add(product);
        return true;
    }
    public IEnumerable<Product> GetProducts()
    {
        return products;
    }
    public Product GetProductById(int id)
    {
        return products.FirstOrDefault(a => a.ProductId == id);
    }
    public bool UpdateProduct(int id, Product product)
    {
        var p = products.FirstOrDefault(a => a.ProductId == id);
        p.ProductName = product.ProductName;
        p.CreatedOn = DateTime.Now;
        p.Price = product.Price;
        return true;
    }
    public bool DeleteProduct(int id)
    {
        var product = products.FirstOrDefault(p => p.ProductId == id);
        if (product == null)
        {
            return false;
        }
        products.Remove(product);
        return true;
    }
    private void GenerateSeedData()
    {
        products = new Faker<Product>()
            .RuleFor(p => p.ProductId, p => p.IndexFaker)
            .RuleFor(p => p.ProductName, p => p.Commerce.ProductName())
            .RuleFor(p => p.Price, p => Convert.ToDecimal(p.Finance.Amount()))
            .RuleFor(p => p.CreatedOn, p => p.Date.Recent())
            .Generate(50);
    }    
}

ENeste código estamos implementando a interface IProductRepository e estamos usando o pacote nuget Bogus para criar gerar dados fictícios para os produtos. Vamos gerar 50 produtos.

Definindo os endpoints

Na classe Program precisamo registrar o serviço do repositório e criar os endpoints para realizar as operações :

using Carter;
using MinApiCarter.Entities;
using MinApiCarter.Repositories;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<IProductRepository, ProductRepository>();
builder.Services.AddCarter();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
//app.MapCarter();
app.UseHttpsRedirection();
// PRODUCTS operations
app.MapPost("/product/create", (Product product, 
                 IProductRepository productsRepo) =>
{
    if (!product.IsValid()) return Results.BadRequest();
    productsRepo.CreateProduct(product);
    return Results.StatusCode(201);
});
app.MapGet("/product/", (IProductRepository repo) =>
{
    return Results.Ok(repo.GetProducts());
});
app.MapGet("/product/{id}", (int id, IProductRepository repo) =>
{
    if (id <= 0) return Results.BadRequest();
    return Results.Ok(repo.GetProductById(id));
});
app.MapPut("/product/update/{id}", (int id, Product product, 
                          IProductRepository productRepo) =>
{
    if (!product.IsValid()) return Results.BadRequest();
    productRepo.UpdateProduct(id, product);
    return Results.Ok();
});
app.MapDelete("/product/delete/{id}", (int id, IProductRepository productsRepo) =>
{
    return productsRepo.DeleteProduct(id);
});
app.Run();
 

Executando o projeto iremos obter a interface do Swagger exibindo os endpoints.

Note que mesmo com poucos endpoints a classe Program já apresenta muito código e em projetos mais complexos o problema tende a se agravar gerando um código difícil de ler e de menter.

Podemos refatorar o código separando os endpoints em uma classe a parte e podemos usar os recursos do Carter para fazer isso.

Refatorando os endpoints com Carter

 

Vamos incluir no projeto o pacote Carter e a seguir registrar o serviço na classe Program:

 

builder.Services.AddCarter();

 

Agora vamos criar uma pasta Modules no projeto e nesta classe vamos criar a classe ProductsModule onde vamos implementar a interface ICarterModule e refatorar os endpoints da API conforme o código abaixo:

 

public class ProductsModule : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapPost("/products/create", CreateProduct);
        app.MapGet("/products/", GetAllProducts);
        app.MapGet("/products/{id}", GetProductById);
        app.MapPut("/products/update/{id}", UpdateProduct);
        app.MapDelete("/products/delete/{id}", DeleteProduct);
    }
    private IResult GetAllProducts(IProductRepository productsRepo)
    {
        var products = productsRepo.GetProducts();
        return Results.Ok(products);
    }
    private IResult CreateProduct(Product product, IProductRepository productsRepo)
    {
        if (!product.IsValid()) return Results.BadRequest();
        productsRepo.CreateProduct(product);
        return Results.StatusCode(201);
    }
    private IResult GetProductById(int id, IProductRepository repo)
    {
        if (id <= 0) return Results.BadRequest();
        return Results.Ok(repo.GetProductById(id));
    }
    private IResult UpdateProduct(int id, Product product, IProductRepository productRepo)
    {
        if (!product.IsValid()) return Results.BadRequest();
        productRepo.UpdateProduct(id, product);
        return Results.Ok();
    }
    private bool DeleteProduct(int id, IProductRepository repo)
    {
        return repo.DeleteProduct(id);
    }
} 

 

Observe que todos os endpoints retornam IResult, exceto o método DeleteProduct, que retorna um booleano. Você não precisa agrupar o resultado do endpoint sempre no objeto Results. Cabe a você retornar o que quiser de um endpoint.

Agora, nossos endpoints no método AddRoutes parecem limpos depois que movemos nossas implementações de endpoint para métodos privados na classe ProductsModule.

 

Agora podemos ajustar o código da classe Program :

 

using Carter;
using MinApiCarter.Repositories;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<IProductRepository, ProductRepository>();
builder.Services.AddCarter();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapCarter();
app.Run(); 

 

Com isso refatoramos os endpoints da nossa  minimal  API usando o Carter, movendo-os para um módulo separado.

 

E estamos conversados...

 

Pegue o projeto aqui : MinApiCarter.zip

 

"Olhai para as aves do céu, que nem semeiam, nem segam, nem ajuntam em celeiros; e vosso Pai celestial as alimenta. Não tendes vós muito mais valor do que elas?"
Mateus 6:26

Referências:


José Carlos Macoratti