Минимальные API — это захватывающая функция, представленная в .NET 6, предназначенная для революционизации процесса создания API.
Представьте себе создание надежных API с минимальным кодом и без лишней рутины — больше не нужно бороться с контроллерами, маршрутизацией или промежуточным ПО. Именно это позволяют делать минимальные API. Идея этих API заключается в упрощении процесса разработки, делая его невероятно легким и эффективным.
В этой статье мы погрузимся в мир минимальных API в .NET 8 и проведем вас через создание полностью функционального API для книжного магазина. Вы узнаете, как получить все книги, извлечь книгу по ее ID, добавлять новые книги и даже удалять книги. Давайте начнем.
Оглавление
Предварительные требования
Прежде чем мы начнем, убедитесь, что у вас установлены следующие предварительные требования на вашем компьютере:
-
Visual Studio Code или любой другой редактор кода на ваш выбор
-
C# Dev Kit для Visual Studio Code
Также вы можете использовать Visual Studio 2022, который поставляется с встроенной поддержкой .NET 8. Но в этой статье мы будем использовать Visual Studio Code. Он легковесный, прост в использовании и кроссплатформенный.
Мы будем использовать Swagger UI для тестирования нашего API. Swagger UI — это мощный инструмент, который позволяет взаимодействовать с вашим API direttamente из браузера. Он предоставляет удобный интерфейс для тестирования конечных точек API, что упрощает тестирование и отладку вашего API.
Когда вы создаете новый проект, он автоматически установит необходимые пакеты и настроит проект для использования Swagger UI. .NET 8 включает Swagger UI по умолчанию, поэтому независимо от того, создаете ли вы приложение в Visual Studio или с помощью .NET, Swagger UI будет настроен для вас.
Запустите ваше приложение, и Swagger UI автоматически откроется в вашем браузере — но поскольку мы используем VS Code, нам нужно нажать на номер порта в нашем терминале.
Вы можете найти исходный код для этого проекта на GitHub.
Введение в минимальные API
Представьте, что вы работаете в кодовой базе с множеством конечных точек, что делает её довольно большой и сложной. Традиционно создание API в ASP.NET Core включает использование контроллеров, маршрутизации, middleware и значительного количества шаблонного кода. Но есть два подхода к созданию API в ASP.NET Core: традиционный и минимальный.
Традиционный способ знаком большинству разработчиков и включает контроллеры и обширный инфраструктурный код. Минимальный способ, представленный в .NET 6
, позволяет создавать API с минимальным количеством кода и без шаблонного кода. Этот подход упрощает процесс разработки, позволяя сосредоточиться на написании бизнес-логики, а не на работе с инфраструктурным кодом.
Минимальные API легковесны, быстры и идеально подходят для создания небольших и средних API. Они идеальны для прототипирования, построения микросервисов или создания простых API, которые не требуют большой сложности. В этом руководстве мы исследуем мир минимальных API в .NET 6 и научимся создавать полностью функциональный API книжного магазина с нуля.
Как создать минимальный API
Создание минимального API является простым при использовании dotnet CLI
, так как шаблон по умолчанию уже является минимальным API. Но если вы используете Visual Studio, вам нужно будет удалить шаблонный код, который приходит с шаблоном проекта.
Давайте начнем с использования dotnet CLI
для создания проекта минимального API.
dotnet new webapi -n BookStoreApi
Команда dotnet new webapi
создает новый проект минимального API под названием BookStoreApi
. Этот проект содержит необходимые файлы и папки, чтобы вы могли начать.
Давайте рассмотрим структуру проекта:
-
Program.cs
: Точка входа в приложение, где конфигурируется хост. -
bookapi-minimal.sln
: Файл решения, который содержит проект. -
bookapi-minimal.http
: Файл, который содержит примеры HTTP-запросов для тестирования API. -
bookapi-minimal.csproj
: Файл проекта, который содержит конфигурацию проекта. -
appsettings.json
: Файл конфигурации, который хранит настройки приложения. -
appsettings.Development.json
: Файл конфигурации для среды разработки.
Когда вы открываете файл program.cs, вы заметите, что код минимален. Файл Program.cs
содержит следующий код:
var builder = WebApplication.CreateBuilder(args);
// Добавьте сервисы в контейнер.
// Узнайте больше о настройке Swagger/OpenAPI на https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Настройте конвейер 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);
}
Если вы еще полностью не понимаете код, не переживайте — мы рассмотрим его подробно в следующих разделах. Основной вывод заключается в том, что минимальные API требуют очень мало кода, что является одним из их главных преимуществ.
Стандартный код настраивает простой API прогноза погоды, который вы можете использовать для проверки своей настройки. Он генерирует список прогнозов погоды и возвращает их при выполнении GET
запроса к конечной точке /weatherforecast
. Также код включает Swagger UI для тестирования API.
Обратите особое внимание на метод app.MapGet
, который сопоставляет маршрут с функцией обработчика. В этом случае он сопоставляет маршрут /weatherforecast
с функцией, которая возвращает список прогнозов погоды. Мы будем использовать подобные методы для создания своих конечных точек в следующих разделах.
Прежде чем мы начнем создавать структуру папок нашего проекта, давайте разберемся с методами HTTP в обоих подходах: на основе контроллеров и минимальных API.
Методы HTTP в подходах на основе контроллеров и минимальных API
В подходе на основе контроллеров, который является традиционным способом создания веб-API, вам нужно создать класс контроллера и определить методы для каждого HTTP-метода. Например:
-
Чтобы создать метод
GET
, вы используете атрибут[HttpGet]
. -
Чтобы создать метод
POST
, вы используете атрибут[HttpPost]
. -
Чтобы создать метод
PUT
, вы используете атрибут[HttpPut]
. -
Чтобы создать метод
DELETE
, вы используете атрибут[HttpDelete]
.
Таким образом создаются конечные точки в подходе на основе контроллеров.
В отличие от этого, Minimal APIs используют методы, такие как app.MapGet
, app.MapPost
, app.MapPut
и app.MapDelete
, для создания конечных точек. Это главное различие между двумя подходами: в контроллер-ориентированных API для определения конечных точек используются атрибуты, а в Minimal API — методы.
Теперь, когда вы понимаете, как обрабатывать HTTP-запросы как в контроллер-ориентированных, так и в Minimal API, давайте создадим структуру папок для нашего проекта.
Прежде чем создать структуру папок нашего проекта, давайте сначала запустим то, что у нас есть. Как мы узнали ранее, при создании проекта с помощью Visual Studio или .NET CLI, он поставляется с проектом WeatherForecast по умолчанию, который мы можем запустить и увидеть в UI. Давайте запустим его, чтобы убедиться, что все работает, прежде чем перейти к созданию структуры папок нашего проекта.
Выполните эту команду:
dotnet run
Вы должны увидеть следующий вывод:
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
Это означает, что приложение запущено и слушает на http://localhost:5228
. Как я упомянул выше, поскольку мы используем dotnet CLI
и Visual Studio Code, приложение не откроет браузер автоматически. Нам нужно сделать это вручную.
Откройте ваш браузер и перейдите по адресу http://localhost:5228/swagger/index.html
, чтобы увидеть ответ по умолчанию от API.
Вы должны увидеть что-то вроде этого:
Теперь следующее, что нам нужно сделать, это найти способ структурировать наш проект и создать необходимые файлы и папки, чтобы начать работу.
Минимальные файлы проекта API
Чтобы организовать наш проект, мы создадим структурированную иерархию папок. Это поможет нам поддерживать код чистым и управляемым. Вот структура папок, которую мы будем использовать:
-
AppContext: Содержит контекст базы данных и связанные конфигурации.
-
Конфигурации: Содержит конфигурации Entity Framework Core и начальные данные для базы данных.
-
Контракты: Содержит объекты передачи данных (DTO), используемые в нашем приложении.
-
Конечные точки: Здесь мы определяем и настраиваем конечные точки нашего минимального API.
-
Исключения: Содержит пользовательские классы исключений, используемые в проекте.
-
Расширения: Содержит методы расширения, которые мы будем использовать на протяжении всего проекта.
-
Модели: Содержит модели бизнес-логики.
-
Сервисы: Содержит классы сервисов, реализующие бизнес-логику.
-
Интерфейсы: Содержит определения интерфейсов, используемых для сопоставления наших сервисов.
В Visual Studio Code вы можете создать эту структуру папок следующим образом:
- AppContext
- Configurations
- Contracts
- Endpoints
- Exceptions
- Extensions
- Models
- Services
- Interfaces
После настройки структура папок вашего проекта должна выглядеть так:
Теперь, когда наша структура проекта настроена, мы можем приступить к написанию кода. Давайте начнем с создания наших моделей.
Как создать модели
В этом разделе мы создадим модели для нашего приложения. Модели являются строительными блоками нашего приложения, представляющими данные, с которыми будет работать наше приложение. Для нашего примера мы создадим модель для книги.
Для начала создайте папку с именем Models
в директории вашего проекта. Внутри этой папки создайте файл с именем BookModel.cs
и добавьте следующий код:
// 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; }
}
}
Этот класс BookModel
определяет свойства, представляющие детали книги, такие как её title
, author
, description
, category
, language
и total pages
. Каждое свойство предназначено для хранения конкретной информации о книге, что упрощает управление и манипулирование данными о книгах в нашем приложении.
Теперь, когда мы создали нашу модель, давайте создадим наш контекст базы данных.
Как создать контекст базы данных
Контекст базы данных — это класс, представляющий сессию с базой данных. Он отвечает за взаимодействие с базой данных и выполнение операций с базой данных. В нашем приложении мы будем использовать Entity Framework Core для взаимодействия с нашей базой данных.
Установите необходимые пакеты
Перед созданием нашего контекста базы данных нам нужно установить следующие пакеты:
-
Microsoft.EntityFrameworkCore
-
Microsoft.EntityFrameworkCore.SqlServer
-
FluentValidation.DependencyInjectionExtensions
Вы можете установить эти пакеты, используя следующие команды:
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
Проверка установки пакетов
Чтобы убедиться, что пакеты установлены, откройте файл bookapi-minimal.csproj
в корневом каталоге вашего проекта. Вы должны увидеть установленные пакеты, перечисленные следующим образом:
<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>
Это подтверждает, что пакеты были успешно установлены.
Теперь давайте создадим наш контекст базы данных.
В папке AppContext создайте новый файл с именем ApplicationContext.cs
и добавьте следующий код:
// AppContext/ApplicationContext.cs
using bookapi_minimal.Models;
using Microsoft.EntityFrameworkCore;
namespace bookapi_minimal.AppContext
{
public class ApplicationContext(DbContextOptions<ApplicationContext> options) : DbContext(options)
{
// Основная схема для контекста базы данных
private const string DefaultSchema = "bookapi";
// DbSet для представления коллекции книг в нашей базе данных
public DbSet<BookModel> Books { get; set; }
// Конструктор для настройки контекста базы данных
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.HasDefaultSchema(DefaultSchema);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationContext).Assembly);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationContext).Assembly);
}
}
}
Разберем код выше:
-
Мы определяем класс с именем
ApplicationContext
, который наследует отDbContext
. КлассDbContext
является частью Entity Framework Core и представляет сессию с базой данных. -
Конструктор принимает экземпляр
DbContextOptions<ApplicationContext>
. Этот конструктор используется для настройки параметров контекста базы данных. -
Мы определяем свойство с именем
Books
типаDbSet<BookModel>
. Это свойство представляет коллекцию книг в нашей базе данных. -
Мы переопределяем метод
OnModelCreating
для настройки схемы базы данных и применения всех конфигураций, определенных в нашем приложении.
Теперь, когда мы создали наш контекст базы данных, давайте создадим наш метод расширения и зарегистрируем наш контекст базы данных в контейнере внедрения зависимостей.
Создание метода расширения
Прежде чем создать метод расширения, давайте разберемся, что такое метод расширения в контексте ASP.NET Core.
Метод расширения — это статический метод, который добавляет новую функциональность к существующему типу без изменения исходного типа. В ASP.NET Core методы расширения часто используются для расширения функциональности интерфейса IServiceCollection
, который используется для регистрации сервисов в контейнере внедрения зависимостей.
Сервисы — это компоненты, которые предоставляют функциональность приложению, такие как доступ к базе данных, ведение журнала и конфигурация. Создавая метод расширения для интерфейса IServiceCollection
, вы можете упростить процесс регистрации ваших сервисов в контейнере внедрения зависимостей.
Вместо того чтобы класть всё в файл Program.cs
, мы создадим метод расширения для регистрации наших сервисов в контейнере внедрения зависимостей. Это поможет нам поддерживать наш код чистым и организованным.
В папке Extensions
создайте новый файл с именем ServiceExtensions.cs
и добавьте следующий код:
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));
// Добавление контекста базы данных
builder.Services.AddDbContext<ApplicationContext>(configure =>
{
configure.UseSqlServer(builder.Configuration.GetConnectionString("sqlConnection"));
});
// Добавление валидаторов из текущей сборки
builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
}
}
}
Разберём код выше:
-
Мы определяем статический класс с именем
ServiceExtensions
, который содержит метод расширения с именемAddApplicationServices
. Этот метод расширяет интерфейсIHostApplicationBuilder
, который используется для настройки конвейера обработки запросов приложения. -
Метод
AddApplicationServices
принимает экземплярIHostApplicationBuilder
в качестве параметра. Этот параметр используется для доступа к конфигурации и сервисам приложения. -
Мы добавляем
ApplicationContext
в контейнер внедрения зависимостей и настраиваем его для использования SQL Server в качестве поставщика базы данных. Мы получаем строку подключения из файлаappsettings.json
с помощью методаGetConnectionString
. -
Мы добавляем
validators
из текущегоassembly
с помощью методаAddValidatorsFromAssembly
. Этот метод сканирует текущую сборку на наличие классов, реализующих интерфейс IValidator, и регистрирует их в контейнере внедрения зависимостей.
Далее нам нужно добавить строку подключения в файл appsettings.json
. Добавьте следующий код в ваш файл appsettings.json
:
{
"ConnectionStrings": {
"sqlConnection": "Server=localhost\\SQLEXPRESS02;Database=BookAPIMinimalAPI;Integrated Security=true;TrustServerCertificate=true;"
}
}
Не забудьте заменить your_password
на ваш реальный пароль SQL Server.
Ваш файл appsettings.json
должен выглядеть так:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"sqlConnection": "Server=localhost\\SQLEXPRESS02;Database=BookAPIMinimalAPI;Integrated Security=true;TrustServerCertificate=true;"
},
"AllowedHosts": "*"
}
Поздравляем! Вы успешно создали контекст базы данных, метод расширения и строку подключения для вашего приложения. В следующем разделе мы создадим Контракт.
Как создать контракт
Контракты являются объектами передачи данных (DTO), которые определяют структуру данных, обмениваемых между клиентом и сервером. В нашем приложении мы будем создавать контракты для представления данных, отправляемых и получаемых нашими конечными точками API.
Вот контракты, которые мы собираемся создать:
-
CreateBookRequest: Это представляет данные, отправляемые при создании новой книги.
-
UpdateBookRequest: tHI Представляет данные, отправляемые при обновлении существующей книги.
-
BookResponse: Представляет данные, возвращаемые при получении книги.
-
ErrorResponse: Представляет ответ об ошибке, возвращаемый при возникновении исключения.
-
ApiResponse: Представляет ответ, возвращаемый API.
В папке Contracts
создайте новый файл с именем CreateBookRequest
и добавьте следующий код:
// 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; }
}
}
В папке Contracts
создайте новый файл с именем UpdateBookRequest
и добавьте следующий код:
// Contracts/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; }
}
}
В папке Contracts
создайте новый файл с именем BookResponse
и добавьте следующий код:
// Contracts/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; }
}
}
В папке Contracts
создайте новый файл с именем ErrorResponse
и добавьте следующий код:
// Contracts/ErrorResponse.cs
namespace bookapi_minimal.Contracts
{
public record ErrorResponse
{
public string Title { get; set; }
public int StatusCode { get; set; }
public string Message { get; set; }
}
}
В папке Contracts
создайте новый файл с именем ApiResponse
и добавьте следующий код:
// Contracts/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;
}
}
}
Эти контракты помогают нам определить структуру данных, обмениваемых между клиентом и сервером, что упрощает работу с данными в нашем приложении.
В следующем разделе мы создадим службы для реализации бизнес-логики нашего приложения.
Как добавить службы
Службы — это компоненты, предоставляющие функциональность приложению. В нашем приложении мы создадим службы для реализации бизнес-логики. Мы создадим службы для обработки CRUD-операций для книг, проверки данных книг и обработки исключений.
В ASP.NET Core службы регистрируются в контейнере внедрения зависимостей и могут быть внедрены в другие компоненты, такие как контроллеры и конечные точки, но это минимальный API, поэтому мы будем внедрять службы непосредственно в конечные точки.
Создадим интерфейс для наших сервисов. В папке Interfaces
создайте новый файл с именем IBookService.cs
и добавьте следующий код:
// 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);
}
}
Разберем код выше: Мы определили интерфейс с именем IBookService
, который содержит методы для обработки операций CRUD для книг. Интерфейс определяет следующие методы:
-
AddBookAsync
: Добавляет новую книгу в базу данных. -
GetBookByIdAsync
: Получает книгу по её ID. -
GetBooksAsync
: Получает все книги из базы данных. -
UpdateBookAsync
: Обновляет существующую книгу.
Мы используем Контракт, который создали ранее в папке Contracts
. Интерфейс IBookService
определяет структуру методов, которые будут реализованы классами сервиса. Это помогает нам отделить интерфейс от реализации, что упрощает поддержку и тестирование нашего кода.
Теперь, когда мы создали интерфейс, создадим класс сервиса, который реализует этот интерфейс.
Как реализовать сервис книг
Этот сервис реализует интерфейс IBookService
и предоставит бизнес-логику для нашего приложения. В папке Services
создайте новый файл с именем BookService.cs
. Ваш начальный файл должен выглядеть так:
// Services/BookService.cs
namespace bookapi_minimal.Services
{
public class BookService
{
}
}
Первое, что нам нужно сделать, это добавить интерфейс к классу BookService
. Обновите класс BookService
для реализации интерфейса IBookService
следующим образом:
// Services/BookService.cs
using bookapi_minimal.Interfaces;
namespace bookapi_minimal.Services
{
public class BookService:IBookService
{
}
}
Когда вы это сделаете, ваш VS Code может показать ошибку, потому что мы не реализовали методы в интерфейсе. Давайте перейдем к реализации методов в классе BookService
.
В VS Code вы можете использовать сочетание клавиш Ctrl + .
для реализации методов в интерфейсе. Тогда вы увидите следующий код, сгенерированный для вас:
using bookapi_minimal.Contracts;
using bookapi_minimal.Interfaces;
namespace bookapi_minimal.Services
{
// Сервисный класс для управления книгами
public class BookService : IBookService
{
// Метод для добавления новой книги в базу данных
public Task<BookResponse> AddBookAsync(CreateBookRequest createBookRequest)
{
throw new NotImplementedException();
}
// Метод для удаления книги из базы данных
public Task<bool> DeleteBookAsync(Guid id)
{
throw new NotImplementedException();
}
// Метод для получения книги из базы данных по её ID
public Task<BookResponse> GetBookByIdAsync(Guid id)
{
throw new NotImplementedException();
}
// Метод для получения всех книг из базы данных
public Task<IEnumerable<BookResponse>> GetBooksAsync()
{
throw new NotImplementedException();
}
// Метод для обновления книги в базе данных
public Task<BookResponse> UpdateBookAsync(Guid id, UpdateBookRequest updateBookRequest)
{
throw new NotImplementedException();
}
}
}
Теперь вы можете видеть, что методы в интерфейсе были реализованы в классе BookService
. Мы реализуем бизнес-логику для каждого метода в следующем разделе.
Прежде чем это сделать, добавим необходимые зависимости в класс BookService
. Нам нужно внедрить зависимости ApplicationContext
и ILogger
в класс BookService
. ApplicationContext
используется для взаимодействия с базой данных, а ILogger
— для ведения журнала.
Чтобы внедрить зависимости, обновите класс BookService
следующим образом:
// Services/BookService.cs
// ...
private readonly ApplicationContext _context; // Контекст базы данных
private readonly ILogger<BookService> _logger; // Логгер для записи информации и ошибок
//..
Поскольку мы добавили зависимости, нам нужно обновить конструктор BookService
, чтобы он принимал зависимости. Обновите конструктор BookService
следующим образом:
// Services/BookService.cs
// ...
// Конструктор для инициализации контекста базы данных и логгера
public BookService(ApplicationContext context, ILogger<BookService> logger)
{
_context = context;
_logger = logger;
}
// ...
Теперь, когда мы добавили зависимости и обновили конструктор, мы можем реализовать бизнес-логику для каждого метода в классе BookService
.
Создадим логику для операций CREATE, READ, UPDATE и DELETE в классе BookService
.
Как реализовать метод AddBookAsync
Как я уже упоминал ранее, мы будем использовать метод AddBookAsync
для добавления новой книги в базу данных. В этом методе мы создадим новую сущность книги, смаппим данные из объекта CreateBookRequest
в сущность книги и сохраним сущность книги в базу данных. Мы также вернем сущность книги в виде объекта BookResponse
.
Обновите метод AddBookAsync
в классе BookService
следующим образом:
// Services/BookService.cs
// ...
/// <summary>
/// Добавить новую книгу
/// </summary>
/// <param name="createBookRequest">Запрос на добавление книги</param>
/// <returns>Детали созданной книги</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
};
// Добавить книгу в базу данных
_context.Books.Add(book);
await _context.SaveChangesAsync();
_logger.LogInformation("Book added successfully.");
// Вернуть детали созданной книги
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;
}
}
// ...
В этом коде мы создаем новую сущность книги из объекта CreateBookRequest
, отображаем данные из объекта CreateBookRequest
на сущность книги, сохраняем сущность книги в базу данных и возвращаем сущность книги в виде объекта BookResponse
.
Мы также ведем логирование информации и ошибок с помощью зависимости ILogger
. Если в процессе возникает исключение, мы логируем сообщение об ошибке и повторно выбрасываем исключение.
Теперь, когда мы реализовали метод AddBookAsync
, давайте реализуем метод GetBookByIdAsync
.
Как реализовать метод GetBookByIdAsync
Метод GetBookByIdAsync
используется для извлечения книги по ее ID из базы данных. В этом методе мы будем запрашивать базу данных на наличие книги с указанным ID, отображать сущность книги на объект BookResponse
и возвращать объект BookResponse
.
Обновите метод GetBookByIdAsync
в классе BookService
следующим образом:
// Services/BookService.cs
//...
/// <summary>
/// Получить книгу по ее идентификатору
/// </summary>
/// <param name="id">Идентификатор книги</param>
/// <returns>Детали книги</returns>
public async Task<BookResponse> GetBookByIdAsync(Guid id)
{
try
{
// Найти книгу по ее идентификатору
var book = await _context.Books.FindAsync(id);
if (book == null)
{
_logger.LogWarning($"Book with ID {id} not found.");
return null;
}
// Вернуть детали книги
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;
}
}
//...
В этом коде мы запрашиваем данные из базы данных для книги с указанным идентификатором, отображаем сущность книги в объект BookResponse
и возвращаем объект BookResponse
. Мы также регистрируем информацию и ошибки, используя зависимость ILogger
.
Если книга с указанным идентификатором не найдена, мы регистрируем предупреждающее сообщение и возвращаем null. Если происходит исключение в процессе, мы регистрируем сообщение об ошибке и повторно генерируем исключение.
Теперь, когда мы реализовали метод GetBookByIdAsync
, давайте реализуем метод GetBooksAsync
.
Как реализовать метод GetBooksAsync
Метод GetBooksAsync
используется для извлечения всех книг из базы данных. В этом методе мы будем запрашивать базу данных на предмет всех книг, сопоставлять каждую сущность книги с объектом BookResponse
и возвращать список объектов BookResponse
.
Обновите метод GetBooksAsync
в классе BookService
следующим образом:
// Services/BookService.cs
//...
/// <summary>
/// Получить все книги
/// </summary>
/// <returns>Список всех книг</returns>
public async Task<IEnumerable<BookResponse>> GetBooksAsync()
{
try
{
// Получить все книги из базы данных
var books = await _context.Books.ToListAsync();
// Вернуть детали всех книг
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;
}
}
//...
Здесь мы запрашиваем базу данных на предмет всех книг, сопоставляем каждую сущность книги с объектом BookResponse
и возвращаем список объектов BookResponse
. Мы также ведем журнал информации и ошибок с помощью зависимости ILogger
. Если в процессе возникает исключение, мы записываем сообщение об ошибке и повторно выбрасываем исключение.
Теперь, когда мы реализовали метод GetBooksAsync
, давайте реализуем метод UpdateBookAsync
.
Как реализовать метод UpdateBookAsync
Метод UpdateBookAsync
используется для обновления существующей книги в базе данных. В этом методе мы будем запрашивать базу данных на наличие книги с указанным ID, обновлять сущность книги данными из объекта UpdateBookRequest
, сохранять обновленную сущность книги в базу данных и возвращать обновленную сущность книги в виде объекта BookResponse
.
Обновите метод UpdateBookAsync
в классе BookService
следующим образом:
// Services/BookService.cs
//...
/// <summary>
/// Обновить существующую книгу
/// </summary>
/// <param name="id">ID книги, которую нужно обновить</param>
/// <param name="book">Обновленная модель книги</param>
/// <returns>Детали обновленной книги</returns>
public async Task<BookResponse> UpdateBookAsync(Guid id, UpdateBookRequest book)
{
try
{
// Найти существующую книгу по ее ID
var existingBook = await _context.Books.FindAsync(id);
if (existingBook == null)
{
_logger.LogWarning($"Book with ID {id} not found.");
return null;
}
// Обновить детали книги
existingBook.Title = book.Title;
existingBook.Author = book.Author;
existingBook.Description = book.Description;
existingBook.Category = book.Category;
existingBook.Language = book.Language;
existingBook.TotalPages = book.TotalPages;
// Сохранить изменения в базу данных
await _context.SaveChangesAsync();
_logger.LogInformation("Book updated successfully.");
// Вернуть детали обновленной книги
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;
}
}
//...
Вот, мы запрашиваем базу данных на книгу с указанным ID, обновляем сущность книги данными из объекта UpdateBookRequest
, сохраняем обновленную сущность книги в базу данных и возвращаем обновленную сущность книги в виде объекта BookResponse
. Мы также логируем информацию и ошибки с помощью зависимости ILogger
.
Если книга с указанным ID не найдена, мы логируем предупреждающее сообщение и возвращаем null. Если в процессе возникает исключение, мы логируем сообщение об ошибке и повторно выбрасываем исключение.
Теперь, когда мы реализовали метод UpdateBookAsync
, давайте реализуем метод DeleteBookAsync
.
Как реализовать метод DeleteBookAsync
Метод DeleteBookAsync
используется для удаления существующей книги из базы данных. В этом методе мы будем запрашивать базу данных на книгу с указанным ID, удалять сущность книги из базы данных и возвращать булево значение, указывающее, была ли книга успешно удалена.
Обновите метод DeleteBookAsync
в классе BookService
следующим образом:
// Services/BookService.cs
//...
/// <summary>
/// Удалить книгу по её ID
/// </summary>
/// <param name="id">ID книги, которую нужно удалить</param>
/// <returns>True, если книга была удалена, иначе false</returns>
public async Task<bool> DeleteBookAsync(Guid id)
{
try
{
// Найти книгу по её ID
var book = await _context.Books.FindAsync(id);
if (book == null)
{
_logger.LogWarning($"Book with ID {id} not found.");
return false;
}
// Удалить книгу из базы данных
_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;
}
}
//...
В этом коде мы запрашиваем базу данных на наличие книги с указанным ID, удаляем сущность книги из базы данных и возвращаем булево значение, указывающее, была ли книга успешно удалена. Мы также логируем информацию и ошибки с помощью зависимости ILogger
.
Если книга с указанным ID не найдена, мы логируем предупреждающее сообщение и возвращаем false. Если в процессе возникает исключение, мы логируем сообщение об ошибке и повторно выбрасываем исключение.
Теперь вы успешно реализовали бизнес-логику для методов AddBookAsync
, GetBookByIdAsync
, GetBooksAsync
, UpdateBookAsync
и DeleteBookAsync
в классе BookService
. Эти методы обрабатывают операции CRUD для книг, проверяют данные книги и обрабатывают исключения. На данный момент ваш класс BookService
должен выглядеть следующим образом:
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; // Контекст базы данных
private readonly ILogger<BookService> _logger; // Логгер для записи информации и ошибок
// Конструктор для инициализации контекста базы данных и логгера
public BookService(ApplicationContext context, ILogger<BookService> logger)
{
_context = context;
_logger = logger;
}
/// Добавить новую книгу
/// </summary>
/// <param name="createBookRequest">Запрос на добавление книги</param>
/// <returns>Детали добавленной книги</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
};
// Добавить книгу в базу данных
_context.Books.Add(book);
await _context.SaveChangesAsync();
_logger.LogInformation("Book added successfully.");
// Вернуть детали добавленной книги
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>
/// Получить книгу по её ID
/// </summary>
/// <param name="id">ID книги</param>
/// <returns>Детали книги</returns>
public async Task<BookResponse> GetBookByIdAsync(Guid id)
{
try
{
// Найти книгу по её ID
var book = await _context.Books.FindAsync(id);
if (book == null)
{
_logger.LogWarning($"Book with ID {id} not found.");
return null;
}
// Вернуть детали книги
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>
/// Получить все книги
/// </summary>
/// <returns>Список всех книг</returns>
public async Task<IEnumerable<BookResponse>> GetBooksAsync()
{
try
{
// Получить все книги из базы данных
var books = await _context.Books.ToListAsync();
// Вернуть детали всех книг
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>
/// Обновить существующую книгу
/// </summary>
/// <param name="id">ID книги для обновления</param>
/// <param name="book">Обновлённая модель книги</param>
/// <returns>Детали обновлённой книги</returns>
public async Task<BookResponse> UpdateBookAsync(Guid id, UpdateBookRequest book)
{
try
{
// Найти существующую книгу по её ID
var existingBook = await _context.Books.FindAsync(id);
if (existingBook == null)
{
_logger.LogWarning($"Book with ID {id} not found.");
return null;
}
// Обновить детали книги
existingBook.Title = book.Title;
existingBook.Author = book.Author;
existingBook.Description = book.Description;
existingBook.Category = book.Category;
existingBook.Language = book.Language;
existingBook.TotalPages = book.TotalPages;
// Сохранить изменения в базу данных
await _context.SaveChangesAsync();
_logger.LogInformation("Book updated successfully.");
// Вернуть детали обновлённой книги
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>
/// Удалить книгу по её ID
/// </summary>
/// <param name="id">ID книги для удаления</param>
/// <returns>True, если книга была удалена, иначе false</returns>
public async Task<bool> DeleteBookAsync(Guid id)
{
try
{
// Найти книгу по её ID
var book = await _context.Books.FindAsync(id);
if (book == null)
{
_logger.LogWarning($"Book with ID {id} not found.");
return false;
}
// Удалить книгу из базы данных
_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;
}
}
}
}
Поздравляем! Вы успешно реализовали бизнес-логику для методов AddBookAsync
, GetBookByIdAsync
, GetBooksAsync
, UpdateBookAsync
и DeleteBookAsync
в классе BookService
.
Есть одна вещь, которую нам нужно сделать: нам нужно зарегистрировать сервис в нашем методе расширения. Давайте сделаем это.
В вашем файле ServiceExtensions.cs
добавьте следующий код:
// Extensions/ServiceExtensions.cs
//..
builder.Services.AddScoped<IBookService, BookService>();
//...
Это зарегистрирует класс BookService
как scoped-сервис. Это означает, что сервис будет создан один раз на запрос и уничтожен после завершения запроса.
Теперь, когда у нас работает сервис, давайте создадим классы исключений.
Как создать исключения
Правильная обработка исключений критически важна для обеспечения стабильности и надежности приложения. В контексте ASP.NET Core существует два основных типа исключений:
-
Системные исключения: Это исключения, выбрасываемые средой выполнения .NET или базовой системой.
-
Исключения приложения: Это исключения, выбрасываемые кодом приложения для обработки специфических ошибок или условий.
В ASP.NET Core с .NET 8 была введена новая функция под названием глобальная обработка исключений. Эта функция позволяет вам обрабатывать исключения глобально в вашем приложении, что упрощает управление ошибками и обеспечивает единообразный пользовательский опыт.
В нашем приложении мы создадим пользовательские классы исключений для обработки специфических ошибок и условий. Мы также воспользуемся функцией глобальной обработки исключений для управления исключениями глобально, обеспечивая единообразный подход к обработке ошибок по всему приложению.
Мы собираемся создать следующие классы исключений:
-
NoBookFoundException
: Выбрасывается, когда книга с указанным ID не найдена. -
BookDoesNotExistException
: Выбрасывается, когда книга с указанным ID не существует. -
GlobalExceptionHandler
: Обрабатывает исключения глобально в приложении.
В папке Exceptions
создайте новый файл с именем NoBookFoundException.cs
и добавьте следующий код:
// Exceptions/NoBookFoundException.cs
namespace bookapi_minimal.Exceptions
{
public class NoBookFoundException : Exception
{
public NoBookFoundException() : base("No books found")
{}
}
}
В этом коде мы создаем пользовательский класс исключения с именем NoBookFoundException
, который наследуется от класса Exception
. Класс NoBookFoundException
используется для обработки сценария, когда в базе данных не найдено ни одной книги. Мы также предоставляем пользовательское сообщение об ошибке для исключения.
В папке Exceptions
создайте новый файл с именем BookDoesNotExistException.cs
и добавьте следующий код:
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;
}
}
}
В этом коде мы создаем пользовательский класс исключения с именем BookDoesNotExistException
, который наследуется от класса Exception
. Класс BookDoesNotExistException
используется для обработки сценария, когда книга с указанным ID не существует в базе данных. Мы также предоставляем пользовательское сообщение об ошибке для исключения.
В папке Exceptions
создайте новый файл с именем GlobalExceptionHandler.cs
и добавьте следующий код:
// Исключения/GlobalExceptionHandler.cs
using System.Net;
using bookapi_minimal.Contracts;
using Microsoft.AspNetCore.Diagnostics;
namespace bookapi_minimal.Exceptions
{
// Класс глобального обработчика исключений, реализующий IExceptionHandler
public class GlobalExceptionHandler : IExceptionHandler
{
private readonly ILogger<GlobalExceptionHandler> _logger;
// Конструктор для инициализации логгера
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
{
_logger = logger;
}
// Метод для асинхронной обработки исключений
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
// Логирование деталей исключения
_logger.LogError(exception, "An error occurred while processing your request");
var errorResponse = new ErrorResponse
{
Message = exception.Message,
Title = exception.GetType().Name
};
// Определение кода состояния на основе типа исключения
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;
}
// Установка кода состояния ответа
httpContext.Response.StatusCode = errorResponse.StatusCode;
// Запись ответа об ошибке в формате JSON
await httpContext.Response.WriteAsJsonAsync(errorResponse, cancellationToken);
// Возврат true для указания, что исключение было обработано
return true;
}
}
}
Разберем код выше:
-
Мы определяем класс с именем
GlobalExceptionHandler
, который реализует интерфейсIExceptionHandler
. ИнтерфейсIExceptionHandler
используется для глобальной обработки исключений в приложении. -
Класс
GlobalExceptionHandler
содержит конструктор, который инициализирует зависимостьILogger<GlobalExceptionHandler>
.ILogger
используется для логирования информации и ошибок. -
Метод
TryHandleAsync
используется для асинхронной обработки исключений. Этот метод принимаетHttpContext
,Exception
иCancellationToken
в качестве параметров. -
Мы логируем детали исключения с помощью зависимости
ILogger
. -
Мы создаем объект
ErrorResponse
для представления ответа об ошибке, возвращаемого API. ОбъектErrorResponse
содержит сообщение об ошибке, заголовок и код состояния. -
Мы определяем код состояния на основе типа исключения. Если исключение является
BadHttpRequestException
, мы устанавливаем код состояния вBadRequest
. Если исключение являетсяNoBookFoundException
илиBookDoesNotExistException
, мы устанавливаем код состояния вNotFound
. В противном случае мы устанавливаем код состояния вInternalServerError
. -
Мы устанавливаем код состояния ответа с помощью свойства
httpContext.Response.StatusCode
. -
Мы записываем ответ об ошибке в формате JSON с помощью метода
httpContext.Response.WriteAsJsonAsync
. -
Мы возвращаем
true
, чтобы указать, что исключение было успешно обработано.
Теперь, когда мы создали классы исключений, давайте зарегистрируем GlobalExceptionHandler
в контейнере внедрения зависимостей. Поскольку мы создали метод расширения для регистрации служб в контейнере внедрения зависимостей, мы добавим GlobalExceptionHandler
в класс ServiceExtensions
.
Обновите класс ServiceExtensions
в папке Extensions
следующим образом:
// Extensions/ServiceExtensions.cs
//...
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
//...
Метод AddExceptionHandler
регистрирует GlobalExceptionHandler
в контейнере внедрения зависимостей. Метод AddProblemDetails
регистрирует класс ProblemDetails
в контейнере внедрения зависимостей.
Теперь, когда мы зарегистрировали GlobalExceptionHandler
в контейнере внедрения зависимостей, мы можем использовать его для глобального обработки исключений в нашем приложении. В следующем разделе мы создадим конечные точки API для взаимодействия с данными книги.
Как создать конечные точки API
В контексте минимальных API в ASP.NET Core существует множество способов настройки конечных точек.
Вы можете определить их непосредственно в файле Program.cs
. Но по мере роста вашего проекта и необходимости добавления большего количества конечных точек или функциональности, полезно лучше организовать ваш код. Один из способов достижения этого — создание отдельного класса для обработки всех конечных точек.
Как мы обсуждали выше, минимальные API не используют контроллеры или представления, как традиционные приложения ASP.NET Core. Вместо этого они используют методы, такие как MapGet
, MapPost
, MapPut
и MapDelete
, для определения HTTP-методов и маршрутов для конечных точек API.
Для начала перейдите в папку Endpoints
и создайте новый файл с именем BookEndpoints.cs
. Добавьте следующий код в файл:
// Endpoints/BookEndpoints.cs
namespace bookapi_minimal.Endpoints
{
public static class BookEndPoint
{
public static IEndpointRouteBuilder MapBookEndPoint(this IEndpointRouteBuilder app)
{
return app;
}
}
}
Класс BookEndpoints
содержит метод MapBookEndPoint
, который возвращает объект IEndpointRouteBuilder
. Объект IEndpointRouteBuilder
используется для определения HTTP-методов и маршрутов для конечных точек API. В следующих разделах мы определим конечные точки API для создания
, чтения
, обновления
и удаления
книг.
Как создать конечную точку AddBookAsync
для книг
В этом разделе мы создадим конечную точку AddBookAsync
. Эта конечная точка будет принимать объект Book
в качестве JSON-загрузки и добавлять его в базу данных. Мы будем использовать метод MapPost
для определения HTTP-метода и маршрута для этой конечной точки.
Добавьте следующий код в класс BookEndpoints
:
// Endpoints/BookEndpoints.cs
//...
// Конечная точка для добавления новой книги
app.MapPost("/books", async (CreateBookRequest createBookRequest, IBookService bookService) =>
{
var result = await bookService.AddBookAsync(createBookRequest);
return Results.Created($"/books/{result.Id}", result);
});
//...
-
Определение маршрута: Метод MapPost определяет маршрут для конечной точки как
/books
. -
Модель запроса: Конечная точка принимает объект
CreateBookRequest
в качестве JSON-загрузки. ОбъектCreateBookRequest
содержит данные, необходимые для создания новой книги. -
Модель ответа: Конечная точка возвращает объект
Book
в виде JSON-загрузки. ОбъектBook
содержит данные для вновь созданной книги. -
Возвращаемое значение: Конечная точка возвращает результат
Created
. РезультатCreated
содержит местоположение вновь созданной книги и объектBook
.
Как создать конечную точку GetBookAsync
для книги
В этом разделе мы создадим конечную точку GetBookAsync
. Эта конечная точка будет принимать идентификатор книги в качестве параметра запроса и возвращать книгу с указанным идентификатором. Мы будем использовать метод MapGet
для определения HTTP-метода и маршрута для этой конечной точки.
Добавьте следующий код в класс BookEndpoints
:
// Endpoints/BookEndpoints.cs
// ...
// Конечная точка для получения всех книг
app.MapGet("/books", async (IBookService bookService) =>
{
var result = await bookService.GetBooksAsync();
return Results.Ok(result);
});
//...
-
Определение маршрута: Метод MapGet определяет маршрут для конечной точки как
/books
. -
Модель запроса: Конечная точка принимает объект
Book
в качестве JSON-загрузки. ОбъектBook
содержит данные, необходимые для создания новой книги. -
Модель ответа: Конечная точка возвращает объект
Book
в качестве JSON-загрузки. ОбъектBook
содержит данные для вновь созданной книги. -
Возвращаемое значение: Конечная точка возвращает результат
Ok
. РезультатOk
содержит объектBook
.
Как создать конечную точку GetBookByIdAsync
для книги
В этом разделе мы создадим конечную точку GetBookByIdAsync
. Эта конечная точка будет принимать идентификатор книги в качестве параметра маршрута и возвращать книгу с указанным идентификатором. Мы будем использовать метод MapGet
для определения HTTP-метода и маршрута для этой конечной точки.
Добавьте следующий код в класс BookEndpoints
:
// Endpoints/BookEndpoints.cs
//...
// Конечная точка для получения книги по 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();
});
//...
-
Определение маршрута: Метод MapGet определяет маршрут для конечной точки как
/books/{id:guid}
. Параметр{id:guid}
указывает, что параметрid
должен быть GUID. -
Модель запроса: Конечная точка принимает объект
Book
в качестве JSON-загрузки. ОбъектBook
содержит данные, необходимые для создания новой книги. -
Модель ответа: Конечная точка возвращает объект
Book
в качестве JSON-загрузки. ОбъектBook
содержит данные для вновь созданной книги. -
Возвращаемое значение: Конечная точка возвращает результат
Ok
, если книга найдена. РезультатNotFound
возвращается, если книга не найдена.
Как создать конечную точку UpdateBookAsync
В этом разделе мы создадим конечную точку UpdateBookAsync
. Эта конечная точка будет принимать идентификатор книги в качестве параметра маршрута и объект Book
в виде JSON-загрузки и обновлять книгу с указанным идентификатором. Мы будем использовать метод MapPut
для определения HTTP-метода и маршрута для этой конечной точки.
Добавьте следующий код в класс BookEndpoints
:
// Endpoints/BookEndpoints.cs
//...
// Конечная точка для обновления книги по 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();
});
//...
-
Определение маршрута: Метод MapPut определяет маршрут для конечной точки как
/books/{id:guid}
. Параметр{id:guid}
указывает, что параметрid
должен быть GUID. -
Модель запроса: Конечная точка принимает объект
Book
в качестве полезной нагрузки JSON. ОбъектBook
содержит данные, необходимые для создания новой книги. -
Модель ответа: Конечная точка возвращает объект
Book
в качестве полезной нагрузки JSON. ОбъектBook
содержит данные для вновь созданной книги. -
Возвращаемое значение: Конечная точка возвращает результат
Ok
, если книга найдена. РезультатNotFound
возвращается, если книга не найдена.
Как создать конечную точку DeleteBookAsync
для книги
В этом разделе мы создадим конечную точку DeleteBookAsync
. Эта конечная точка будет принимать идентификатор книги в качестве параметра маршрута и удалять книгу с указанным идентификатором. Мы будем использовать метод MapDelete
для определения HTTP-метода и маршрута для этой конечной точки.
Добавьте следующий код в класс BookEndpoints
:
// Endpoints/BookEndpoints.cs
//...
// Конечная точка для удаления книги по ID
app.MapDelete("/books/{id:guid}", async (Guid id, IBookService bookService) =>
{
var result = await bookService.DeleteBookAsync(id);
return result ? Results.NoContent() : Results.NotFound();
});
//...
-
Определение маршрута: Метод MapDelete определяет маршрут для конечной точки как
/books/{id:guid}
. Параметр{id:guid}
указывает, что параметрid
должен быть GUID. -
Модель запроса: Конечная точка принимает объект
Book
в качестве JSON-загрузки. ОбъектBook
содержит данные, необходимые для создания новой книги. -
Модель ответа: Конечная точка возвращает объект
Book
в качестве JSON-загрузки. ОбъектBook
содержит данные для вновь созданной книги. -
Возвращаемое значение: Конечная точка возвращает результат
NoContent
, если книга успешно удалена. РезультатNotFound
возвращается, если книга не найдена.
Теперь мы определили все методы для конечных точек книги. Поэтому ваш класс конечных точек должен выглядеть так:
// 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)
{
// Определение конечных точек
// Конечная точка для добавления новой книги
app.MapPost("/books", async (CreateBookRequest createBookRequest, IBookService bookService) =>
{
var result = await bookService.AddBookAsync(createBookRequest);
return Results.Created($"/books/{result.Id}", result);
});
// Конечная точка для получения всех книг
app.MapGet("/books", async (IBookService bookService) =>
{
var result = await bookService.GetBooksAsync();
return Results.Ok(result);
});
// Конечная точка для получения книги по 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();
});
// Конечная точка для обновления книги по 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();
});
// Конечная точка для удаления книги по 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;
}
}
}
Поздравляем! Вы создали все конечные точки для API книги. Конечные точки обрабатывают операции CRUD для книг и возвращают соответствующие ответы в зависимости от запроса и данных.
Как зарегистрировать конечные точки
После определения конечных точек API для книжного API следующим шагом является регистрация этих конечных точек в файле Program.cs
. Мы будем использовать метод MapBookEndpoints
для регистрации конечных точек книги.
Также следует очистить наш класс Program.cs
, чтобы обеспечить его организованность и поддерживаемость.
// 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" });
// Установить путь для комментариев к Swagger JSON и UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
var app = builder.Build();
// Настроить конвейер HTTP-запросов.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseExceptionHandler();
app.MapGroup("/api/v1/")
.WithTags(" Book endpoints")
.MapBookEndPoint();
app.Run();
Разберем ключевые компоненты файла Program.cs
:
-
AddApplicationServices: Этот метод регистрирует необходимые сервисы для API. Это расширение метода, которое мы создали ранее для добавления сервисов в контейнер внедрения зависимостей.
-
AddSwaggerGen: Этот метод регистрирует генератор Swagger, который используется для создания документации Swagger для API. Мы указываем заголовок, версию и описание API в документе Swagger.
-
MapGroup: Этот метод группирует конечные точки. Он принимает путь в качестве параметра и возвращает объект
IEndpointRouteBuilder
. Мы используем методWithTags
для добавления тегов к конечным точкам и методMapBookEndpoints
для регистрации конечных точек книги. -
Run: Этот метод запускает приложение.
Для включения документации Swagger необходимо добавить свойство GenerateDocumentationFile
в ваш файл .csproj
. В этом примере файл называется bookapi-minimal.csproj
, но имя может варьироваться в зависимости от вашего проекта.
Добавьте следующую строку в ваш файл .csproj
:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
В конце концов, файл bookapi-minimal.csproj должен выглядеть так:
<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>
Теперь, когда мы зарегистрировали конечные точки книги в файле Program.cs
, мы можем запустить приложение и протестировать конечные точки API с помощью Swagger.
Когда вы запускаете приложение, вы должны увидеть документацию Swagger по следующему URL: https://localhost:5001/swagger/index.html
. Документация Swagger предоставляет информацию об оконечных точках API, моделях запросов и ответов, а также позволяет тестировать оконечные точки непосредственно из браузера. Вы должны увидеть что-то вроде этого:
Поздравляем! Вы реализовали бизнес-логику для сервиса книг, создали пользовательские исключения, определили оконечные точки API и зарегистрировали их в файле Program.cs
. Вы также включили документацию Swagger для тестирования оконечных точек API.
Как добавить начальные данные в базу данных
Еще один важный шаг — это заполнение базы данных начальными данными при запуске приложения. Эти начальные данные заполнят базу данных, позволяя вам тестировать оконечные точки API без ручного добавления данных.
Давайте добавим некоторые начальные данные перед выполнением миграций и тестированием наших оконечных точек API.
Для этого мы создадим новый класс в нашей папке Configuration под названием BookTypeConfigurations
и добавим следующий код:
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)
{
// Настроить имя таблицы
builder.ToTable("Books");
// Настроить первичный ключ
builder.HasKey(x => x.Id);
// Настроить свойства
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();
// Начальные данные
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
}
);
}
}
}
Давайте разберем код выше:
В Entity Framework Core вы можете использовать интерфейс IEntityTypeConfiguration
для настройки типа сущности и начальных данных для базы данных. Класс BookTypeConfigurations
реализует интерфейс IEntityTypeConfiguration<BookModel>
и предоставляет конфигурацию для сущности BookModel
.
-
Метод Configure: Этот метод используется для настройки типа сущности
BookModel
. Он определяет имя таблицы, первичный ключ и свойства для сущностиBookModel
.-
Имя таблицы: Метод
ToTable
указывает имя таблицы, которая будет создана в базе данных. В данном случае имя таблицы установлено как “Books”. -
Первичный ключ: Метод
HasKey
указывает первичный ключ для сущностиBookModel
. Первичный ключ установлен на свойствоId
. -
Свойства: Метод
Property
настраивает свойства сущностиBookModel
. Он указывает тип данных, длину и ограничения для каждого свойства.
-
-
Начальные данные: Метод
HasData
заполняет базу данных начальными данными. Он создает три объектаBookModel
с образцами данных для тестирования конечных точек API.
Теперь, когда мы создали класс BookTypeConfigurations
, нам нужно зарегистрировать эту конфигурацию в классе ApplicationContext
. Это гарантирует, что конфигурация будет применена при создании или миграции базы данных.
Мы, наконец, почти готовы к тестированию нашего API. Но перед этим нам нужно выполнить миграции для создания базы данных и применения начальных данных.
Помните, что мы добавили строку подключения к базе данных в файле appsettings.json
? Теперь давайте выполним миграцию и позже обновим нашу базу данных, чтобы миграция вступила в силу.
Как выполнить миграцию
Миграции позволяют обновлять схему базы данных на основе изменений, внесенных в ваши классы моделей. В Entity Framework Core вы можете использовать команду dotnet ef migrations add
для создания новой миграции, отражающей эти изменения.
Чтобы выполнить миграцию, запустите следующую команду в терминале:
dotnet ef migrations add InitialCreate
Если команда выполнена успешно, вы должны увидеть вывод, подобный этому:
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
Теперь вы увидите новую папку под названием Migrations
в вашем проекте. Эта папка содержит файлы миграций, созданные на основе изменений, внесенных в ваши классы моделей. Эти файлы миграций включают SQL-команды, необходимые для обновления схемы базы данных.
Как обновить базу данных
После создания миграции вам нужно применить её для обновления схемы базы данных. Вы можете использовать команду dotnet ef database update
для применения миграции и обновления базы данных. Убедитесь, что SQL Server запущен.
Запустите следующую команду в терминале:
dotnet ef database update
Это обновит схему базы данных на основе изменений, внесенных в ваши классы моделей. Убедитесь, что в вашей строке подключения к базе данных нет ошибок.
Как протестировать конечные точки API
Теперь мы можем протестировать наши конечные точки с помощью Swagger. Для этого запустите приложение, выполнив следующую команду в терминале:
dotnet run
Это запустит наше приложение. Вы можете открыть браузер и перейти по адресу https://localhost:5001/swagger/index.html
, чтобы получить доступ к документации Swagger. Вы должны увидеть список конечных точек API, моделей запросов и ответов, а также возможность тестирования конечных точек directamente из браузера.
Если ваш номер порта отличается от 5001
, не переживайте – это все равно сработает. Порт может изменяться в зависимости от типа используемой машины, но результат будет тем же.
Как протестировать конечную точку Get All Books
Чтобы протестировать конечную точку Get All Books
, выполните следующие шаги:
-
В документации Swagger нажмите на конечную точку
GET /api/v1/books
. -
Нажмите кнопку
Try it out
. -
Нажмите кнопку
Execute
.
Это отправит запрос к API для получения всех книг в базе данных.
Вы должны увидеть ответ от API, который будет включать список книг, которые были добавлены в базу данных.
На изображении ниже показан ответ от API:
Как протестировать конечную точку Get Book by ID
Чтобы протестировать конечную точку Get Book by ID
, выполните следующие шаги:
-
В документации Swagger нажмите на конечную точку
GET /api/v1/books/{id}
. -
Введите ID книги в поле
id
. Вы можете использовать один из ID книг, который был добавлен в базу данных. -
Нажмите кнопку
Try it out
.
Это отправит запрос к API для получения книги с указанным ID. Вы должны увидеть ответ от API, который будет включать книгу с указанным ID.
На изображении ниже показан ответ от API:
Как протестировать конечную точку Add Book
Чтобы протестировать конечную точку Add Book
, выполните следующие шаги:
-
В документации Swagger нажмите на конечную точку
POST /api/v1/books
. -
Нажмите кнопку
Try it out
. -
Введите детали книги в теле запроса.
-
Нажмите кнопку
Выполнить
.
Это отправит запрос к API для добавления новой книги в базу данных.
Вы должны увидеть ответ от API, который будет включать вновь созданную книгу.
На изображении ниже показан ответ от API:
Как протестировать конечную точку Обновить книгу
Чтобы протестировать конечную точку Обновить книгу
, выполните следующие шаги:
-
В документации Swagger нажмите на конечную точку
PUT /api/v1/books/{id}
. -
Введите ID книги в поле
id
. Вы можете использовать ID одной из книг, которые мы только что добавили. -
Нажмите кнопку
Попробовать
.
Это отправит запрос к API для обновления книги с указанным ID.
Вы должны увидеть ответ от API, который будет включать обновленную книгу.
На изображении ниже показан ответ от API:
Как протестировать конечную точку Удалить книгу
Чтобы протестировать конечную точку Удалить книгу
, выполните следующие шаги:
-
В документации Swagger нажмите на конечную точку
DELETE /api/v1/books/{id}
. -
Введите ID книги в поле
id
. Вы можете использовать любой из идентификаторов книг, которые мы только что добавили, или данные из начальной загрузки. -
Нажмите кнопку
Попробовать
.
Это отправит запрос в API для удаления книги с указанным ID.
На изображении ниже показан ответ от API:
Поздравляем! Вы реализовали все операции CRUD для книг и протестировали конечные точки API с помощью Swagger, убедившись, что они работают как ожидалось. Теперь вы можете на этом фундаменте добавить больше функций и возможностей в ваш API.
Заключение
В этом руководстве мы рассмотрели, как создать минимальный API в ASP.NET Core с использованием .NET 8. Мы построили комплексный API для книг, поддерживающий операции CRUD, реализовали пользовательские исключения, определили и зарегистрировали конечные точки API, а также включили документацию Swagger для удобного тестирования.
Следуя этому учебнику, вы получили solid основу для создания минимальных API с ASP.NET Core. Теперь вы можете применить эти знания и создать надежные API для различных доменов и отраслей.
Надеюсь, вы нашли этот учебник полезным и информативным. Спасибо за чтение!
Не стесняйтесь связываться со мной в социальных сетях:
Source:
https://www.freecodecamp.org/news/create-a-minimal-api-in-net-core-handbook/