APIs mínimas são um recurso empolgante introduzido no .NET 6, projetado para revolucionar a forma como você cria APIs.
Imagine construir APIs robustas com código mínimo e zero boilerplate—sem mais lutar com controladores, roteamento ou middleware. É isso que as APIs mínimas permitem que você faça. A ideia com essas APIs é simplificar o processo de desenvolvimento, tornando-o incrivelmente fácil e eficiente.
Neste artigo, vamos mergulhar no mundo das APIs mínimas no .NET 8 e guiá-lo na criação de uma API de livraria totalmente funcional. Você aprenderá como obter todos os livros, recuperar um livro pelo seu ID, adicionar novos livros e até mesmo excluir livros. Vamos começar.
Índice
Pré-requisitos
Antes de começarmos, certifique-se de ter os seguintes pré-requisitos instalados em sua máquina:
-
Visual Studio Code ou qualquer outro editor de código de sua preferência
-
Kit de desenvolvimento C# para o Visual Studio Code
Alternativamente, você pode usar o Visual Studio 2022, que vem com suporte integrado para .NET 8. Mas neste artigo, estaremos usando o Visual Studio Code. É leve, fácil de usar e multiplataforma.
Vamos usar o Swagger UI para testar nossa API. O Swagger UI é uma ferramenta poderosa que permite interagir com sua API diretamente do seu navegador. Ele fornece uma interface amigável para testar os pontos finais da API, facilitando os testes e a depuração da sua API.
Ao criar um novo projeto, ele instalará automaticamente os pacotes necessários e configurará o projeto para usar o Swagger UI. O .NET 8 inclui o Swagger UI por padrão, então, quer você crie sua aplicação no Visual Studio ou com .NET, o Swagger UI será configurado para você.
Execute sua aplicação, e o Swagger UI abrirá automaticamente em seu navegador – mas como estamos usando o VS Code, precisamos clicar no número da porta em nosso terminal.
Você pode encontrar o código-fonte deste projeto no GitHub.
Introdução às APIs Mínimas
Imagine trabalhar em uma base de código com inúmeros endpoints, tornando-a bastante grande e complexa. Tradicionalmente, a construção de uma API no ASP.NET Core envolve o uso de controladores, roteamento, middleware e uma quantidade significativa de código boilerplate. Mas existem duas abordagens para construir uma API no ASP.NET Core: a tradicional e a minimalista.
A abordagem tradicional é familiar para a maioria dos desenvolvedores, envolvendo controladores e extenso código de infraestrutura. A abordagem minimalista, introduzida no .NET 6
, permite que você crie APIs com código mínimo e sem boilerplate. Esta abordagem simplifica o processo de desenvolvimento, permitindo que você se concentre em escrever lógica de negócios em vez de lidar com o código de infraestrutura.
As APIs mínimas são leves, rápidas e perfeitas para construir APIs de pequeno a médio porte. Elas são ideais para prototipagem, construção de microsserviços ou criação de APIs simples que não exigem muita complexidade. Neste manual, vamos explorar o mundo das APIs mínimas no .NET 6 e aprender como criar uma API de livraria totalmente funcional do zero.
Como Criar uma API Mínima
É fácil criar uma API mínima ao usar o dotnet CLI
, pois o modelo padrão já é uma API mínima. Mas se você usar o Visual Studio, precisará remover o código boilerplate que vem com o modelo do projeto.
Vamos começar usando o dotnet CLI
para criar um projeto de API mínimo.
dotnet new webapi -n BookStoreApi
O comando dotnet new webapi
cria um novo projeto de API mínimo chamado BookStoreApi
. Este projeto contém os arquivos e pastas necessários para você começar.
Vamos explorar a estrutura do projeto:
-
Program.cs
: O ponto de entrada da aplicação, onde o host é configurado. -
bookapi-minimal.sln
: O arquivo de solução que contém o projeto. -
bookapi-minimal.http
: Um arquivo que contém solicitações HTTP de exemplo para testar a API. -
bookapi-minimal.csproj
: O arquivo do projeto que contém a configuração do projeto. -
appsettings.json
: O arquivo de configuração que armazena as configurações da aplicação. -
appsettings.Development.json
: O arquivo de configuração para o ambiente de desenvolvimento.
Quando você abrir o arquivo program.cs, você notará que o código é mínimo. O arquivo Program.cs
contém o seguinte código:
var builder = WebApplication.CreateBuilder(args);
// Adicione serviços ao contêiner.
// Saiba mais sobre como configurar o Swagger/OpenAPI em https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure o pipeline de solicitação HTTP.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Se você ainda não entende completamente o código, não se preocupe—vamos abordá-lo detalhadamente nas próximas seções. A principal conclusão é que as APIs mínimas requerem muito pouco código, o que é uma das suas principais vantagens.
O código padrão configura uma API simples de previsão do tempo que você pode usar para testar sua configuração. Ele gera uma lista de previsões do tempo e as retorna quando você faz uma solicitação GET
para o endpoint /weatherforecast
. Além disso, o código inclui o Swagger UI para ajudá-lo a testar a API.
Preste atenção especial no método app.MapGet
, que mapeia uma rota para uma função de manipulador. Neste caso, ele mapeia a rota /weatherforecast
para uma função que retorna uma lista de previsões do tempo. Vamos usar métodos semelhantes para criar nossos próprios endpoints nas próximas seções.
Antes de começarmos a criar a estrutura de pastas do nosso projeto, vamos entender os métodos HTTP tanto em APIs baseadas em Controller quanto em APIs mínimas.
Métodos HTTP em APIs baseadas em Controller e APIs mínimas
Em uma abordagem baseada em Controller, que é a maneira tradicional de criar APIs da web, você precisa criar uma classe de controlador e definir métodos para cada método HTTP. Por exemplo:
-
Para criar um método
GET
, você usa o atributo[HttpGet]
. -
Para criar um método
POST
, você usa o atributo[HttpPost]
. -
Para criar um método
PUT
, você usa o atributo[HttpPut]
. -
Para criar um método
DELETE
, você usa o atributo[HttpDelete]
.
Assim são criados os endpoints em uma abordagem baseada em Controller.
Em contraste, as APIs Mínimas usam métodos como app.MapGet
, app.MapPost
, app.MapPut
e app.MapDelete
para criar endpoints. Essa é a principal diferença entre as duas abordagens: APIs baseadas em Controller usam atributos para definir endpoints, enquanto as APIs Mínimas usam métodos.
Agora que você entende como lidar com solicitações HTTP em APIs baseadas em Controller e APIs Mínimas, vamos criar a estrutura de pastas do nosso projeto.
Antes de criarmos a estrutura de pastas do nosso projeto, vamos primeiro executar o que temos. Como aprendemos anteriormente, ao criar um projeto com o Visual Studio ou .NET CLI, ele vem com um projeto WeatherForecast padrão que podemos executar e ver na interface do usuário. Vamos executá-lo para garantir que tudo funcione antes de prosseguirmos para criar nossa pasta de projeto.
Execute este comando:
dotnet run
Você deve ver a seguinte saída:
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5228
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: D:\Devolopemnt\Dotnet\bookapi-minimal
Isso significa que a aplicação está em execução e ouvindo em http://localhost:5228
. Como mencionei acima, como estamos usando o dotnet CLI
e o Visual Studio Code, a aplicação não abrirá automaticamente o navegador para nós. Precisamos fazer isso manualmente.
Abra seu navegador e acesse http://localhost:5228/swagger/index.html
para ver a resposta padrão da API.
Você deve ver algo assim:
Agora, a próxima coisa a fazer é encontrar uma maneira de estruturar nosso projeto e criar os arquivos e pastas necessários para começarmos.
Arquivos do Projeto Minimal API
Para organizar nosso projeto, vamos criar uma hierarquia de pastas estruturada. Isso ajudará a manter nosso código limpo e mantível. Aqui está a estrutura de pastas que iremos usar:
-
AppContext: Contém o contexto do banco de dados e configurações relacionadas.
-
Configurações: Contém configurações do Entity Framework Core e dados iniciais para o banco de dados.
-
Contratos: Contém Objetos de Transferência de Dados (DTOs) usados em nossa aplicação.
-
Pontos de Extremidade: Onde definimos e configuramos nossos pontos de extremidade de API minimalista.
-
Exceções: Contém classes de exceção personalizadas usadas no projeto.
-
Extensões: Contém métodos de extensão que utilizaremos ao longo do projeto.
-
Modelos: Contém modelos de lógica de negócios.
-
Serviços: Contém classes de serviço que implementam lógica de negócios.
-
Interfaces: Contém definições de interface usadas para mapear nossos serviços.
No Visual Studio Code, você pode criar esta estrutura de pastas da seguinte forma:
- AppContext
- Configurations
- Contracts
- Endpoints
- Exceptions
- Extensions
- Models
- Services
- Interfaces
Após a configuração, a estrutura da sua pasta de projeto deve se parecer com isso:
Agora que nossa estrutura de projeto está configurada, podemos prosseguir e começar a escrever nosso código. Vamos começar criando nossos modelos.
Como Criar os Modelos
Nesta seção, criaremos modelos para nossa aplicação. Os modelos são os blocos de construção da nossa aplicação, representando os dados com os quais nossa aplicação irá trabalhar. Para nosso exemplo, criaremos um modelo para um livro.
Para começar, crie uma pasta chamada Models
no diretório do seu projeto. Dentro desta pasta, crie um arquivo chamado BookModel.cs
e adicione o seguinte código:
// Models/BookModel.cs
namespace bookapi_minimal.Models
{
public class BookModel
{
public Guid Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public string Description { get; set; }
public string Category { get; set; }
public string Language { get; set; }
public int TotalPages { get; set; }
}
}
Esta classe BookModel
define as propriedades que representam os detalhes de um livro, como seu título
, autor
, descrição
, categoria
, idioma
e total de páginas
. Cada propriedade é projetada para conter informações específicas sobre o livro, facilitando a gestão e manipulação dos dados do livro dentro da nossa aplicação.
Agora que criamos nosso modelo, vamos criar nosso contexto de banco de dados.
Como Criar o Contexto de Banco de Dados
O contexto de banco de dados é uma classe que representa uma sessão com o banco de dados. É responsável por interagir com o banco de dados e executar operações de banco de dados. Em nossa aplicação, usaremos o Entity Framework Core para interagir com nosso banco de dados.
Instale os Pacotes Necessários
Antes de criar nosso contexto de banco de dados, precisamos instalar os seguintes pacotes:
-
Microsoft.EntityFrameworkCore
-
Microsoft.EntityFrameworkCore.SqlServer
-
FluentValidation.DependencyInjectionExtensions
Você pode instalar esses pacotes usando os seguintes comandos:
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package FluentValidation.DependencyInjectionExtensions
Verificar a Instalação dos Pacotes
Para verificar se os pacotes estão instalados, abra o arquivo bookapi-minimal.csproj
no diretório raiz do seu projeto. Você deve ver os pacotes instalados listados da seguinte forma:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>bookapi_minimal</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
</Project>
Isso confirma que os pacotes foram instalados com sucesso.
Agora vamos criar nosso contexto de banco de dados.
Na pasta AppContext, crie um novo arquivo chamado ApplicationContext.cs
e adicione o seguinte código:
// AppContext/ApplicationContext.cs
using bookapi_minimal.Models;
using Microsoft.EntityFrameworkCore;
namespace bookapi_minimal.AppContext
{
public class ApplicationContext(DbContextOptions<ApplicationContext> options) : DbContext(options)
{
// Esquema padrão para o contexto do banco de dados
private const string DefaultSchema = "bookapi";
// DbSet para representar a coleção de livros em nosso banco de dados
public DbSet<BookModel> Books { get; set; }
// Construtor para configurar o contexto do banco de dados
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.HasDefaultSchema(DefaultSchema);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationContext).Assembly);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationContext).Assembly);
}
}
}
Vamos analisar o código acima:
-
Nós definimos uma classe chamada
ApplicationContext
que herda deDbContext
. A classeDbContext
faz parte do Entity Framework Core e representa uma sessão com o banco de dados. -
O construtor aceita uma instância de
DbContextOptions<ApplicationContext>
. Este construtor é usado para configurar as opções do contexto do banco de dados. -
Nós definimos uma propriedade chamada
Books
do tipoDbSet<BookModel>
. Esta propriedade representa a coleção de livros em nosso banco de dados. -
Sobrescrevemos o método
OnModelCreating
para configurar o esquema do banco de dados e aplicar quaisquer configurações definidas em nossa aplicação.
Agora que criamos nosso contexto de banco de dados, vamos criar nosso método de extensão e registrar nosso contexto de banco de dados no contêiner de injeção de dependência.
Criar um Método de Extensão
Antes de criarmos o método de extensão, vamos entender o que é um método de extensão no contexto do ASP.NET Core.
Um método de extensão é um método estático que adiciona nova funcionalidade a um tipo existente sem modificar o tipo original. No ASP.NET Core, os métodos de extensão são comumente usados para estender a funcionalidade da interface IServiceCollection
, que é usada para registrar serviços no contêiner de injeção de dependência.
Os serviços são componentes que fornecem funcionalidades a uma aplicação, como acesso a banco de dados, logging e configuração. Ao criar um método de extensão para a interface IServiceCollection
, você pode simplificar o processo de registrar seus serviços no contêiner de injeção de dependência.
Em vez de colocar tudo no arquivo Program.cs
, criaremos um método de extensão para registrar nossos serviços no contêiner de injeção de dependência. Isso nos ajudará a manter nosso código limpo e organizado.
Na pasta Extensions
, crie um novo arquivo chamado ServiceExtensions.cs
e adicione o seguinte código:
using System.Reflection;
using bookapi_minimal.AppContext;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
namespace bookapi_minimal.Extensions
{
public static class ServiceExtensions
{
public static void AddApplicationServices(this IHostApplicationBuilder builder)
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
if (builder.Configuration == null) throw new ArgumentNullException(nameof(builder.Configuration));
// Adicionando o contexto do banco de dados
builder.Services.AddDbContext<ApplicationContext>(configure =>
{
configure.UseSqlServer(builder.Configuration.GetConnectionString("sqlConnection"));
});
// Adicionando validadores da montagem atual
builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
}
}
}
Vamos analisar o código acima:
-
Definimos uma classe estática chamada
ServiceExtensions
que contém um método de extensão chamadoAddApplicationServices
. Este método estende a interfaceIHostApplicationBuilder
, que é usada para configurar o pipeline de processamento de solicitações da aplicação. -
O método
AddApplicationServices
aceita uma instância deIHostApplicationBuilder
como parâmetro. Esse parâmetro é usado para acessar a configuração e os serviços da aplicação. -
Adicionamos o
ApplicationContext
ao contêiner de injeção de dependência e o configuramos para usar o SQL Server como provedor de banco de dados. Recuperamos a string de conexão do arquivoappsettings.json
usando o métodoGetConnectionString
. -
Adicionamos
validators
doassembly
atual usando o métodoAddValidatorsFromAssembly
. Esse método escaneia o assembly atual em busca de classes que implementam a interface IValidator e as registra no contêiner de injeção de dependência.
Em seguida, precisamos adicionar a string de conexão ao arquivo appsettings.json
. Adicione o seguinte código ao seu arquivo appsettings.json
:
{
"ConnectionStrings": {
"sqlConnection": "Server=localhost\\SQLEXPRESS02;Database=BookAPIMinimalAPI;Integrated Security=true;TrustServerCertificate=true;"
}
}
Certifique-se de substituir your_password
pela sua senha real do SQL Server.
Seu arquivo appsettings.json
deve se parecer com isto:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"sqlConnection": "Server=localhost\\SQLEXPRESS02;Database=BookAPIMinimalAPI;Integrated Security=true;TrustServerCertificate=true;"
},
"AllowedHosts": "*"
}
Parabéns! Você criou com sucesso o contexto do banco de dados, o método de extensão e a string de conexão para sua aplicação. Na próxima seção, criaremos um Contrato.
Como Criar um Contrato
Contratos são Objetos de Transferência de Dados (DTOs) que definem a estrutura dos dados trocados entre o cliente e o servidor. Em nossa aplicação, vamos criar contratos para representar os dados enviados e recebidos pelos endpoints da nossa API.
Aqui estão os contratos que vamos criar:
-
CreateBookRequest: Isso representa os dados enviados ao criar um novo livro.
-
UpdateBookRequest: Isso representa os dados enviados ao atualizar um livro existente.
-
BookResponse: Representa os dados retornados ao recuperar um livro.
-
ErrorResponse: Representa a resposta de erro retornada quando ocorre uma exceção.
-
ApiResponse: Representa a resposta retornada pela API.
No diretório Contracts
, crie um novo arquivo chamado CreateBookRequest
e adicione o seguinte código:
// Contracts/CreateBookRequest.cs
namespace bookapi_minimal.Contracts
{
public record CreateBookRequest
{
public string Title { get; init; }
public string Author { get; init; }
public string Description { get; init; }
public string Category { get; init; }
public string Language { get; init; }
public int TotalPages { get; init; }
}
}
No diretório Contracts
, crie um novo arquivo chamado UpdateBookRequest
e adicione o seguinte código:
// Contratos/UpdateBookRequest.cs
namespace bookapi_minimal.Contracts
{
public record UpdateBookRequest
{
public string Title { get; set; }
public string Author { get; set; }
public string Description { get; set; }
public string Category { get; set; }
public string Language { get; set; }
public int TotalPages { get; set; }
}
}
Na pasta Contratos
, crie um novo arquivo chamado BookResponse
e adicione o seguinte código:
// Contratos/BookResponse.cs
namespace bookapi_minimal.Contracts
{
public record BookResponse
{
public Guid Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public string Description { get; set; }
public string Category { get; set; }
public string Language { get; set; }
public int TotalPages { get; set; }
}
}
Na pasta Contratos
, crie um novo arquivo chamado ErrorResponse
e adicione o seguinte código:
// Contratos/ErrorResponse.cs
namespace bookapi_minimal.Contracts
{
public record ErrorResponse
{
public string Title { get; set; }
public int StatusCode { get; set; }
public string Message { get; set; }
}
}
Na pasta Contratos
, crie um novo arquivo chamado ApiResponse
e adicione o seguinte código:
// Contratos/ApiResponse.cs
namespace bookapi_minimal.Contracts
{
public class ApiResponse<T>
{
public T Data { get; set; }
public string Message { get; set; }
public ApiResponse(T data, string message)
{
Data = data;
Message = message;
}
}
}
Esses contratos nos ajudam a definir a estrutura dos dados trocados entre o cliente e o servidor, facilitando o trabalho com os dados em nossa aplicação.
Na próxima seção, criaremos serviços para implementar a lógica de negócios de nossa aplicação.
Como Adicionar Serviços
Serviços são componentes que fornecem funcionalidades a uma aplicação. Em nossa aplicação, criaremos serviços para implementar a lógica de negócios de nossa aplicação. Criaremos serviços para lidar com operações CRUD para livros, validar dados de livros e lidar com exceções.
No ASP.NET Core, os serviços são registrados no contêiner de injeção de dependência e podem ser injetados em outros componentes, como controladores e pontos de extremidade, mas este é um API mínima, então injetaremos os serviços diretamente nos pontos de extremidade.
Vamos criar uma interface para nossos serviços. Na pasta Interfaces
, crie um novo arquivo chamado IBookService.cs
e adicione o seguinte código:
// Interfaces/IBookService.cs
using bookapi_minimal.Contracts;
namespace bookapi_minimal.Interfaces
{
public interface IBookService
{
Task<BookResponse> AddBookAsync(CreateBookRequest createBookRequest);
Task<BookResponse> GetBookByIdAsync(Guid id);
Task<IEnumerable<BookResponse>> GetBooksAsync();
Task<BookResponse> UpdateBookAsync(Guid id, UpdateBookRequest updateBookRequest);
Task<bool> DeleteBookAsync(Guid id);
}
}
Vamos analisar o código acima: Definimos uma interface chamada IBookService
que contém métodos para lidar com operações CRUD para livros. A interface define os seguintes métodos:
-
AddBookAsync
: Adiciona um novo livro ao banco de dados. -
GetBookByIdAsync
: Recupera um livro pelo seu ID. -
GetBooksAsync
: Recupera todos os livros do banco de dados. -
UpdateBookAsync
: Atualiza um livro existente.
Estamos utilizando o Contrato que criamos anteriormente na pasta Contracts
. A interface IBookService
define a estrutura dos métodos que serão implementados pelas classes de serviço. Isso nos ajuda a separar a interface da implementação, facilitando a manutenção e teste de nosso código.
Agora que criamos a interface, vamos criar a classe de serviço que implementa a interface.
Como Implementar o Serviço de Livros
Este serviço irá implementar a interface IBookService
e fornecer a lógica de negócios para nossa aplicação. Na pasta Services
, crie um novo arquivo chamado BookService.cs
. Seu arquivo inicial deve se parecer com isso:
// Services/BookService.cs
namespace bookapi_minimal.Services
{
public class BookService
{
}
}
A primeira coisa que precisamos fazer é adicionar a interface à classe BookService
. Atualize a classe BookService
para implementar a interface IBookService
da seguinte forma:
// Services/BookService.cs
using bookapi_minimal.Interfaces;
namespace bookapi_minimal.Services
{
public class BookService:IBookService
{
}
}
Ao fazer isso, seu VS Code pode mostrar um erro porque não implementamos os métodos na interface. Vamos em frente e implementar os métodos na classe BookService
.
No VS Code, você pode usar o atalho Ctrl + .
para implementar os métodos na interface. Em seguida, você verá o seguinte código gerado para você:
using bookapi_minimal.Contracts;
using bookapi_minimal.Interfaces;
namespace bookapi_minimal.Services
{
// Classe de serviço para gerenciar livros
public class BookService : IBookService
{
// Método para adicionar um novo livro ao banco de dados
public Task<BookResponse> AddBookAsync(CreateBookRequest createBookRequest)
{
throw new NotImplementedException();
}
// Método para excluir um livro do banco de dados
public Task<bool> DeleteBookAsync(Guid id)
{
throw new NotImplementedException();
}
// Método para obter um livro do banco de dados pelo seu ID
public Task<BookResponse> GetBookByIdAsync(Guid id)
{
throw new NotImplementedException();
}
// Método para obter todos os livros do banco de dados
public Task<IEnumerable<BookResponse>> GetBooksAsync()
{
throw new NotImplementedException();
}
// Método para atualizar um livro no banco de dados
public Task<BookResponse> UpdateBookAsync(Guid id, UpdateBookRequest updateBookRequest)
{
throw new NotImplementedException();
}
}
}
Agora você pode ver que os métodos na interface foram implementados na classe BookService
. Vamos implementar a lógica de negócios para cada método na próxima seção.
Antes de fazermos isso, vamos adicionar as dependências necessárias à classe BookService
. Precisamos injetar as dependências ApplicationContext
e ILogger
na classe BookService
. ApplicationContext
é usado para interagir com o banco de dados, enquanto ILogger
é usado para fazer o log.
Para injetar as dependências, atualize a classe BookService
da seguinte forma:
// Services/BookService.cs
// ...
private readonly ApplicationContext _context; // Contexto do banco de dados
private readonly ILogger<BookService> _logger; // Logger para informações e erros de log
//..
Como adicionamos as dependências, precisamos atualizar o construtor da classe BookService
para aceitar as dependências. Atualize o construtor da classe BookService
da seguinte forma:
// Services/BookService.cs
// ...
// Construtor para inicializar o contexto do banco de dados e o logger
public BookService(ApplicationContext context, ILogger<BookService> logger)
{
_context = context;
_logger = logger;
}
// ...
Agora que adicionamos as dependências e atualizamos o construtor, podemos implementar a lógica de negócios para cada método na classe BookService
.
Vamos criar a lógica para as operações DE CRIAÇÃO, LEITURA, ATUALIZAÇÃO e EXCLUSÃO na classe BookService
.
Como Implementar o Método AddBookAsync
Como mencionado anteriormente, vamos usar o método AddBookAsync
para adicionar um novo livro ao banco de dados. Neste método, vamos criar uma nova entidade de livro, mapear os dados do objeto CreateBookRequest
para a entidade do livro e salvar a entidade do livro no banco de dados. Também iremos retornar a entidade do livro como um objeto BookResponse
.
Atualize o método AddBookAsync
na classe BookService
da seguinte forma:
// Services/BookService.cs
// ...
/// <summary>
/// Adicionar um novo livro
/// </summary>
/// <param name="createBookRequest">Requisição do livro a ser adicionado</param>
/// <returns>Detalhes do livro criado</returns>
public async Task<BookResponse> AddBookAsync(CreateBookRequest createBookRequest)
{
try
{
var book = new BookModel
{
Title = createBookRequest.Title,
Author = createBookRequest.Author,
Description = createBookRequest.Description,
Category = createBookRequest.Category,
Language = createBookRequest.Language,
TotalPages = createBookRequest.TotalPages
};
// Adicionar o livro ao banco de dados
_context.Books.Add(book);
await _context.SaveChangesAsync();
_logger.LogInformation("Book added successfully.");
// Retornar os detalhes do livro criado
return new BookResponse
{
Id = book.Id,
Title = book.Title,
Author = book.Author,
Description = book.Description,
Category = book.Category,
Language = book.Language,
TotalPages = book.TotalPages
};
}
catch (Exception ex)
{
_logger.LogError($"Error adding book: {ex.Message}");
throw;
}
}
// ...
Neste código, estamos criando uma nova entidade de livro a partir do objeto CreateBookRequest
, mapeando os dados do objeto CreateBookRequest
para a entidade de livro, salvando a entidade de livro no banco de dados e retornando a entidade de livro como um objeto BookResponse
.
Também estamos registrando informações e erros usando a dependência ILogger
. Se ocorrer uma exceção durante o processo, registramos a mensagem de erro e relançamos a exceção.
Agora que implementamos o método AddBookAsync
, vamos implementar o método GetBookByIdAsync
.
Como Implementar o Método GetBookByIdAsync
O método GetBookByIdAsync
é usado para recuperar um livro pelo seu ID do banco de dados. Neste método, iremos consultar o banco de dados para o livro com o ID especificado, mapear a entidade do livro para um objeto BookResponse
e retornar o objeto BookResponse
.
Atualize o método GetBookByIdAsync
na classe BookService
da seguinte forma:
// Serviços/BookService.cs
//...
/// <summary>
/// Obter um livro pelo seu ID
/// </summary>
/// <param name="id">ID do livro</param>
/// <returns>Detalhes do livro</returns>
public async Task<BookResponse> GetBookByIdAsync(Guid id)
{
try
{
// Encontrar o livro pelo seu ID
var book = await _context.Books.FindAsync(id);
if (book == null)
{
_logger.LogWarning($"Book with ID {id} not found.");
return null;
}
// Retornar os detalhes do livro
return new BookResponse
{
Id = book.Id,
Title = book.Title,
Author = book.Author,
Description = book.Description,
Category = book.Category,
Language = book.Language,
TotalPages = book.TotalPages
};
}
catch (Exception ex)
{
_logger.LogError($"Error retrieving book: {ex.Message}");
throw;
}
}
//...
Neste código, estamos consultando o banco de dados para o livro com o ID especificado, mapeando a entidade do livro para um objeto BookResponse
e retornando o objeto BookResponse
. Também estamos registrando informações e erros usando a dependência ILogger
.
Se o livro com o ID especificado não for encontrado, registramos uma mensagem de aviso e retornamos null. Se ocorrer uma exceção durante o processo, registramos a mensagem de erro e relançamos a exceção.
Agora que implementamos o método GetBookByIdAsync
, vamos implementar o método GetBooksAsync
.
Como Implementar o Método GetBooksAsync
O método GetBooksAsync
é usado para recuperar todos os livros do banco de dados. Neste método, iremos consultar o banco de dados para todos os livros, mapear cada entidade de livro para um objeto BookResponse
e retornar uma lista de objetos BookResponse
. Atualize o método GetBooksAsync
na classe BookService
da seguinte forma:
// Services/BookService.cs
//...
/// <summary>
/// Obter todos os livros
/// </summary>
/// <returns>Lista de todos os livros</returns>
public async Task<IEnumerable<BookResponse>> GetBooksAsync()
{
try
{
// Obter todos os livros do banco de dados
var books = await _context.Books.ToListAsync();
// Retornar os detalhes de todos os livros
return books.Select(book => new BookResponse
{
Id = book.Id,
Title = book.Title,
Author = book.Author,
Description = book.Description,
Category = book.Category,
Language = book.Language,
TotalPages = book.TotalPages
});
}
catch (Exception ex)
{
_logger.LogError($"Error retrieving books: {ex.Message}");
throw;
}
}
//...
Aqui, estamos consultando o banco de dados para todos os livros, mapeando cada entidade de livro para um objeto BookResponse
e retornando uma lista de objetos BookResponse
. Também estamos registrando informações e erros usando a dependência ILogger
. Se ocorrer uma exceção durante o processo, registramos a mensagem de erro e relançamos a exceção.
Agora que implementamos o método GetBooksAsync
, vamos implementar o método UpdateBookAsync
.
Como Implementar o Método UpdateBookAsync
O método UpdateBookAsync
é usado para atualizar um livro existente no banco de dados. Neste método, iremos consultar o banco de dados para o livro com o ID especificado, atualizar a entidade do livro com os dados do objeto UpdateBookRequest
, salvar a entidade do livro atualizada no banco de dados e retornar a entidade do livro atualizada como um objeto BookResponse
. Atualize o método UpdateBookAsync
na classe BookService
da seguinte forma:
// Services/BookService.cs
//...
/// <summary>
/// Atualizar um livro existente
/// </summary>
/// <param name="id">ID do livro a ser atualizado</param>
/// <param name="book">Modelo de livro atualizado</param>
/// <returns>Detalhes do livro atualizado</returns>
public async Task<BookResponse> UpdateBookAsync(Guid id, UpdateBookRequest book)
{
try
{
// Encontrar o livro existente pelo seu ID
var existingBook = await _context.Books.FindAsync(id);
if (existingBook == null)
{
_logger.LogWarning($"Book with ID {id} not found.");
return null;
}
// Atualizar os detalhes do livro
existingBook.Title = book.Title;
existingBook.Author = book.Author;
existingBook.Description = book.Description;
existingBook.Category = book.Category;
existingBook.Language = book.Language;
existingBook.TotalPages = book.TotalPages;
// Salvar as alterações no banco de dados
await _context.SaveChangesAsync();
_logger.LogInformation("Book updated successfully.");
// Retornar os detalhes do livro atualizado
return new BookResponse
{
Id = existingBook.Id,
Title = existingBook.Title,
Author = existingBook.Author,
Description = existingBook.Description,
Category = existingBook.Category,
Language = existingBook.Language,
TotalPages = existingBook.TotalPages
};
}
catch (Exception ex)
{
_logger.LogError($"Error updating book: {ex.Message}");
throw;
}
}
//...
Aqui, estamos consultando o banco de dados pelo livro com o ID especificado, atualizando a entidade do livro com os dados do objeto UpdateBookRequest
, salvando a entidade do livro atualizada no banco de dados e retornando a entidade do livro atualizada como um objeto BookResponse
. Também estamos registrando informações e erros usando a dependência ILogger
.
Se o livro com o ID especificado não for encontrado, registramos uma mensagem de aviso e retornamos nulo. Se ocorrer uma exceção durante o processo, registramos a mensagem de erro e relançamos a exceção.
Agora que implementamos o método UpdateBookAsync
, vamos implementar o método DeleteBookAsync
.
Como Implementar o Método DeleteBookAsync
O método DeleteBookAsync
é usado para excluir um livro existente do banco de dados. Neste método, vamos consultar o banco de dados pelo livro com o ID especificado, remover a entidade do livro do banco de dados e retornar um valor booleano indicando se o livro foi excluído com sucesso.
Atualize o método DeleteBookAsync
na classe BookService
da seguinte forma:
// Services/BookService.cs
//...
/// <summary>
/// Excluir um livro pelo seu ID
/// </summary>
/// <param name="id">ID do livro a ser excluído</param>
/// <returns>Verdadeiro se o livro foi excluído, falso caso contrário</returns>
public async Task<bool> DeleteBookAsync(Guid id)
{
try
{
// Encontrar o livro pelo seu ID
var book = await _context.Books.FindAsync(id);
if (book == null)
{
_logger.LogWarning($"Book with ID {id} not found.");
return false;
}
// Remover o livro do banco de dados
_context.Books.Remove(book);
await _context.SaveChangesAsync();
_logger.LogInformation($"Book with ID {id} deleted successfully.");
return true;
}
catch (Exception ex)
{
_logger.LogError($"Error deleting book: {ex.Message}");
throw;
}
}
//...
Neste código, estamos consultando o banco de dados para o livro com o ID especificado, removendo a entidade do livro do banco de dados e retornando um valor booleano indicando se o livro foi excluído com sucesso. Também estamos registrando informações e erros usando a dependência ILogger
.
Se o livro com o ID especificado não for encontrado, registramos uma mensagem de aviso e retornamos falso. Se ocorrer uma exceção durante o processo, registramos a mensagem de erro e lançamos a exceção novamente.
Agora você implementou com sucesso a lógica de negócios para os métodos AddBookAsync
, GetBookByIdAsync
, GetBooksAsync
, UpdateBookAsync
e DeleteBookAsync
na classe BookService
. Esses métodos lidam com as operações CRUD para livros, validam os dados do livro e tratam exceções. Neste ponto, sua classe BookService
deve estar assim:
using bookapi_minimal.AppContext;
using bookapi_minimal.Contracts;
using bookapi_minimal.Interfaces;
using bookapi_minimal.Models;
using Microsoft.EntityFrameworkCore;
namespace bookapi_minimal.Services
{
public class BookService : IBookService
{
private readonly ApplicationContext _context; // Contexto do banco de dados
private readonly ILogger<BookService> _logger; // Logger para registrar informações e erros
// Construtor para inicializar o contexto do banco de dados e o logger
public BookService(ApplicationContext context, ILogger<BookService> logger)
{
_context = context;
_logger = logger;
}
/// Adicionar um novo livro
/// </summary>
/// <param name="createBookRequest">Solicitação de livro a ser adicionada</param>
/// <returns>Detalhes do livro criado</returns>
public async Task<BookResponse> AddBookAsync(CreateBookRequest createBookRequest)
{
try
{
var book = new BookModel
{
Title = createBookRequest.Title,
Author = createBookRequest.Author,
Description = createBookRequest.Description,
Category = createBookRequest.Category,
Language = createBookRequest.Language,
TotalPages = createBookRequest.TotalPages
};
// Adicionar o livro ao banco de dados
_context.Books.Add(book);
await _context.SaveChangesAsync();
_logger.LogInformation("Book added successfully.");
// Retornar os detalhes do livro criado
return new BookResponse
{
Id = book.Id,
Title = book.Title,
Author = book.Author,
Description = book.Description,
Category = book.Category,
Language = book.Language,
TotalPages = book.TotalPages
};
}
catch (Exception ex)
{
_logger.LogError($"Error adding book: {ex.Message}");
throw;
}
}
/// <summary>
/// Obter um livro pelo seu ID
/// </summary>
/// <param name="id">ID do livro</param>
/// <returns>Detalhes do livro</returns>
public async Task<BookResponse> GetBookByIdAsync(Guid id)
{
try
{
// Encontrar o livro pelo seu ID
var book = await _context.Books.FindAsync(id);
if (book == null)
{
_logger.LogWarning($"Book with ID {id} not found.");
return null;
}
// Retornar os detalhes do livro
return new BookResponse
{
Id = book.Id,
Title = book.Title,
Author = book.Author,
Description = book.Description,
Category = book.Category,
Language = book.Language,
TotalPages = book.TotalPages
};
}
catch (Exception ex)
{
_logger.LogError($"Error retrieving book: {ex.Message}");
throw;
}
}
/// <summary>
/// Obter todos os livros
/// </summary>
/// <returns>Lista de todos os livros</returns>
public async Task<IEnumerable<BookResponse>> GetBooksAsync()
{
try
{
// Obter todos os livros do banco de dados
var books = await _context.Books.ToListAsync();
// Retornar os detalhes de todos os livros
return books.Select(book => new BookResponse
{
Id = book.Id,
Title = book.Title,
Author = book.Author,
Description = book.Description,
Category = book.Category,
Language = book.Language,
TotalPages = book.TotalPages
});
}
catch (Exception ex)
{
_logger.LogError($"Error retrieving books: {ex.Message}");
throw;
}
}
/// <summary>
/// Atualizar um livro existente
/// </summary>
/// <param name="id">ID do livro a ser atualizado</param>
/// <param name="book">Modelo de livro atualizado</param>
/// <returns>Detalhes do livro atualizado</returns>
public async Task<BookResponse> UpdateBookAsync(Guid id, UpdateBookRequest book)
{
try
{
// Encontrar o livro existente pelo seu ID
var existingBook = await _context.Books.FindAsync(id);
if (existingBook == null)
{
_logger.LogWarning($"Book with ID {id} not found.");
return null;
}
// Atualizar os detalhes do livro
existingBook.Title = book.Title;
existingBook.Author = book.Author;
existingBook.Description = book.Description;
existingBook.Category = book.Category;
existingBook.Language = book.Language;
existingBook.TotalPages = book.TotalPages;
// Salvar as alterações no banco de dados
await _context.SaveChangesAsync();
_logger.LogInformation("Book updated successfully.");
// Retornar os detalhes do livro atualizado
return new BookResponse
{
Id = existingBook.Id,
Title = existingBook.Title,
Author = existingBook.Author,
Description = existingBook.Description,
Category = existingBook.Category,
Language = existingBook.Language,
TotalPages = existingBook.TotalPages
};
}
catch (Exception ex)
{
_logger.LogError($"Error updating book: {ex.Message}");
throw;
}
}
/// <summary>
/// Excluir um livro pelo seu ID
/// </summary>
/// <param name="id">ID do livro a ser excluído</param>
/// <returns>Verdadeiro se o livro foi excluído, falso caso contrário</returns>
public async Task<bool> DeleteBookAsync(Guid id)
{
try
{
// Encontrar o livro pelo seu ID
var book = await _context.Books.FindAsync(id);
if (book == null)
{
_logger.LogWarning($"Book with ID {id} not found.");
return false;
}
// Remover o livro do banco de dados
_context.Books.Remove(book);
await _context.SaveChangesAsync();
_logger.LogInformation($"Book with ID {id} deleted successfully.");
return true;
}
catch (Exception ex)
{
_logger.LogError($"Error deleting book: {ex.Message}");
throw;
}
}
}
}
Parabéns! Você implementou com sucesso a lógica de negócios para os métodos AddBookAsync
, GetBookByIdAsync
, GetBooksAsync
, UpdateBookAsync
e DeleteBookAsync
na classe BookService
.
Há uma coisa que precisamos fazer: precisamos registrar o serviço em nosso método de extensão. Vamos em frente e fazer isso.
No seu arquivo ServiceExtensions.cs
, adicione o seguinte código:
// Extensions/ServiceExtensions.cs
//..
builder.Services.AddScoped<IBookService, BookService>();
//...
Isto irá registrar a classe BookService
como um serviço de escopo. Isso significa que o serviço será criado uma vez por requisição e será descartado após a conclusão da requisição.
Agora que o serviço está funcionando, vamos em frente e criar as classes de exceção.
Como Criar Exceções
O tratamento adequado de exceções é crucial para garantir a estabilidade e confiabilidade de uma aplicação. No contexto do ASP.NET Core, existem dois tipos principais de exceções:
-
Exceções do Sistema: Estas são exceções lançadas pelo tempo de execução do .NET ou pelo sistema subjacente.
-
Exceções de Aplicação: Estas são exceções lançadas pelo código da aplicação para lidar com erros ou condições específicas.
No ASP.NET Core com .NET 8, foi introduzido um novo recurso chamado tratamento global de exceções. Esse recurso permite lidar com exceções globalmente em sua aplicação, tornando mais fácil gerenciar erros e fornecer uma experiência de usuário consistente.
Em nossa aplicação, vamos criar classes de exceção personalizadas para lidar com erros e condições específicas. Também vamos aproveitar o recurso de tratamento global de exceções para gerenciar exceções globalmente, garantindo uma abordagem uniforme para o tratamento de erros em toda a aplicação.
Vamos criar as seguintes classes de exceção:
-
NoBookFoundException
: Lançada quando um livro com o ID especificado não é encontrado. -
BookDoesNotExistException
: Lançada quando um livro com o ID especificado não existe. -
GlobalExceptionHandler
: Lida com exceções globalmente na aplicação.
No diretório Exceptions
, crie um novo arquivo chamado NoBookFoundException.cs
e adicione o seguinte código:
// Exceptions/NoBookFoundException.cs
namespace bookapi_minimal.Exceptions
{
public class NoBookFoundException : Exception
{
public NoBookFoundException() : base("No books found")
{}
}
}
Neste código, estamos criando uma classe de exceção personalizada chamada NoBookFoundException
que herda da classe Exception
. A classe NoBookFoundException
é usada para lidar com o cenário em que nenhum livro é encontrado no banco de dados. Também estamos fornecendo uma mensagem de erro personalizada para a exceção.
Na pasta Exceptions
, crie um novo arquivo chamado BookDoesNotExistException.cs
e adicione o seguinte código:
namespace bookapi_minimal.Exceptions
{
public class BookDoesNotExistException : Exception
{
private int id { get; set; }
public BookDoesNotExistException(int id) : base($"Book with id {id} does not exist")
{
this.id = id;
}
}
}
Neste código, estamos criando uma classe de exceção personalizada chamada BookDoesNotExistException
que herda da classe Exception
. A classe BookDoesNotExistException
é usada para lidar com o cenário em que um livro com o ID especificado não existe no banco de dados. Também estamos fornecendo uma mensagem de erro personalizada para a exceção.
Na pasta Exceptions
, crie um novo arquivo chamado GlobalExceptionHandler.cs
e adicione o seguinte código:
// Exceções/GlobalExceptionHandler.cs
using System.Net;
using bookapi_minimal.Contracts;
using Microsoft.AspNetCore.Diagnostics;
namespace bookapi_minimal.Exceptions
{
// Classe global de tratamento de exceções que implementa IExceptionHandler
public class GlobalExceptionHandler : IExceptionHandler
{
private readonly ILogger<GlobalExceptionHandler> _logger;
// Construtor para inicializar o logger
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
{
_logger = logger;
}
// Método para tratar exceções de forma assíncrona
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
// Registrar os detalhes da exceção
_logger.LogError(exception, "An error occurred while processing your request");
var errorResponse = new ErrorResponse
{
Message = exception.Message,
Title = exception.GetType().Name
};
// Determinar o código de status com base no tipo de exceção
switch (exception)
{
case BadHttpRequestException:
errorResponse.StatusCode = (int)HttpStatusCode.BadRequest;
break;
case NoBookFoundException:
case BookDoesNotExistException:
errorResponse.StatusCode = (int)HttpStatusCode.NotFound;
break;
default:
errorResponse.StatusCode = (int)HttpStatusCode.InternalServerError;
break;
}
// Definir o código de status da resposta
httpContext.Response.StatusCode = errorResponse.StatusCode;
// Escrever a resposta de erro como JSON
await httpContext.Response.WriteAsJsonAsync(errorResponse, cancellationToken);
// Retornar verdadeiro para indicar que a exceção foi tratada
return true;
}
}
}
Vamos analisar o código acima:
-
Definimos uma classe chamada
GlobalExceptionHandler
que implementa a interfaceIExceptionHandler
. A interfaceIExceptionHandler
é usada para tratar exceções globalmente na aplicação. -
A classe
GlobalExceptionHandler
contém um construtor que inicializa a dependênciaILogger<GlobalExceptionHandler>
. OILogger
é usado para registrar informações e erros. -
O método
TryHandleAsync
é usado para lidar com exceções de forma assíncrona. Este método aceita os parâmetrosHttpContext
,Exception
eCancellationToken
. -
Registramos os detalhes da exceção usando a dependência
ILogger
. -
Criamos um objeto
ErrorResponse
para representar a resposta de erro retornada pela API. O objetoErrorResponse
contém a mensagem de erro, título e código de status. -
Determinamos o código de status com base no tipo de exceção. Se a exceção for uma
BadHttpRequestException
, definimos o código de status paraBadRequest
. Se a exceção for umaNoBookFoundException
ouBookDoesNotExistException
, definimos o código de status paraNotFound
. Caso contrário, definimos o código de status paraInternalServerError
. -
Definimos o código de status da resposta usando a propriedade
httpContext.Response.StatusCode
. -
Escrevemos a resposta de erro como JSON usando o método
httpContext.Response.WriteAsJsonAsync
. -
Retornamos
true
para indicar que a exceção foi tratada com sucesso.
Agora que criamos as classes de exceção, vamos registrar o GlobalExceptionHandler
no contêiner de injeção de dependência. Como criamos um método de extensão para registrar serviços no contêiner de injeção de dependência, adicionaremos o GlobalExceptionHandler
à classe ServiceExtensions
.
Atualize a classe ServiceExtensions
na pasta Extensions
da seguinte forma:
// Extensões/ServiceExtensions.cs
//...
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
//...
O método AddExceptionHandler
registra o GlobalExceptionHandler
no contêiner de injeção de dependência. O método AddProblemDetails
registra a classe ProblemDetails
no contêiner de injeção de dependência.
Agora que registramos o GlobalExceptionHandler
no contêiner de injeção de dependência, podemos usá-lo para lidar com exceções globalmente em nossa aplicação. Na próxima seção, criaremos os pontos de extremidade da API para interagir com os dados do livro.
Como Criar os Pontos de Extremidade da API
No contexto de APIs mínimas no ASP.NET Core, existem várias maneiras de configurar seus pontos de extremidade.
Você pode defini-los diretamente em seu arquivo Program.cs
. Mas à medida que seu projeto cresce e você precisa adicionar mais pontos de extremidade ou funcionalidades, é útil organizar melhor seu código. Uma maneira de fazer isso é criando uma classe separada para lidar com todos os pontos de extremidade.
Como discutimos anteriormente, as APIs mínimas não usam controladores ou visualizações como as aplicações tradicionais do ASP.NET Core. Em vez disso, elas usam métodos como MapGet
, MapPost
, MapPut
e MapDelete
para definir métodos HTTP e rotas para os pontos de extremidade da API.
Para começar, navegue até a pasta Endpoints
e crie um novo arquivo chamado BookEndpoints.cs
. Adicione o seguinte código ao arquivo:
// Endpoints/BookEndpoints.cs
namespace bookapi_minimal.Endpoints
{
public static class BookEndPoint
{
public static IEndpointRouteBuilder MapBookEndPoint(this IEndpointRouteBuilder app)
{
return app;
}
}
}
A classe BookEndpoints
contém um método MapBookEndPoint
que retorna um objeto IEndpointRouteBuilder
. O objeto IEndpointRouteBuilder
é usado para definir os métodos HTTP e rotas para os endpoints da API. Nas próximas seções, definiremos os endpoints da API para criar
, ler
, atualizar
e deletar
livros.
Como Criar o Endpoint AddBookAsync
para Livros
Nesta seção, criaremos o endpoint AddBookAsync
. Este endpoint aceitará um objeto Book
como um payload JSON e o adicionará ao banco de dados. Usaremos o método MapPost
para definir o método HTTP e a rota para este endpoint.
Adicione o seguinte código à classe BookEndpoints
:
// Endpoints/BookEndpoints.cs
//...
// Endpoint para adicionar um novo livro
app.MapPost("/books", async (CreateBookRequest createBookRequest, IBookService bookService) =>
{
var result = await bookService.AddBookAsync(createBookRequest);
return Results.Created($"/books/{result.Id}", result);
});
//...
-
Definição da Rota: O método MapPost define a rota para o endpoint como
/books
. -
Modelo de Requisição: O endpoint aceita um objeto
CreateBookRequest
como um payload JSON. O objetoCreateBookRequest
contém os dados necessários para criar um novo livro. -
Modelo de resposta: O endpoint retorna um objeto
Book
como carga JSON. O objetoBook
contém os dados do livro recém-criado. -
Valor de retorno: O endpoint retorna um resultado
Created
. O resultadoCreated
contém a localização do livro recém-criado e o objetoBook
.
Como criar o endpoint do livro GetBookAsync
Nesta seção, criaremos o endpoint GetBookAsync
. Este endpoint aceitará um ID de livro como parâmetro de consulta e retornará o livro com o ID especificado. Usaremos o método MapGet
para definir o método HTTP e a rota para este endpoint.
Adicione o seguinte código à classe BookEndpoints
:
// Endpoints/BookEndpoints.cs
// ...
// Endpoint para obter todos os livros
app.MapGet("/books", async (IBookService bookService) =>
{
var result = await bookService.GetBooksAsync();
return Results.Ok(result);
});
//...
-
Definição da Rota: O método MapGet define a rota para o endpoint como
/livros
. -
Modelo de Requisição: O endpoint aceita um objeto
Book
como um payload JSON. O objetoBook
contém os dados necessários para criar um novo livro. -
Modelo de Resposta: O endpoint retorna um objeto
Book
como um payload JSON. O objetoBook
contém os dados do livro recém-criado. -
Valor de Retorno: O endpoint retorna um resultado
Ok
. O resultadoOk
contém o objetoBook
.
Como Criar o Endpoint de Livro GetBookByIdAsync
Nesta seção, criaremos o endpoint GetBookByIdAsync
. Este endpoint irá aceitar um ID de livro como parâmetro de rota e retornar o livro com o ID especificado. Usaremos o método MapGet
para definir o método HTTP e a rota para este endpoint.
Adicione o seguinte código à classe BookEndpoints
:
// Endpoints/BookEndpoints.cs
//...
// Endpoint para obter um livro por ID
app.MapGet("/books/{id:guid}", async (Guid id, IBookService bookService) =>
{
var result = await bookService.GetBookByIdAsync(id);
return result != null ? Results.Ok(result) : Results.NotFound();
});
//...
-
Definição de Rota: O método MapGet define a rota para o endpoint como
/livros/{id:guid}
. O parâmetro{id:guid}
especifica que o parâmetroid
deve ser um GUID. -
Modelo de Requisição: O endpoint aceita um objeto
Book
como um payload JSON. O objetoBook
contém os dados necessários para criar um novo livro. -
Modelo de Resposta: O endpoint retorna um objeto
Book
como um payload JSON. O objetoBook
contém os dados para o livro recém-criado. -
Valor de Retorno: O endpoint retorna um resultado
Ok
se o livro for encontrado. O resultadoNotFound
é retornado se o livro não for encontrado.
Como Criar o Endpoint do Livro UpdateBookAsync
Nesta seção, vamos criar o endpoint UpdateBookAsync
. Este endpoint irá aceitar um ID de livro como parâmetro de rota e um objeto Book
como carga JSON e atualizar o livro com o ID especificado. Vamos usar o método MapPut
para definir o método HTTP e a rota para este endpoint.
Adicione o seguinte código à classe BookEndpoints
:
// Endpoints/BookEndpoints.cs
//...
// Endpoint para atualizar um livro por ID
app.MapPut("/books/{id:guid}", async (Guid id, UpdateBookRequest updateBookRequest, IBookService bookService) =>
{
var result = await bookService.UpdateBookAsync(id, updateBookRequest);
return result != null ? Results.Ok(result) : Results.NotFound();
});
//...
-
Definição de Rota: O método MapPut define a rota para o endpoint como
/livros/{id:guid}
. O parâmetro{id:guid}
especifica que o parâmetroid
deve ser um GUID. -
Modelo de Solicitação: O endpoint aceita um objeto
Livro
como carga JSON. O objetoLivro
contém os dados necessários para criar um novo livro. -
Modelo de Resposta: O endpoint retorna um objeto
Livro
como carga JSON. O objetoLivro
contém os dados para o livro recém-criado. -
Valor de Retorno: O endpoint retorna um resultado
Ok
se o livro for encontrado. O resultadoNotFound
é retornado se o livro não for encontrado.
Como Criar o Endpoint do Livro DeleteBookAsync
Nesta seção, criaremos o endpoint DeleteBookAsync
. Este endpoint aceitará um ID de livro como parâmetro de rota e excluirá o livro com o ID especificado. Usaremos o método MapDelete
para definir o método HTTP e a rota para este endpoint.
Adicione o seguinte código à classe BookEndpoints
:
// Endpoints/BookEndpoints.cs
//...
// Endpoint para excluir um livro por ID
app.MapDelete("/books/{id:guid}", async (Guid id, IBookService bookService) =>
{
var result = await bookService.DeleteBookAsync(id);
return result ? Results.NoContent() : Results.NotFound();
});
//...
-
Definição de Rota: O método MapDelete define a rota para o endpoint como
/books/{id:guid}
. O parâmetro{id:guid}
especifica que o parâmetroid
deve ser um GUID. -
Modelo de Requisição: O endpoint aceita um objeto
Book
como carga útil JSON. O objetoBook
contém os dados necessários para criar um novo livro. -
Modelo de Resposta: O endpoint retorna um objeto
Book
como carga útil JSON. O objetoBook
contém os dados do livro recém-criado. -
Valor de Retorno: O endpoint retorna um resultado
NoContent
se o livro for excluído com sucesso. O resultadoNotFound
é retornado se o livro não for encontrado.
Agora que definimos todos os métodos para os endpoints de livro. Portanto, sua classe de endpoint deve se parecer com isso:
// Endpoints/BookEndpoints.cs
using bookapi_minimal.Contracts;
using bookapi_minimal.Interfaces;
namespace bookapi_minimal.Endpoints
{
public static class BookEndPoint
{
public static IEndpointRouteBuilder MapBookEndPoint(this IEndpointRouteBuilder app)
{
// Definir os endpoints
// Endpoint para adicionar um novo livro
app.MapPost("/books", async (CreateBookRequest createBookRequest, IBookService bookService) =>
{
var result = await bookService.AddBookAsync(createBookRequest);
return Results.Created($"/books/{result.Id}", result);
});
// Endpoint para obter todos os livros
app.MapGet("/books", async (IBookService bookService) =>
{
var result = await bookService.GetBooksAsync();
return Results.Ok(result);
});
// Endpoint para obter um livro por ID
app.MapGet("/books/{id:guid}", async (Guid id, IBookService bookService) =>
{
var result = await bookService.GetBookByIdAsync(id);
return result != null ? Results.Ok(result) : Results.NotFound();
});
// Endpoint para atualizar um livro por ID
app.MapPut("/books/{id:guid}", async (Guid id, UpdateBookRequest updateBookRequest, IBookService bookService) =>
{
var result = await bookService.UpdateBookAsync(id, updateBookRequest);
return result != null ? Results.Ok(result) : Results.NotFound();
});
// Endpoint para excluir um livro por ID
app.MapDelete("/books/{id:guid}", async (Guid id, IBookService bookService) =>
{
var result = await bookService.DeleteBookAsync(id);
return result ? Results.NoContent() : Results.NotFound();
});
return app;
}
}
}
Parabéns! Você criou todos os endpoints para a API de livros. Os endpoints lidam com as operações CRUD para livros e retornam as respostas apropriadas com base na solicitação e nos dados.
Como Registrar os Endpoints
Depois de definir os pontos de extremidade da API para a API de livros, o próximo passo é registrar esses pontos de extremidade no arquivo Program.cs
. Vamos usar o método MapBookEndpoints
para registrar os pontos de extremidade do livro.
Também devemos limpar nossa classe Program.cs
para garantir que ela permaneça organizada e mantida.
// Program.cs
using System.Reflection;
using bookapi_minimal.Endpoints;
using bookapi_minimal.Services;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.AddApplicationServices();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c=>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Mimal API", Version = "v1", Description = "Showing how you can build minimal " +
"api with .net" });
// Defina o caminho dos comentários para o Swagger JSON e UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
var app = builder.Build();
// Configurar o pipeline de solicitação HTTP.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseExceptionHandler();
app.MapGroup("/api/v1/")
.WithTags(" Book endpoints")
.MapBookEndPoint();
app.Run();
Vamos dividir os principais componentes do arquivo Program.cs
:
-
AddApplicationServices: Este método registra os serviços necessários para a API. É um método de extensão que criamos anteriormente para adicionar serviços ao contêiner de injeção de dependência.
-
AddSwaggerGen: Este método registra o gerador do Swagger, que é usado para criar a documentação do Swagger para a API. Especificamos o título, versão e descrição da API no documento do Swagger.
-
MapGroup: Este método agrupa os endpoints. Ele recebe um caminho como parâmetro e retorna um objeto
IEndpointRouteBuilder
. Usamos o métodoWithTags
para adicionar tags aos endpoints e o métodoMapBookEndpoints
para registrar os endpoints do livro. -
Run: Este método inicia a aplicação.
Para habilitar a documentação Swagger, você precisa adicionar a propriedade GenerateDocumentationFile
ao seu arquivo .csproj
. Neste exemplo, o arquivo é chamado bookapi-minimal.csproj
, mas o nome pode variar de acordo com seu projeto.
Adicione a linha a seguir ao seu arquivo .csproj
:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
Ao final, bookapi-minimal.csproj deve se parecer com isso:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RootNamespace>bookapi_minimal</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
</Project>
Agora que registramos os endpoints do livro no arquivo Program.cs
, podemos iniciar a aplicação e testar os endpoints da API usando o Swagger.
Ao executar o aplicativo, você deve ver a documentação do Swagger no seguinte URL: https://localhost:5001/swagger/index.html
. A documentação do Swagger fornece informações sobre os endpoints da API, modelos de requisição e resposta, e permite que você teste os endpoints diretamente do navegador. Você deve ver algo assim:
Parabéns! Você implementou a lógica de negócios para o serviço de livros, criou exceções personalizadas, definiu os endpoints da API e registrou os endpoints no arquivo Program.cs
. Você também habilitou a documentação do Swagger para testar os endpoints da API.
Como Adicionar Dados Iniciais ao Banco de Dados
Um passo importante adicional é popular o banco de dados com dados iniciais quando o aplicativo é iniciado. Esses dados iniciais irão preencher o banco de dados, permitindo que você teste seus endpoints da API sem adicionar dados manualmente.
Vamos adicionar alguns dados iniciais antes de realizar migrações e testar nossos endpoints da API.
Para realizar isso, vamos criar uma nova classe em nossa pasta de Configuração chamada BookTypeConfigurations
e adicionar o seguinte código:
using bookapi_minimal.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace bookapi_minimal.Configurations
{
public class BookTypeConfigurations : IEntityTypeConfiguration<BookModel>
{
public void Configure(EntityTypeBuilder<BookModel> builder)
{
// Configurar o nome da tabela
builder.ToTable("Books");
// Configurar a chave primária
builder.HasKey(x => x.Id);
// Configurar propriedades
builder.Property(x => x.Id).ValueGeneratedOnAdd();
builder.Property(x => x.Title).IsRequired().HasMaxLength(100);
builder.Property(x => x.Author).IsRequired().HasMaxLength(100);
builder.Property(x => x.Description).IsRequired().HasMaxLength(500);
builder.Property(x => x.Category).IsRequired().HasMaxLength(100);
builder.Property(x => x.Language).IsRequired().HasMaxLength(50);
builder.Property(x => x.TotalPages).IsRequired();
// Dados iniciais
builder.HasData(
new BookModel
{
Id = Guid.NewGuid(),
Title = "The Alchemist",
Author = "Paulo Coelho",
Description = "The Alchemist follows the journey of an Andalusian shepherd",
Category = "Fiction",
Language = "English",
TotalPages = 208
},
new BookModel
{
Id = Guid.NewGuid(),
Title = "To Kill a Mockingbird",
Author = "Harper Lee",
Description = "A novel about the serious issues of rape and racial inequality.",
Category = "Fiction",
Language = "English",
TotalPages = 281
},
new BookModel
{
Id = Guid.NewGuid(),
Title = "1984",
Author = "George Orwell",
Description = "A dystopian social science fiction novel and cautionary tale about the dangers of totalitarianism. ",
Category = "Fiction",
Language = "English",
TotalPages = 328
}
);
}
}
}
Vamos analisar o código acima:
No Entity Framework Core, você pode usar a interface IEntityTypeConfiguration
para configurar o tipo de entidade e os dados iniciais para o banco de dados. A classe BookTypeConfigurations
implementa a interface IEntityTypeConfiguration<BookModel>
e fornece a configuração para a entidade BookModel
.
-
Método Configure: Este método é usado para configurar o tipo de entidade
BookModel
. Ele define o nome da tabela, a chave primária e as propriedades para a entidadeBookModel
.-
Nome da Tabela: O método
ToTable
especifica o nome da tabela a ser criada no banco de dados. Neste caso, o nome da tabela é definido como “Books”. -
Chave Primária: O método
HasKey
especifica a chave primária para a entidadeBookModel
. A chave primária é definida como a propriedadeId
. -
Propriedades: O método
Property
configura as propriedades da entidadeBookModel
. Ele especifica o tipo de dados, comprimento e restrições para cada propriedade.
-
-
Dados Seed: O método
HasData
insere dados iniciais no banco de dados. Ele cria três objetosBookModel
com dados de amostra para testar os endpoints da API.
Agora que criamos a classe BookTypeConfigurations
, precisamos registrar essa configuração na classe ApplicationContext
. Isso garante que a configuração seja aplicada quando o banco de dados é criado ou migrado.
Finalmente, estamos quase prontos para testar nossa API. Mas antes disso, precisamos realizar migrações para criar o banco de dados e aplicar os dados iniciais.
Lembre-se que adicionamos a string de conexão do banco de dados no arquivo appsettings.json
? Agora vamos realizar uma migração e depois atualizar nosso banco de dados para que a migração tenha efeito.
Como Realizar uma Migração
Migrações permitem que você atualize o esquema do banco de dados com base nas alterações feitas em suas classes de modelo. No Entity Framework Core, você pode usar o comando dotnet ef migrations add
para criar uma nova migração refletindo essas alterações.
Para realizar uma migração, execute o seguinte comando no terminal:
dotnet ef migrations add InitialCreate
Se o comando for bem-sucedido, você deverá ver uma saída semelhante a esta:
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
Agora você verá uma nova pasta chamada Migrations
em seu projeto. Esta pasta contém os arquivos de migração que foram criados com base nas alterações feitas em suas classes de modelo. Esses arquivos de migração incluem os comandos SQL necessários para atualizar o esquema do banco de dados.
Como Atualizar o Banco de Dados
Após criar a migração, você precisa aplicá-la para atualizar o esquema do banco de dados. Você pode usar o comando dotnet ef database update
para aplicar a migração e atualizar o banco de dados. Certifique-se de que o SQL Server está em execução.
Execute o seguinte comando no terminal:
dotnet ef database update
Isso irá atualizar o esquema do banco de dados com base nas alterações feitas em suas classes de modelo. Certifique-se de que não haja erros na string de conexão do seu banco de dados.
Como Testar os Endpoints da API
Agora podemos testar nossos endpoints usando o Swagger. Para fazer isso, execute a aplicação executando o seguinte comando no terminal:
dotnet run
Isso executará nossa aplicação. Você pode abrir seu navegador e acessar https://localhost:5001/swagger/index.html
para acessar a documentação do Swagger. Você deverá ver uma lista de endpoints da API, modelos de requisição e resposta, e a capacidade de testar os endpoints diretamente do navegador.
Se o número da porta for diferente de 5001
, não se preocupe – ainda funcionará. A porta pode mudar dependendo do tipo de máquina que você está usando, mas ainda alcançará o mesmo resultado.
Como Testar o Endpoint Obter Todos os Livros
Para testar o endpoint Obter Todos os Livros
, siga estes passos:
-
Na documentação do Swagger, clique no endpoint
GET /api/v1/books
. -
Clique no botão
Experimente
. -
Clique no botão
Executar
.
Isso enviará uma requisição para a API para recuperar todos os livros no banco de dados.
Você deverá ver a resposta da API, que incluirá a lista de livros que foram inseridos no banco de dados.
A imagem abaixo mostra a resposta da API:
Como Testar o Endpoint Obter Livro por ID
Para testar o endpoint Obter Livro por ID
, siga estes passos:
-
No documento do Swagger, clique no endpoint
GET /api/v1/books/{id}
. -
Insira o ID de um livro no campo
id
. Você pode usar um dos IDs de livros que foram inseridos no banco de dados. -
Clique no botão
Tentar
.
Isso enviará uma solicitação para a API para recuperar o livro com o ID especificado. Você deve ver a resposta da API, que incluirá o livro com o ID especificado.
A imagem abaixo mostra a resposta da API:
Como Testar o Endpoint Adicionar Livro
Para testar o endpoint Adicionar Livro
, siga estes passos:
-
No documento do Swagger, clique no endpoint
POST /api/v1/books
. -
Clique no botão
Tentar
. -
Insira os detalhes do livro no corpo da solicitação.
-
Clique no botão
Executar
.
Isso enviará uma solicitação para a API para adicionar um novo livro ao banco de dados.
Você deverá ver a resposta da API, que incluirá o livro recém-criado.
A imagem abaixo mostra a resposta da API:
Como Testar o Endpoint Atualizar Livro
Para testar o endpoint Atualizar Livro
, siga estas etapas:
-
No documento do Swagger, clique no endpoint
PUT /api/v1/books/{id}
. -
Insira o ID de um livro no campo
id
. Você pode usar o ID de um dos livros que acabamos de adicionar. -
Clique no botão
Experimente
.
Isso enviará uma solicitação para a API para atualizar o livro com o ID especificado.
Você deverá ver a resposta da API, que incluirá o livro atualizado.
A imagem abaixo mostra a resposta da API:
Como Testar o Endpoint Deletar Livro
Para testar o endpoint Deletar Livro
, siga estas etapas:
- No documento do Swagger, clique no endpoint
DELETE /api/v1/books/{id}
. -
Insira o ID de um livro no campo
id
. Você pode usar qualquer um dos IDs dos livros que acabamos de adicionar ou dos dados predefinidos. -
Clique no botão
Try it out
.
Isso enviará uma solicitação para a API para excluir o livro com o ID especificado.
A imagem abaixo mostra a resposta da API:
Parabéns! Você implementou todas as operações CRUD para livros e testou os endpoints da API usando o Swagger, verificando que funcionam conforme o esperado. Agora você pode construir sobre essa base para adicionar mais recursos e funcionalidades à sua API.
Conclusão
Este manual explorou como criar uma API mínima no ASP.NET Core com .NET 8. Construímos uma API de livros abrangente que suporta operações CRUD, implementamos exceções personalizadas, definimos e registramos endpoints da API e habilitamos a documentação do Swagger para testes fáceis.
Seguindo este tutorial, você adquiriu uma base sólida para construir APIs mínimas com o ASP.NET Core. Agora você pode aplicar esse conhecimento e criar APIs robustas para diversos domínios e indústrias.
Espero que tenha achado este tutorial útil e informativo. Obrigado por ler!
Sinta-se à vontade para se conectar comigo nas redes sociais:
Source:
https://www.freecodecamp.org/news/create-a-minimal-api-in-net-core-handbook/