ASP .NET Core 6 - Estruturando uma Minimal APIs (SQLite)


Hoje veremos como estrutura uma Minimal APIs criada no .NET 6.0.

O novo recurso do .NET 6.0 - Minimal APIs - para a ASP .NET Core 6 permite criar APIs com o mínimo de dependência do framework WebAPI e o mínimo de código e arquivos necessários para o desenvolvimento minimalista de APIs.

Apresentando o problema

Um dos objetivos do novo recurso Minimal APIs do NET 6 é facilitar o aprendizado para quem esta iniciando com a plataforma .NET.

Além disso este modelo de projeto promete criar Web APis mais leves e assim podemos criar um microsserviço e iniciar a prototipagem sem a necessidade de criar muitos códigos clichê e se preocupar muito com a estrutura do código.

Como eu já mostrei em artigos anteriores o projeto para uma Minimal API possui dois arquivos e 4 linhas de código iniciais:

Esse tipo de estilo tem tudo para aumentar a produtividade e nivelar a curva de aprendizado para os iniciantes. Naturalmente este modelo de projeto tem as suas desvantagens e um dos problemas é que o arquivo Program.cs pode ficar muito grande.

Assim, esta simplicidade inicial pode levar o desenvolvedor a criar um tipo de solução conhecida como o grande bola de lama.

Como exemplo veja como ficou o arquivo Program.cs da API que eu apresente no artigo : ASP .NET Core 6 - Explorando Minimal APIs (SQLite) :

using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("SqliteConnectionString") 
          ?? "Data Source=Tarefas.db";
builder.Services.AddSqlite<TarefaDbContext>(connectionString);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
await AsseguraDBExiste(app.Services, app.Logger);

app.MapGet("/error", () => Results.Problem("Ocorreu um erro.", statusCode: 500))
   .ExcludeFromDescription();
app.MapSwagger();
app.UseSwaggerUI();
app.MapGet("/tarefas", async (TarefaDbContext db) =>
    await db.Tarefas.ToListAsync())
    .WithName("GetTarefas");
app.MapGet("/tarefas/{id}", async (int id, TarefaDbContext db) =>
    await db.Tarefas.FindAsync(id)
        is Tarefa tarefa
            ? Results.Ok(tarefa)
            : Results.NotFound())
    .WithName("GetTarefaById")
    .Produces<Tarefa>(StatusCodes.Status200OK)
    .Produces(StatusCodes.Status404NotFound);
app.MapPost("/tarefas", async (Tarefa tarefa, TarefaDbContext db) =>
{
    if (tarefa != null)
    {
        db.Tarefas.Add(tarefa);
        await db.SaveChangesAsync();
        return Results.Created($"/tarefas/{tarefa.Id}", tarefa);
    }
    else
    {
        return Results.BadRequest("Request inválido");
    }
}
).WithName("CreateTarefa")
 .ProducesValidationProblem()
 .Produces<Tarefa>(StatusCodes.Status201Created);
app.MapPut("/tarefas/{id}", async (int id, Tarefa inputTarefa, TarefaDbContext db) =>
{
    var tarefa = await db.Tarefas.FindAsync(id);
    if (tarefa is null) return Results.NotFound();
    tarefa.Titulo = inputTarefa.Titulo;
    tarefa.IsCompleta = inputTarefa.IsCompleta;
    await db.SaveChangesAsync();
    return Results.NoContent();
})
    .WithName("UpdateTarefa")
    .ProducesValidationProblem()
    .Produces(StatusCodes.Status204NoContent)
    .Produces(StatusCodes.Status404NotFound);
app.MapDelete("/tarefas/{id}", async (int id, TarefaDbContext db) =>
{
    if (await db.Tarefas.FindAsync(id) is Tarefa tarefa)
    {
        db.Tarefas.Remove(tarefa);
        await db.SaveChangesAsync();
        return Results.Ok(tarefa);
    }
    return Results.NotFound();
})
    .WithName("DeleteTarefa")
    .Produces(StatusCodes.Status204NoContent)
    .Produces(StatusCodes.Status404NotFound);
app.Run();
async Task AsseguraDBExiste(IServiceProvider services, ILogger logger)
{
    logger.LogInformation("Garantindo que o banco de dados exista e esteja na string de conexão :" +
        " '{connectionString}'", connectionString);
    using var db = services.CreateScope().ServiceProvider.GetRequiredService<TarefaDbContext>();
    await db.Database.EnsureCreatedAsync();
    await db.Database.MigrateAsync();
}
class Tarefa
{
    public int Id { get; set; }
    [Required]
    public string? Titulo { get; set; }
    public bool IsCompleta { get; set; }
}
class TarefaDbContext : DbContext
{
    public TarefaDbContext(DbContextOptions<TarefaDbContext> options)
        : base(options) { }
    public DbSet<Tarefa> Tarefas => Set<Tarefa>();
}

Este código foi obtido para um projeto simples onde não foram definidos recursos mais complexos como autenticação, a autorização, o uso de repositórios, serviços, etc.

É claro que não queremos trilhar por este caminho e podemos resolver este problema refatorando o código e estruturando o projeto para obter um código mais robusto.

Nota: Para simplificar não vamos usar o método AsseguraDbExiste() no projeto estruturado.

Estruturando uma Minimal API

Vamos então mostrar como estrutura este projeto de uma forma simples e objetiva.

Nosso ponto de partida será o projeto Tarefas que foi criado no artigo citado e que pode ser obtido no artigo.

Nosso objetivo será estruturar o arquivo Program.cs definindo um código mais enxuto e distribuindo os recursos usados em outras pastas e arquivos que iremos criar no projeto.

Para isso vamos usar o conceito de Composition Root que representa um local  exclusivo em um aplicativo onde os módulos são compostos juntos de forma que basta olhar o código que poderemos entender o seu propósito.

No nosso exemplo iremos compor o arquivo Program.cs de forma a conter o seguinte código:

Para isso vamos estrutura a nossa solução criando novas pastas de forma a obter a seguinte estrutura:

Vamos iniciar criando uma pasta que eu vou chamar Data onde iremos definir a classe Tarefa e a classe de contexto do EF Core.

Após criar a pasta Data vamos mover a classe Tarefa e a classe TarefaDbContext para esta pasta:

1- TarefaDbContext

using Microsoft.EntityFrameworkCore;

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

    public DbSet<Tarefa> Tarefas => Set<Tarefa>();
}
 

2- Tarefa

using System.ComponentModel.DataAnnotations;

public class Tarefa
{    
    public int Id { get; set; }
    [Required]
    public string? Titulo { get; set; }
    public bool IsCompleta { get; set; }
}

A seguir vamos criar a pasta Endpoints e nesta pasta vamos criar a classe estática TarefasEndpoints e nesta classe vamos criar o método de extensão MapTarefasEndpoints() para o recurso WebApplication:

1- MapTarefasEndPoints

namespace Tarefas.Endpoints;

using Microsoft.EntityFrameworkCore;

public static class TarefasEndpoints
{
    public static void MapTarefasEndpoints(this WebApplication app)
    {
        app.MapGet("/tarefas", async (TarefaDbContext db) =>
            await db.Tarefas.ToListAsync())
            .WithName("GetTarefas");

        app.MapGet("/tarefas/{id}", async (int id, TarefaDbContext db) =>
            await db.Tarefas.FindAsync(id)
                is Tarefa tarefa
                    ? Results.Ok(tarefa)
                    : Results.NotFound())
            .WithName("GetTarefaById")
            .Produces<Tarefa>(StatusCodes.Status200OK)
            .Produces(StatusCodes.Status404NotFound);

        app.MapPost("/tarefas", async (Tarefa tarefa, TarefaDbContext db) =>
        {
            if (tarefa != null)
            {
                db.Tarefas.Add(tarefa);
                await db.SaveChangesAsync();

                return Results.Created($"/tarefas/{tarefa.Id}", tarefa);
            }
            else
            {
                return Results.BadRequest("Request inválido");
            }
        }
        ).WithName("CreateTarefa")
         .ProducesValidationProblem()
         .Produces<Tarefa>(StatusCodes.Status201Created);

        app.MapPut("/tarefas/{id}", async (int id, Tarefa inputTarefa, TarefaDbContext db) =>
        {
            var tarefa = await db.Tarefas.FindAsync(id);

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

            tarefa.Titulo = inputTarefa.Titulo;
            tarefa.IsCompleta = inputTarefa.IsCompleta;

            await db.SaveChangesAsync();

            return Results.NoContent();
        })
            .WithName("UpdateTarefa")
            .ProducesValidationProblem()
            .Produces(StatusCodes.Status204NoContent)
            .Produces(StatusCodes.Status404NotFound);

        app.MapDelete("/tarefas/{id}", async (int id, TarefaDbContext db) =>
        {
            if (await db.Tarefas.FindAsync(id) is Tarefa tarefa)
            {
                db.Tarefas.Remove(tarefa);
                await db.SaveChangesAsync();
                return Results.Ok(tarefa);
            }

            return Results.NotFound();
        })
        .WithName("DeleteTarefa")
        .Produces(StatusCodes.Status204NoContent)
        .Produces(StatusCodes.Status404NotFound);

        app.MapDelete("/tarefas/delete-tarefas", async (TarefaDbContext db) =>
          Results.Ok(await db.Database.ExecuteSqlRawAsync("DELETE FROM Tarefas")))
          .WithName("DeleteTarefas")
          .Produces<int>(StatusCodes.Status200OK);
       }
  }

Para esta classe movemos os métodos onde definimos os endpoints da nossa API.

Agora vamos criar uma pasta chamada ApplicationBuilderExtensions onde vamos criar a classe de mesmo nome e onde vamos definir métodos de extensão :

  1. UseExceptionHandling
  2. UseAppCors
  3. UseSwaggerEndpoints

1- ApplicationBuilderExtensions

namespace Microsoft.AspNetCore.Builder;

internal static class ApplicationBuilderExtensions
{
    public static IApplicationBuilder UseExceptionHandling(this IApplicationBuilder app,
        IWebHostEnvironment environment)
    {
        if (environment.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        return app;
    }
    public static IApplicationBuilder UseAppCors(this IApplicationBuilder app)
    {
        app.UseCors(p =>
        {
            p.AllowAnyOrigin();
            p.WithMethods("GET");
            p.AllowAnyHeader();
        });
        return app;
    }

    public static IApplicationBuilder UseSwaggerEndpoints(this IApplicationBuilder app)
    {
        app.UseSwagger();
        app.UseSwaggerUI
(c =>{});

        return app;
    }
}

Finalmente vamos criar a pasta ServicesCollectionExtensions e nesta pasta vamos criar a classe de mesmo nome onde vamos definir os métodos de extensão :

  1. AddSwagger
  2. AddPersistence

1- ServicesCollectionExtensions

namespace Microsoft.Extensions.DependencyInjection;
using Microsoft.Data.Sqlite;
using Microsoft.OpenApi.Models;

public static class ServiceCollectionExtensions
{
    public static WebApplicationBuilder AddSwagger(this WebApplicationBuilder builder)
    {
        builder.Services.AddSwagger();
        return builder;
    }

    public static IServiceCollection AddSwagger(this IServiceCollection services)
    {
        services.AddEndpointsApiExplorer();
        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo()
            {
            });
        });
        return services;
    }

    public static WebApplicationBuilder AddPersistence(this WebApplicationBuilder builder)
    {
        var connectionString = builder.Configuration.GetConnectionString("SqliteConnectionString")
          ?? "Data Source=Tarefas.db";

        builder.Services.AddSqlite<TarefaDbContext>(connectionString);

        builder.Services.AddScoped(_ => new SqliteConnection(connectionString));

        return builder;
    } 
}

Finalmente temos o arquivo de Program.cs que agora ficou com o seguinte código :

using Tarefas.Endpoints;
var builder = WebApplication.CreateBuilder(args);
builder.AddSwagger();
builder.Services.AddCors();
builder.AddPersistence();
var app = builder.Build();
app.MapTarefasEndpoints();
var environment = app.Environment;
app
    .UseExceptionHandling(environment)
    .UseSwaggerEndpoints()
    .UseAppCors();
app.Run();

 

Executando o projeto iremos obter os endpoints na interface do Swagger:

Naturalmente em um projeto mais complexo teríamos mais trabalho a fazer mas creio que você já deve uma ideia do que podemos fazer para estruturar uma Minimal API. E assim podemos ter projetos complexos estruturados de forma a manter as boas práticas.

Pegue o projeto exemplo aqui:  Tarefas_Estruturado.zip (sem as referências)

"Portanto, irmãos, empenhem-se ainda mais para consolidar o chamado e a eleição de vocês, pois se agirem dessa forma, jamais tropeçarão, e assim vocês estarão ricamente providos quando entrarem no Reino eterno de nosso Senhor e Salvador Jesus Cristo."
2 Pedro 1:10,11

Referências:


José Carlos Macoratti