ASP .NET Core 6 - Explorando Minimal APIs (SQLite)
Hoje vamos continuar explorando o recurso Minimal APIs disponível a partir do .NET 6.0 para projetos ASP .NET Core 6. |
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.
API Mínima com EF Core, SQLite e Swagger
Neste artigo vamos criar uma API mínima, habilitar o Swagger e usar o EF Core para acessar um banco de dados SQLite.
A título de exercício vamos criar uma aplicação para gerenciar tarefas e realizar o CRUD básico no SQLite.
Eu vou usar o Visual Studio 2022 17.0 Preview 5.0 e criar um projeto usando o template “ASP.NET Core Empty” :
Nota: Se preferir usar o VS Code pode emitir o comando : dotnet new web -o MinApi na ferramenta de linha de comando .NET CLI.
Ao criar o projeto no VS 2022, após selecionar o template, informe o nome do projeto : Tarefas
A seguir vamos selecionar o Framework .NET 6.0 (que atualmente esta em RC1) e clicar em Create :
E a solução será criada com a seguinte estrutura :
Observe que temos apenas o arquivo appsettings.json e o arquivo Program e a pasta Properties.
Abrindo o arquivo Program temos um código simplificado usando o recurso do Top Level Statements do C# 9.0.
Antes de prosseguir temos que incluir no projeto as referências aos seguintes pacotes:
Podemos incluir os pacotes usando o menu Tools->Nuget Package Manager -> Manage Nuget Package for Solutions, clicar na guia Browse e selecionar e instalar os pacotes.
Nota: Para instalar as versões dos pacotes em pré-release marque a caixa - Include prerelease - no VS 2022.
Vamos começar definindo no final do arquivo Program a classe Tarefa que representa o nosso modelo de domínio e a classe de contexto TarefaDbContext que representa o contexto do EF Core.
class Tarefa { public int Id { get; set; } [Required] public string? Titulo { get; set; } public bool IsCompleta { get; set; } } |
A classe Tarefa contém apenas 3 propriedades : Id, Titulo e IsCompleta
class TarefaDbContext : DbContext { public TarefaDbContext(DbContextOptions<TarefaDbContext> options) : base(options) { } public DbSet<Tarefa> Tarefas => Set<Tarefa>(); |
A classe de contexto herda de DbContext e define o mapeamento ORM para a tabela Tarefas.
Agora podemos definir a string de conexão e o registro do contexto como um serviço usando a injeção de dependência.
Para isso vamos incluir o código destacado em azul conforme abaixo no arquivo Program:
using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("SqliteConnectionString")
builder.Services.AddSqlite<TarefaDbContext>(connectionString); builder.Services.AddEndpointsApiExplorer(); var app = builder.Build(); app.MapGet("/", () => "Hello World!"); app.Run(); |
A partir da versão RC1 do .NET 6 o middleware DeveloperExceptionPageMiddleware agora será registrado como o primeiro middleware se o ambiente atual for de desenvolvimento. Isso significa que IWebHostEnvironment.IsDevelopment() é verdadeiro.
Isso elimina a necessidade de registrar manualmente o middleware pelos desenvolvedores, conforme mostrado no exemplo abaixo.
if (!app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } |
No código acima também já estamos habilitando o swagger.
Precisamos definir a string de conexão com o SQLite no arquivo appsettings.json :
{ "ConnectionStrings": { "SqliteConnectionString": "Data Source=tarefas.db" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" } |
Como vamos usar o EF Core na abordagem Code-First, desejamos criar o banco de dados no SQLite, e para isso vamos criar um método chamado AsseguraDBExiste com o seguinte código:
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>(); |
Neste código estamos logando a informação sobre o banco e a string de conexão e resolvendo a dependência do contexto; TarefaDbContext estamos criando uma instância do contexto para a seguir aplicar qualquer migração pendente para o contexto existente usando o método MigrateAsync().
E agora podemos invocar este método :
await AsseguraDBExiste(app.Services, app.Logger);
Neste momento o código do arquivo Program deve estar da seguinte forma:
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations; var builder = WebApplication.CreateBuilder(args); var connectionString = builder.Configuration.GetConnectionString("TarefaDb")
builder.Services.AddSqlite<TarefaDbContext>(connectionString); builder.Services.AddEndpointsApiExplorer(); await AsseguraDBExiste(app.Services, app.Logger); app.MapGet("/", () => "Hello World!"); app.MapSwagger(); app.Run(); async Task AsseguraDBExiste(IServiceProvider services, ILogger logger) using var db = services.CreateScope().ServiceProvider.GetRequiredService<TarefaDbContext>(); class Tarefa class TarefaDbContext : DbContext public DbSet<Tarefa> Tarefas => Set<Tarefa>(); |
Executando o projeto neste momento teremos o resultado da execução:
E a exibição no navegador:
Agora podemos remover a linha de código : app.MapGet("/", () => "Hello World!");
Nota: No FireFox você pode obter um erro relacionado ao certificado. Isso pode ser corrigido mas é mais fácil usar o Chrome.
Realizando o CRUD
Vamos usar agora os métodos de extensão auxiliares Map{verbo} para configurar os endpoints.
Nota: Este recurso estava presente no .NET 5, mas agora eles dão suporte ao model binding, a integração com a injeção de dependência e acessam facilmente os dados da rota, o que torna a sua utilização mais completa.
Vamos iniciar definindo um endpoint para incluir uma tarefa na tabela tarefas do SQLite usando um MapPost.
Para isso vamos definir o código abaixo:
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); |
O metadado WithName mapeia o nome do endpoint para um operationId em documentos OpenAPI gerados. Observe que o nome do endpoint também é usado ao usar LinkGenerator para gerar URL/links para endpoints.
O recurso ProducesValidationProblem indica que o endpoint produzirá códigos de status 4xx http e detalhes de erro com application / validationproblem + json content type.
E o metadado
Produces<T>(...) indica quais tipos de resposta um método produz, onde
cada resposta é a combinação de:
- Um código de status HTTP;
- Um ou mais tipos de conteúdo, por exemplo “Application/json”;
- Um esquema opcional por tipo de conteúdo;
Ao executar o projeto teremos a exibição do endpoint na interface do swagger:
Vamos incluir um registro na tabela tarefas clicando no botão
Post e a seguir em Try
Out e vamos informar os valores conforme abaixo: (O valor do Id
não precisa ser informado)
Ao clicar no botão Execute iremos obter o seguinte resultado:
Temos assim um registro na tabela Tarefas. Vamos repetir o procedimento e incluir mais alguns registros para testar.
Agora que temos alguns registros na tabela vamos definir um endpoint para retornar todos os registros usando um MapGet com seguinte código:
app.MapGet("/tarefas", async (TarefaDbContext db) => await db.Tarefas.ToListAsync()) .WithName("GetTarefas"); |
Executando novamente o projeto teremos agora:
Acessando o endpoint Get /tarefas teremos o resultado abaixo:
A seguir vamos incluir um endpoint para retornar uma tarefa pelo seu id usando um MapGet:
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); |
Agora, para concluir vamos definir os endpoints para alterar um registro usando um MapPut e para excluir um registro usando um MapDelete:
1- MapPut (HttpPut)
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; await db.SaveChangesAsync(); return Results.NoContent();
|
2- MapDelete (HttpDelete)
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();
|
Se você quiser deletar todos os registros da tabela pode definir um endpoint usando MapDelete da seguinte forma:
app.MapDelete("/tarefas/delete-tarefas", async (TarefaDbContext db) =>
Results.Ok(await db.Database.ExecuteSqlRawAsync("DELETE FROM Tarefas")))
.WithName("DeleteTarefas")
.Produces<int>(StatusCodes.Status200OK);
|
No código acima estamos usando o método ExecuteSqlRawAsync que executa o SQL fornecido no banco de dados e retorna o número de linhas afetadas.
Obs:Naturalmente você deve evitar implementar um método para excluir todos os registros do banco em produção.
Com isso temos implementado o mapeamento de todos os endpoints usando os métodos auxiliares, e, o código completo do arquivo Program agora ficou assim:
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>();
}
|
Executando o projeto teremos acesso a todos os endpoints via interface do Swagger :
E poderemos realizar as operações CRUD e gerenciar as informações de tarefas armazenadas no SQLite usando os recursos das Minimal APIs.
Pegue o projeto exemplo aqui: Tarefas.zip (sem as referências)
"Ele (Jesus) não cometeu pecado
algum, e nenhum engano foi encontrado em sua boca.
Quando insultado, não revidava; quando sofria, não fazia ameaças, mas
entregava-se àquele que julga com justiça."
1 Pedro 2:22,23
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