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:

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:

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 de DbContext. A classe DbContext 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 tipo DbSet<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 chamado AddApplicationServices. Este método estende a interface IHostApplicationBuilder, que é usada para configurar o pipeline de processamento de solicitações da aplicação.

  • O método AddApplicationServices aceita uma instância de IHostApplicationBuilder 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 arquivo appsettings.json usando o método GetConnectionString.

  • Adicionamos validators do assembly atual usando o método AddValidatorsFromAssembly. 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 interface IExceptionHandler. A interface IExceptionHandler é usada para tratar exceções globalmente na aplicação.

  • A classe GlobalExceptionHandler contém um construtor que inicializa a dependência ILogger<GlobalExceptionHandler>. O ILogger é 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âmetros HttpContext, Exception e CancellationToken.

  • 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 objeto ErrorResponse 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 para BadRequest. Se a exceção for uma NoBookFoundException ou BookDoesNotExistException, definimos o código de status para NotFound. Caso contrário, definimos o código de status para InternalServerError.

  • 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 objeto CreateBookRequest contém os dados necessários para criar um novo livro.

  • Modelo de resposta: O endpoint retorna um objeto Book como carga JSON. O objeto Book contém os dados do livro recém-criado.

  • Valor de retorno: O endpoint retorna um resultado Created. O resultado Created contém a localização do livro recém-criado e o objeto Book.

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 objeto Book 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 objeto Book contém os dados do livro recém-criado.

  • Valor de Retorno: O endpoint retorna um resultado Ok. O resultado Ok contém o objeto Book.

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âmetro id deve ser um GUID.

  • Modelo de Requisição: O endpoint aceita um objeto Book como um payload JSON. O objeto Book 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 objeto Book 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 resultado NotFound é 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âmetro id deve ser um GUID.

  • Modelo de Solicitação: O endpoint aceita um objeto Livro como carga JSON. O objeto Livro contém os dados necessários para criar um novo livro.

  • Modelo de Resposta: O endpoint retorna um objeto Livro como carga JSON. O objeto Livro 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 resultado NotFound é 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âmetro id deve ser um GUID.

  • Modelo de Requisição: O endpoint aceita um objeto Book como carga útil JSON. O objeto Book 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 objeto Book 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 resultado NotFound é 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étodo WithTags para adicionar tags aos endpoints e o método MapBookEndpoints 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 entidade BookModel.

    • 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 entidade BookModel. A chave primária é definida como a propriedade Id.

    • Propriedades: O método Property configura as propriedades da entidade BookModel. 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 objetos BookModel 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:

  1. Na documentação do Swagger, clique no endpoint GET /api/v1/books.

  2. Clique no botão Experimente.

  3. 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:

  1. No documento do Swagger, clique no endpoint GET /api/v1/books/{id}.

  2. Insira o ID de um livro no campo id. Você pode usar um dos IDs de livros que foram inseridos no banco de dados.

  3. 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:

  1. No documento do Swagger, clique no endpoint POST /api/v1/books.

  2. Clique no botão Tentar.

  3. Insira os detalhes do livro no corpo da solicitação.

  4. 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:

  1. No documento do Swagger, clique no endpoint PUT /api/v1/books/{id}.

  2. Insira o ID de um livro no campo id. Você pode usar o ID de um dos livros que acabamos de adicionar.

  3. 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:

  1. No documento do Swagger, clique no endpoint DELETE /api/v1/books/{id}.
  2. 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.

  3. 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: