 ASP .NET Core 6 - Explorando Minimal APIs (SQLite)
 
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)
 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