.NET 6中引入的Minimal APIs是一个令人兴奋的功能,旨在彻底改变您创建API的方式。
想象一下,用最少的代码和零样板代码构建强大的API——不再需要与控制器、路由或中间件纠缠。这就是Minimal APIs允许您实现的功能。这些API的理念是简化开发过程,使其变得极为简单和高效。
在本文中,我们将深入探讨.NET 8中的Minimal APIs世界,并指导您创建一个完全功能的书店API。您将学习如何获取所有图书,通过ID检索图书,添加新图书,甚至删除图书。让我们开始吧。
目录
先决条件
在开始之前,请确保您的计算机上安装了以下先决条件:
-
Visual Studio Code或者您喜欢的其他代码编辑器
或者,您可以使用内置支持.NET 8的Visual Studio 2022。但在本文中,我们将使用Visual Studio Code。它轻量级、易于使用,并且跨平台。
我们将使用Swagger UI来测试我们的API。Swagger UI是一个强大的工具,允许您直接从浏览器与API交互。它提供了一个用户友好的界面来测试您的API端点,使测试和调试API变得更容易。
当您创建一个新项目时,它将自动安装必要的包并配置项目以使用Swagger UI。.NET 8默认包含Swagger UI,因此无论您是在Visual Studio中还是使用.NET创建应用程序,Swagger UI都将为您配置。
运行您的应用程序,Swagger UI将自动在您的浏览器中打开 – 但由于我们在使用VS Code,我们需要在终端上点击端口号。
您可以在GitHub上找到此项目的源代码。
简介:极简 API
想象一下在一个具有众多端点的代码库中工作,使其变得相当庞大和复杂。传统上,在ASP.NET Core中构建 API 需要使用控制器、路由、中间件和大量样板代码。但在 ASP.NET Core 中构建 API 有两种方法:传统方式和极简方式。
传统方式对大多数开发人员来说很熟悉,涉及控制器和大量基础设施代码。而极简方式是在.NET 6
中引入的,允许您使用最少的代码和零样板创建 API。这种方法简化了开发过程,使您能够专注于编写业务逻辑,而不是处理基础设施代码。
极简 API 轻量、快速,非常适合构建小到中型的 API。它们非常适合用于原型设计、构建微服务或创建不需要太多复杂性的简单 API。在本手册中,我们将探索 .NET 6 中极简 API 的世界,并学习如何从头开始创建一个完全功能的书店 API。
如何创建极简 API
使用dotnet CLI
创建极简 API 很简单,因为默认模板已经是极简 API。但如果您使用 Visual Studio,则需要删除项目模板中附带的样板代码。
让我们首先使用dotnet CLI
创建一个最小的API项目。
dotnet new webapi -n BookStoreApi
dotnet new webapi
命令将创建一个名为BookStoreApi
的新最小API项目。该项目包含了开始所需的文件和文件夹。
让我们来探索项目结构:
-
Program.cs
:应用程序的入口点,其中配置了主机。 -
bookapi-minimal.sln
:包含项目的解决方案文件。 -
bookapi-minimal.http
:包含用于测试API的示例HTTP请求的文件。 -
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,您可以使用它来测试您的设置。它生成一系列天气预报,并在您对 /weatherforecast
端点发出 GET
请求时返回它们。此外,代码包括 Swagger UI,帮助您测试 API。
特别注意 app.MapGet
方法,它将路由映射到处理程序函数。在这种情况下,它将 /weatherforecast
路由映射到一个返回天气预报列表的函数。我们将使用类似的方法在下一节中创建我们自己的端点。
在我们开始创建项目文件夹结构之前,让我们了解基于控制器和简化 API 中的 HTTP 方法。
基于控制器和简化 API 中的 HTTP 方法
在基于控制器的方法中,这是创建 Web API 的传统方式,您需要创建一个控制器类并为每个 HTTP 方法定义方法。例如:
-
要创建一个
GET
方法,您需要使用[HttpGet]
属性。 -
要创建一个
POST
方法,您需要使用[HttpPost]
属性。 -
要创建一个
PUT
方法,您需要使用[HttpPut]
属性。 -
要创建一个
DELETE
方法,您需要使用[HttpDelete]
属性。
这就是在基于控制器的方法中创建端点的方式。
相比之下,Minimal API 使用诸如app.MapGet
、app.MapPost
、app.MapPut
和app.MapDelete
这样的方法来创建端点。这是这两种方法之间的主要区别:基于控制器的 API 使用属性来定义端点,而 Minimal API 使用方法。
现在您已经了解了如何在基于控制器和 Minimal API 中处理 HTTP 请求,让我们来创建项目文件夹结构。
在我们创建项目文件夹结构之前,让我们先运行我们已有的内容。正如我们之前学习的那样,当你使用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的默认响应。
你应该会看到类似这样的内容:
现在我们接下来要做的事情是找到一种方法来组织我们的项目,并创建必要的文件和文件夹,以便开始。
Minimal 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作为数据库提供程序。我们使用GetConnectionString
方法从appsettings.json
文件中检索连接字符串。 -
我们使用
AddValidatorsFromAssembly
方法从当前assembly
中添加validators
。该方法扫描当前程序集以查找实现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": "*"
}
恭喜!您已成功为应用程序创建了数据库上下文、扩展方法和连接字符串。在下一节中,我们将创建一个Contract。
如何创建合同
合同是数据传输对象(DTO),用于定义客户端和服务器之间交换的数据结构。在我们的应用程序中,我们将创建合同来表示通过API端点发送和接收的数据。
以下是我们将要创建的合同:
-
CreateBookRequest:表示创建新书时发送的数据。
-
UpdateBookRequest:表示更新现有书籍时发送的数据。
-
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
的新文件,并添加以下代码:
// 合同/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; }
}
}
在合同
文件夹中,创建一个名为BookResponse
的新文件,并添加以下代码:
// 合同/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; }
}
}
在合同
文件夹中,创建一个名为ErrorResponse
的新文件,并添加以下代码:
// 合同/ErrorResponse.cs
namespace bookapi_minimal.Contracts
{
public record ErrorResponse
{
public string Title { get; set; }
public int StatusCode { get; set; }
public string Message { get; set; }
}
}
在合同
文件夹中,创建一个名为ApiResponse
的新文件,并添加以下代码:
// 合同/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
类中为每个方法实现业务逻辑。
让我们在BookService
类中为创建、读取、更新和删除操作创建逻辑。
如何实现AddBookAsync
方法
正如我之前提到的,我们将使用AddBookAsync
方法向数据库中添加新书。在这个方法中,我们将创建一个新的书实体,将来自CreateBookRequest
对象的数据映射到书实体,并将书实体保存到数据库。我们还将书实体作为BookResponse
对象返回。
按照以下方式更新BookService
类中的AddBookAsync
方法:
// 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
对象。
按照以下方式更新BookService
类中的GetBookByIdAsync
方法:
// Services/BookService.cs
//...
/// <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;
}
}
//...
在这段代码中,我们正在查询具有指定ID的书籍的数据库,将书籍实体映射到BookResponse
对象,并返回BookResponse
对象。我们还在使用ILogger
依赖项记录信息和错误。
如果找不到具有指定ID的书籍,我们会记录警告消息并返回null。如果在过程中发生异常,我们会记录错误消息并重新抛出异常。
现在我们已经实现了GetBookByIdAsync
方法,让我们实现GetBooksAsync
方法。
如何实现GetBooksAsync
方法
GetBooksAsync
方法用于从数据库中检索所有图书。在这个方法中,我们将查询所有图书的数据库,将每本书实体映射到一个 BookResponse
对象,并返回一个 BookResponse
对象列表。
更新 BookService
类中的 GetBooksAsync
方法如下:
// 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
对象返回。
按以下方式更新BookService
类中的UpdateBookAsync
方法:
// 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 的书籍,从数据库中删除书籍实体,并返回一个布尔值,指示书籍是否成功删除。
按如下方式更新 BookService
类中的 DeleteBookAsync
方法:
// 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。如果在过程中发生异常,我们将记录错误信息并重新抛出异常。
现在您已成功在BookService
类中实现了AddBookAsync
、GetBookByIdAsync
、GetBooksAsync
、UpdateBookAsync
和DeleteBookAsync
方法的业务逻辑。这些方法处理图书的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
类注册为一个作用域服务。这意味着该服务将在每个请求中创建一次,并在请求完成后被销毁。
现在我们已经让服务正常工作,让我们继续创建异常类。
创建异常的方法
正确处理异常对于确保应用程序的稳定性和可靠性至关重要。在ASP.NET Core的上下文中,有两种主要类型的异常:
-
系统异常:这些是由.NET运行时或底层系统抛出的异常。
-
应用程序异常:这些是应用程序代码抛出的异常,用于处理特定错误或条件。
在带有.NET 8的ASP.NET Core中,引入了一个名为全局异常处理的新功能。该功能允许您在应用程序中全局处理异常,使得更容易管理错误并提供一致的用户体验。
在我们的应用程序中,我们将创建自定义异常类来处理特定的错误和条件。我们还将利用全局异常处理功能来全局管理异常,确保在整个应用程序中采用统一的错误处理方法。
我们将创建以下异常类:
-
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
属性设置响应状态码。 -
我们使用
httpContext.Response.WriteAsJsonAsync
方法将错误响应写成JSON格式。 -
我们返回
true
来指示异常已成功处理。
现在我们已经创建了异常类,让我们在依赖注入容器中注册GlobalExceptionHandler
。由于我们为在依赖注入容器中注册服务创建了一个扩展方法,我们将GlobalExceptionHandler
添加到ServiceExtensions
类中。
按照以下方式更新Extensions
文件夹中的ServiceExtensions
类:
// Extensions/ServiceExtensions.cs
//...
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
//...
AddExceptionHandler
方法在依赖注入容器中注册GlobalExceptionHandler
。AddProblemDetails
方法在依赖注入容器中注册ProblemDetails
类。
现在我们已经在依赖注入容器中注册了GlobalExceptionHandler
,我们可以使用它来全局处理应用程序中的异常。在下一节中,我们将创建用于与书籍数据交互的API端点。
如何创建API端点
在ASP.NET Core的最小API环境中,有许多设置端点的方法。
您可以直接在Program.cs
文件中定义它们。但随着项目的增长以及需要添加更多端点或功能,更好地组织代码是有帮助的。一个实现这一目标的方法是创建一个单独的类来处理所有端点。
正如我们上面讨论的那样,最小API不像传统的ASP.NET Core应用程序那样使用控制器或视图。相反,它们使用MapGet
、MapPost
、MapPut
和MapDelete
等方法来定义API端点的HTTP方法和路由。
要开始,请转到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
对象用于定义 API 端点的 HTTP 方法和路由。 在接下来的部分,我们将定义用于 创建
、阅读
、更新
和 删除
书籍的 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
对象包含创建新书所需的数据。 -
响应模型: 该端点作为JSON负载返回一个
Book
对象。Book
对象包含新创建书的数据。 -
返回值: 该端点返回一个
Created
结果。Created
结果包含新创建书的位置和Book
对象。
如何创建GetBookAsync
书籍端点
在本节中,我们将创建GetBookAsync
端点。 此端点将接受书籍ID作为查询参数,并返回具有指定ID的书籍。 我们将使用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
对象包含创建新书籍所需的数据。 -
响应模型: 端点以JSON负载形式返回一个
Book
对象。Book
对象包含新创建书籍的数据。 -
返回值: 端点返回一个
Ok
结果。Ok
结果包含Book
对象。
如何创建GetBookByIdAsync
书籍端点
在本节中,我们将创建GetBookByIdAsync
端点。该端点将接受书籍ID作为路由参数,并返回指定ID的书籍。我们将使用MapGet
方法来定义此端点的HTTP方法和路由。
将以下代码添加到BookEndpoints
类中:
// 端点/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
端点。该端点将接受书籍 ID 作为路由参数,以及一个 Book
对象作为 JSON 负载,并更新具有指定 ID 的书籍。我们将使用 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
端点。该端点将接受书籍ID作为路由参数,并删除指定ID的书籍。我们将使用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生成器,用于为API创建Swagger文档。我们在Swagger文档中指定了API的标题、版本和描述。
-
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
文件中注册了书籍端点,我们可以运行应用程序并使用 Swagger 测试 API 端点。
当您运行该应用程序时,您应该在以下URL看到Swagger文档:https://localhost:5001/swagger/index.html
。Swagger文档提供关于API端点、请求和响应模型的信息,并允许您直接从浏览器测试端点。您应该看到类似于以下内容:
恭喜!您已实现了书籍服务的业务逻辑,创建了自定义异常,定义了API端点,并在Program.cs
文件中注册了端点。您还启用了Swagger文档以测试API端点。
如何向数据库添加种子数据
另一个重要步骤是在应用程序启动时向数据库添加初始数据。这些种子数据将填充数据库,使您能够在不手动添加数据的情况下测试API端点。
在执行迁移和测试API端点之前,让我们先添加一些种子数据。
为实现这一目的,我们将在配置文件夹中创建一个名为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
实体提供了配置。
-
配置方法:此方法用于配置
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端点列表,请求和响应模型,并能够直接从浏览器测试端点。
如果您的端口号与5001
不同,不用担心 – 它仍会起作用。端口号可能会根据您使用的设备类型而变化,但仍会达到相同的结果。
如何测试获取所有书籍
端点
要测试获取所有书籍
端点,请按照以下步骤操作:
-
在Swagger文档中,点击
GET /api/v1/books
端点。 -
点击
试用
按钮。 -
点击
执行
按钮。
这将向API发送请求以检索数据库中的所有书籍。
您应该看到API的响应,其中包括在数据库中种植的书籍列表。
下面的图片显示了API的响应:
如何测试按ID获取书籍
端点
要测试按ID获取书籍
端点,请按照以下步骤操作:
-
在Swagger文档中,点击
GET /api/v1/books/{id}
端点。 -
在
id
字段中输入一本书的ID。您可以使用数据库中预设的书籍ID之一。 -
点击
试一试
按钮。
这将向API发送一个请求,以检索具有指定ID的书籍。您应该看到来自API的响应,其中将包含具有指定ID的书籍。
下面的图片显示了来自API的响应:
如何测试添加书籍
端点
要测试添加书籍
端点,请按照以下步骤操作:
-
在Swagger文档中,点击
POST /api/v1/books
端点。 -
点击
试一试
按钮。 -
在请求体中输入书籍详细信息。
-
单击
执行
按钮。
这将向API发送一个请求,以将新书添加到数据库中。
您应该会看到来自API的响应,其中将包含新创建的书籍。
下面的图片显示了来自API的响应:
如何测试更新书籍
端点
要测试更新书籍
端点,请按照以下步骤进行:
-
在Swagger文档中,单击
PUT /api/v1/books/{id}
端点。 -
在
id
字段中输入一本书的ID。您可以使用刚刚添加的书籍中的任意一个ID。 -
单击
试一下
按钮。
这将向API发送一个请求,以更新具有指定ID的书籍。
您应该会看到来自API的响应,其中将包含更新后的书籍。
下面的图片显示了来自API的响应:
如何测试删除书籍
端点
要测试删除书籍
端点,请按照以下步骤进行:
-
在Swagger文档中,单击
DELETE /api/v1/books/{id}
端点。 -
在
id
字段中输入一本书的ID。您可以使用我们刚刚添加的书籍中的任何ID或种子数据中的ID。 -
点击
Try it out
按钮。
这将向API发送请求以删除指定ID的书籍。
下面的图片显示了来自API的响应:
恭喜!您已经实现了书籍的所有CRUD操作,并使用Swagger测试了API端点,验证了它们的正常工作。您现在可以在此基础上添加更多功能和特性到您的API中。
结论
本手册探讨了如何在ASP.NET Core中使用.NET 8创建一个最小化API。我们构建了一个全面的书籍API,支持CRUD操作,实施了自定义异常,定义并注册了API端点,并启用了Swagger文档以便于测试。
按照本教程,您已经为构建ASP.NET Core最小化API打下了坚实的基础。您现在可以应用这些知识,为各种领域和行业创建强大的API。
我希望您发现这个教程既有帮助又有信息量。感谢您的阅读!
欢迎在社交媒体上与我联系:
Source:
https://www.freecodecamp.org/news/create-a-minimal-api-in-net-core-handbook/