.NET 6中引入的Minimal APIs是一个令人兴奋的功能,旨在彻底改变您创建API的方式。

想象一下,用最少的代码和零样板代码构建强大的API——不再需要与控制器、路由或中间件纠缠。这就是Minimal APIs允许您实现的功能。这些API的理念是简化开发过程,使其变得极为简单和高效。

在本文中,我们将深入探讨.NET 8中的Minimal APIs世界,并指导您创建一个完全功能的书店API。您将学习如何获取所有图书,通过ID检索图书,添加新图书,甚至删除图书。让我们开始吧。

目录

先决条件

在开始之前,请确保您的计算机上安装了以下先决条件:

或者,您可以使用内置支持.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.MapGetapp.MapPostapp.MapPutapp.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类定义了表示书籍细节的属性,例如titleauthordescriptioncategorylanguagetotal pages。每个属性旨在保存有关书籍的特定信息,从而便于在我们的应用程序中管理和操作书籍数据。

现在我们已经创建了模型,让我们创建我们的数据库上下文。

如何创建数据库上下文

数据库上下文是表示与数据库的会话的类。它负责与数据库交互并执行数据库操作。在我们的应用程序中,我们将使用 Entity Framework Core 与我们的数据库交互。

安装所需的包

在创建我们的数据库上下文之前,我们需要安装以下包:

您可以使用以下命令安装这些包:

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的类,该类继承自DbContextDbContext类是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类中。我们需要将ApplicationContextILogger依赖项注入到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类中实现了AddBookAsyncGetBookByIdAsyncGetBooksAsyncUpdateBookAsyncDeleteBookAsync方法的业务逻辑。这些方法处理图书的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;
            }
        }

    }
}

恭喜!您已成功为AddBookAsyncGetBookByIdAsyncGetBooksAsyncUpdateBookAsyncDeleteBookAsync方法实现了业务逻辑,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 方法用于异步处理异常。此方法接受 HttpContextExceptionCancellationToken 作为参数。

  • 我们使用 ILogger 依赖项记录异常详细信息。

  • 我们创建一个 ErrorResponse 对象来表示 API 返回的错误响应。 ErrorResponse 对象包含错误消息、标题和状态码。

  • 我们根据异常类型确定状态码。如果异常是 BadHttpRequestException,我们将状态码设置为 BadRequest。如果异常是 NoBookFoundExceptionBookDoesNotExistException,我们将状态码设置为 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方法在依赖注入容器中注册GlobalExceptionHandlerAddProblemDetails方法在依赖注入容器中注册ProblemDetails类。

现在我们已经在依赖注入容器中注册了GlobalExceptionHandler,我们可以使用它来全局处理应用程序中的异常。在下一节中,我们将创建用于与书籍数据交互的API端点。

如何创建API端点

在ASP.NET Core的最小API环境中,有许多设置端点的方法。

您可以直接在Program.cs文件中定义它们。但随着项目的增长以及需要添加更多端点或功能,更好地组织代码是有帮助的。一个实现这一目标的方法是创建一个单独的类来处理所有端点。

正如我们上面讨论的那样,最小API不像传统的ASP.NET Core应用程序那样使用控制器或视图。相反,它们使用MapGetMapPostMapPutMapDelete等方法来定义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不同,不用担心 – 它仍会起作用。端口号可能会根据您使用的设备类型而变化,但仍会达到相同的结果。

如何测试获取所有书籍端点

要测试获取所有书籍端点,请按照以下步骤操作:

  1. 在Swagger文档中,点击GET /api/v1/books端点。

  2. 点击试用按钮。

  3. 点击执行按钮。

这将向API发送请求以检索数据库中的所有书籍。

您应该看到API的响应,其中包括在数据库中种植的书籍列表。

下面的图片显示了API的响应:

如何测试按ID获取书籍端点

要测试按ID获取书籍端点,请按照以下步骤操作:

  1. 在Swagger文档中,点击GET /api/v1/books/{id}端点。

  2. id字段中输入一本书的ID。您可以使用数据库中预设的书籍ID之一。

  3. 点击试一试按钮。

这将向API发送一个请求,以检索具有指定ID的书籍。您应该看到来自API的响应,其中将包含具有指定ID的书籍。

下面的图片显示了来自API的响应:

如何测试添加书籍端点

要测试添加书籍端点,请按照以下步骤操作:

  1. 在Swagger文档中,点击POST /api/v1/books端点。

  2. 点击试一试按钮。

  3. 在请求体中输入书籍详细信息。

  4. 单击执行按钮。

这将向API发送一个请求,以将新书添加到数据库中。

您应该会看到来自API的响应,其中将包含新创建的书籍。

下面的图片显示了来自API的响应:

如何测试更新书籍端点

要测试更新书籍端点,请按照以下步骤进行:

  1. 在Swagger文档中,单击PUT /api/v1/books/{id}端点。

  2. id字段中输入一本书的ID。您可以使用刚刚添加的书籍中的任意一个ID。

  3. 单击试一下按钮。

这将向API发送一个请求,以更新具有指定ID的书籍。

您应该会看到来自API的响应,其中将包含更新后的书籍。

下面的图片显示了来自API的响应:

如何测试删除书籍端点

要测试删除书籍端点,请按照以下步骤进行:

  1. 在Swagger文档中,单击DELETE /api/v1/books/{id}端点。

  2. id字段中输入一本书的ID。您可以使用我们刚刚添加的书籍中的任何ID或种子数据中的ID。

  3. 点击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。

我希望您发现这个教程既有帮助又有信息量。感谢您的阅读!

欢迎在社交媒体上与我联系: