ASP .NET Core - Minimal API com PostgreSQL e EF Core - I
Neste artigo veremos como criar uma API usando a abordagem das Minimal APIs para realizar um CRUD básico em um banco de dados PostgreSQL usando o EF Core. |
Podemos dizer que vamos implementar um CRUD com base em um microsserviço RESTful usando o recurso das APIs mínimas da ASP.NET Core no ambiente do .NET 6.
Neste artigo você aprenderá como implementar Criar, Ler, Atualizar e Excluir endpoints REST.
Para resumir, nossa API web mínima vai usar os seguintes recursos:
Para acompanhar o artigo você terá que ter os seguintes recursos:
Criando o projeto ASP.NET Core Web API na pasta API
Abra o VS 2022, clique em New Project e selecione o template ASP .NET Core Web API e clique em Next;
Informe o nome CatalogoApi e clique em Next;
A seguir selecione o Framework, Authentication Type e demais configurações conforme mostra a figura:
Obs: Note que vamos usar as minimal APIs.
Clique em Create.
Com o projeto criado vamos criar as pastas Models e Context no projeto e incluir os seguintes pacotes:
Na pasta Models vamos criar as classes Produto e Categoria que representam o nosso modelo de domínio:
1- Categoria
public class Categoria { public int CategriaId { get; set; } public string? Nome { get; set; } public string? Descricao { get; set; } public ICollection<Produto> Produtos { get; set; } } |
2- Produto
using System.Text.Json.Serialization; public class Produto { public int ID { get; set; } public string? Nome { get; set; } public string? Descricao { get; set; } public decimal Preco { get; set; } public string? ImagemUrl { get; set; } public DateTime DataCompra { get; set; } public int Estoque { get; set; } public int CategoriaId { get; set; } [JsonIgnore] public Categoria Categoria { get; set; } } |
Observe que eu defini as propriedades de navegação nas entidades para que o EF Core possa inferir o relacionamento entre Categoria e Produto. Note também que definimos o atributo [JsonIgnore] para ignorar a propriedade lógica na serialização e na desserialização.
A seguir vamos criar nesta pasta a classe de contexto AppDbContext que herda de DbContext:
using CatalogoApi.Models; using Microsoft.EntityFrameworkCore;
namespace CatalogoApi.Context mb.Entity<Categoria>().HasKey(c => c.CategoriaId); mb.Entity<Produto>() |
Na definição do contexto estamos usando a Fluent API para definir o mapeamento das propriedades das entidade Produto e Categoria e também definindo o relacionamento um-para-muitos entre Categoria e Produto.
No arquivo appsettings.json vamos definir a string de conexão com o banco CatalogoDB do PostgreSQL que iremos criar:
{ "ConnectionStrings": { "DefaultConnection": "Host=localhost;Port=5432;Pooling=true;Database=CatalogoDB;User Id=seu_usuario;Password=sua_senha;" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" } |
No arquivo Program vamos registrar o serviço do contexto usando o provedor NpgSQL e obtendo a string de conexão:
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddDbContext<AppDbContext>(options => options.UseNpgsql(connectionString)); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); |
Aplicando o Migrations na abordagem Code-First
Já temos os pacotes instalados para trabalhar com o Migrations e vamos usar a abordagem Code-First do EF Core para criar o banco e as tabelas.
Para isso vamos usar a ferramenta de linha de comando do EF Core. A seguir temos os comandos para instalar ou atualizar a ferramenta:
Esta ferramenta
pode ser usada para os fins mencionados abaixo:
Para criar uma nova migração – isso é para gerar um novo arquivo de migração
Para remover uma migração existente – isso é para remover (excluir) o arquivo de
migração.
Para aplicar migrações a um banco de dados – isso é equivalente a criar um novo
banco de dados ou alterar o existente
Para remover o banco de dados existente - isso é equivalente a descartar o banco
de dados existente
Vamos então criar uma migração inicial estando na pasta do projeto e digitando o comando:
dotnet ef migrations add MigracaoInicial
Este comando vai criar a pasta Migrations no projeto contendo os arquivos 20220203113849_MigracaoInicial.cs que é o arquivo de script contendo os comandos que serão usados para gerar o banco e as tabelas.
Após verificar o arquivo gerado para aplicar a migração podemos usar o comando:
dotnet ef database update MigracaoInicial
Este comando vai gerar o banco e as tabelas no PostgreSQL e podemos usar o PgAdmin para fazer a verificação conforme mostra a figura:
Com isso já temos o banco de dados CatalogoDB e as tabelas Produtos e Categorias criadas no PostgreSQL e podemos continuar.
Criando os endpoints RESTFul da minimal API
Começaremos a
criar agora os endpoints da nossa API RESTful para realizar o CRUD básico em
Categorias e Produtos.
A seguir temos uma visão geral dos endpoints que iremos criar e dos verbos HTTP
que serão usados bem com os status code envolvidos:
Verbo HTTP | Endpoint | Status Code(s) | Descrição |
---|---|---|---|
POST | /categorias | 201 Created | Criar um recurso : |
GET | /categorias/{id:int} | 200 OK, 404 Not Found | Obtém um recurso pelo seu Id |
GET | /categorias | 200 OK | Retorna todos os recursos |
PUT | /categorias/{id:int} | 200 OK, 404 Not Found, 400 Bad Request | Atualiza um recurso |
DELETE | /categorias/{id:int} | 204 No Content | Deleta um recurso |
Os endpoints para Produtos seguem o mesma esquema.
1 - Criando um novo recurso - POST
Vamos iniciar com o endpoint para cri ar um novo recurso que pode ser Categoria ou Produto.
Para implementar a criação do endpoint para criar um recurso o verbo HTTP que usaremos será o POST.
A solicitação POST enviará uma representação JSON do objeto Produto ou Categoria como payload para a /categorias ou /produtos.
Após a criação do recurso ser bem sucedido, o corpo da resposta terá um novo recurso Categoria/Produto criado junto com o id.
O campo id é uma chave primária no banco de dados, e é gerado automaticamente pelo banco de dados.
O código de status de resposta será 201 Create.
Além disso, a resposta terá um cabeçalho Location definido para o recurso para ajudar a encontrar a nova nota criada.
Verbo HTTP | Endpoint | Status Code | Descrição da operação |
POST | /categorias | 201 Created | Cria uma categoria |
POST | /produtos | 201 Created | Cria um produto |
Para isso vamos usar o seguinte código:
app.MapPost("/categorias/", async (Categoria categoria, AppDbContext db) =>
{ db.Categorias.Add(categoria); await db.SaveChangesAsync(); return Results.Created($"/categorias/{categoria.CategoriaId}", categoria); }); |
Estamos usando o método de extensão MapPost em uma instância de app onde usamos dois parâmetros:
Em nosso caso estamos fazendo um tratamento assíncrono que usa dois parâmetros de entrada:
Como não estamos usando classes Controllers nem seus construtores, estamos realizando a injeção de dependência no parâmetro de entrada do delegate.
Assim, o MapPost tem o handler injetado com o payload da Categoria e com a dependência DbContext de AppDbContext injetada adicionará a Categoria recebida ao banco de dados.
Como o
handler de delegate está operando no modo
assíncrono, aguardaremos o contexto do banco de dados para salvar as alterações
no banco de dados.
Depois de criar com sucesso um registro de Categoria
no banco de dados, construiremos a resposta com o código de status
201 Created juntamente com o local que representa a
url da nova Categoria que acabou de ser criada
usando o código :
return Results.Created($"/categorias/{categoria.CategoriaId}", categoria);
Executando o projeto neste momento teremos a visualização abaixo:
Para Produtos o código do endpoint é praticamente o mesmo :
app.MapPost("/produtos/", async
(Produto produto, AppDbContext db) => { db.Produtos.Add(produto); await db.SaveChangesAsync(); return Results.Created($"/produtos/{produto.ProdutoId}", produto); }); |
2 - Lendo dados de um Categoria/Produdo - GET
Para criar o endpoint que lê uma única Categoria/Produto pelo seu ID vamos usar o verbo HTTP GET.
Verbo HTTP | Endpoint | Status Code | Descrição da operação |
GET | /categorias/{id:int} | 200 OK, 404 Not Found | Ele uma categoria pelo seu id |
GET | /produtos/{id:int} | 200 OK, 404 Not Found | Ele um produto pelo seu id |
Se o id solicitante do recurso for encontrado no banco de dados, a resposta terá uma representação JSON do objeto Categoria/Produto com o código de status 200 OK. Se o id não for encontrado, o código de status será 404 Not Found.
app.MapGet("/categorias/{id:int}",
async (int id, AppDbContext db) => { return await db.Categorias.FindAsync(id) is Categoria categoria ? Results.Ok(categoria) : Results.NotFound(); }); |
Neste código estamos chamando MapGet para realizar a operação GET.
O padrão de url também possui um parâmetro de rota {id:int} para indicar o id da Categoria que usamos para encontrar a Categoria.
3- Ler dados de todas as Categorias/Produtos - GET
Agora vamos implementar um endpoint para recuperar a coleção de objetos Categoria/Produto.
O verbo HTTP usado será o GET e o corpo da resposta será um array de representação JSON de objetos Categoria/Produto e o código de status será 200 OK.
Verbo HTTP | Endpoint | Status Code | Descrição da operação |
GET | /categorias | 200 OK | Retorna todas as categorias |
GET | /produtos | 200 OK | Retorna todos os produtos |
Aqui podemos retornar apenas as categorias ou as categorias com os seus produtos, neste caso usamos a cláusula include :
app.MapGet("/categorias", async (AppDbContext db) => await db.Categorias.ToListAsync()); |
app.MapGet("/categorias", async (AppDbContext
db) => await db.Categorias .Include(p=> p.Produtos) .ToListAsync()); |
4- Atualizar uma Categoria/Produto - PUT
Para atualizar um objeto Categoria/Produto podemos usar o verbo HTTP PUT. Vamos definir um endpoint para atualizar um objeto com base no seu id.
Verbo HTTP | Endpoint | Status Code | Descrição da operação |
PUT | /categorias/{id:int} | 200 OK, 404 Not Found | Atualiza uma categoria |
PUT | /produtos/{id:int} | 200 OK, 404 Not Found | Atualiza um produto |
Se o id solicitante do recurso for encontrado no banco de dados, a resposta terá uma representação JSON do objeto com código de status 200 OK. Se o id não for encontrado, o código de status será 404 Not Found.
app.MapPut("/categorias/{id:int}", async (int id, Categoria categoria, AppDbContext db) => { if (categoria.CategoriaId != id) { return Results.BadRequest(); } var categoriaDB = await db.Categorias.FindAsync(id); if (categoriaDB is null) return Results.NotFound(); categoriaDB.Nome = categoria.Nome; await db.SaveChangesAsync(); |
Para Produtos o código é o seguinte:
app.MapPut("/produtos/{id:int}", async (int id, Produto produto, AppDbContext db) => { if (produto.ProdutoId != id) { return Results.BadRequest(); } var produtoDB = await db.Produtos.FindAsync(id); if (produtoDB is null) return Results.NotFound(); produtoDB.Nome = produto.Nome; await db.SaveChangesAsync(); |
No código usado estamos chamando MapPut para realizar a operação PUT.
O
padrão de url também possui um parâmetro de rota {id:int} para indicar o id do
objeto.
Verificamos se o id do parâmetro de caminho corresponde ao id da Categoria obtida da
carga JSON. Quando esses id não correspondem, o resultado é
400 Bad Request. A seguir encontramos objeto com
base no ID de entrada.
Se o objeto com o id que está sendo solicitado para atualização não existir no
banco de dados, o resultado será 404 Not Found.
Quando o objeto for encontrado, vamos atualizar seus campos com a carga útil
recebida e salvaremos o objeto atualizado no banco de dados.
Finalmente respondemos com a objeto que foi atualizado como uma resposta JSON
com o código de status 200 OK.
5- Deletar um objeto Categoria/Produto - DELETE
Para excluir uma Categoria/Produto do banco de dados, usaremos o verbo HTTP DELETE.
O endpoint excluirá uma única Categoria/Produto do banco de dados com base em seu id.
Verbo HTTP | Endpoint | Status Code | Descrição da operação |
DELETE | /categorias/{id:int} | 204 No Content | Deleta uma categoria |
DELETE | /produtos/{id:int} | 204 No Content | Deleta a um produto |
A solicitação DELETE terá um id em seu URL do endpoint.
app.MapDelete("/categorias/{id:int}", async (int id, AppDbContext db) => { var categoria = await db.Categorias.FindAsync(id); if (categoria is not null) return Results.NoContent(); }); |
Com isso temos a implementação dos endpoints para realizar o CRUD em Categoria e Produto e ao final o código da nossa Minimal API ficou assim:
using CatalogoApi.Context; using CatalogoApi.Models; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddDbContext<AppDbContext>(options => builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddEndpointsApiExplorer(); var app = builder.Build(); app.MapPost("/categorias/", async (Categoria categoria, AppDbContext db) => { return Results.Created($"/categorias/{categoria.CategoriaId}", categoria); app.MapGet("/categorias", async (AppDbContext db) => await db.Categorias.ToListAsync()); app.MapPut("/categorias/{id:int}", async (int id, Categoria categoria, AppDbContext db) => var categoriaDB = await db.Categorias.FindAsync(id); if (categoriaDB is null) return Results.NotFound(); categoriaDB.Nome = categoria.Nome; await db.SaveChangesAsync(); app.MapDelete("/categorias/{id:int}", async (int id, AppDbContext db) => { var categoria = await db.Categorias.FindAsync(id); if (categoria is not null) return Results.NoContent(); }); app.MapPost("/produtos/", async (Produto produto, AppDbContext db) => { return Results.Created($"/produtos/{produto.ProdutoId}", produto); app.MapGet("/produtos", async (AppDbContext db) => await db.Produtos.ToListAsync()); app.MapGet("/produtos/{id:int}", async (int id, AppDbContext db) => app.MapPut("/produtos/{id:int}", async (int id, Produto produto, AppDbContext db) => var produtoDB = await db.Produtos.FindAsync(id); if (produtoDB is null) return Results.NotFound(); produtoDB.Nome = produto.Nome; await db.SaveChangesAsync(); app.MapDelete("/produtos/{id:int}", async (int id, AppDbContext db) => { var produto = await db.Produtos.FindAsync(id); if (produto is not null) return Results.NoContent(); }); if (app.Environment.IsDevelopment()) app.Run(); |
Executando o projeto teremos o resultado abaixo:
O que podemos concluir ?
O código gerado para gerenciar o CRUD para apenas duas entidades em um único arquivo ficou extenso e a tendência é aumentar ainda mais quando incluirmos outros recursos e precisarmos realizar outras configurações.
Assim, como o nome diz, as minimal APIs, devem ser usadas para projetos pequenos, protótipos ou provas visto que em um projeto complexo teremos um código que não será muito fácil manter.
Dessa forma devemos ver as minimal APIs como mais um recurso que podemos usar e não como um padrão que devemos seguir.
Naturalmente podemos organizar o código apresentado aqui e iremos fazer isso mais adiante, mas antes precisamos implementar a segurança em nossa API e vamos fazer isso no próximo artigo.
Pegue o projeto completo aqui : CatalogoApi_Inicio.zip
"Porque, se
vivemos, para o Senhor vivemos; se morremos, para o Senhor morremos. De
sorte que, ou vivamos ou morramos, somos do Senhor."
Romanos 14:8
Referências:
ASP .NET Core - Implementando a segurança com
ASP.NET Core MVC - Criando um Dashboard .
C# - Gerando QRCode - Macoratti
ASP .NET - Gerando QRCode com a API do Google
ASP .NET Core 2.1 - Como customizar o Identity
Usando o ASP .NET Core Identity - Macoratti
ASP .NET Core - Apresentando o IdentityServer4
ASP .NET Core 3.1 - Usando Identity de cabo a rabo