 ASP .NET Core 6 - Estruturando uma Minimal APIs (SQLite)
 
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 | 
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        
		app.MapGet("/tarefas/{id}", async (int id, 
		TarefaDbContext db) =>        
		app.MapPost("/tarefas", async (Tarefa 
		tarefa, TarefaDbContext db) =>                 
		return Results.Created($"/tarefas/{tarefa.Id}", tarefa);       
		 app.MapPut("/tarefas/{id}", async 
		(int id, Tarefa inputTarefa, TarefaDbContext db) => if (tarefa is null) return Results.NotFound();             
		tarefa.Titulo = inputTarefa.Titulo; await db.SaveChangesAsync();             
		return Results.NoContent();      
		  app.MapDelete("/tarefas/{id}", async 
		(int id, TarefaDbContext db) =>             
		return Results.NotFound();       
		 app.MapDelete("/tarefas/delete-tarefas", 
		async (TarefaDbContext db) => | 
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- ApplicationBuilderExtensions
| namespace Microsoft.AspNetCore.Builder; 
		internal static 
		class ApplicationBuilderExtensions     
		public static IApplicationBuilder 
		UseSwaggerEndpoints(this IApplicationBuilder app)         
		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- ServicesCollectionExtensions
| namespace Microsoft.Extensions.DependencyInjection; using Microsoft.Data.Sqlite; using Microsoft.OpenApi.Models; 
		public static class 
		ServiceCollectionExtensions     
		public static IServiceCollection AddSwagger(this 
		IServiceCollection services)     
		public static WebApplicationBuilder AddPersistence(this 
		WebApplicationBuilder builder) 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)
 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:
C# - Lendo e escrevendo em arquivos textos e binários
C# - Entendo o I/O na plataforma .NET
C# - Fluxo assíncrono ou async streams
C#- Apresentando Streams assíncronos