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)
"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