.NET 6 中引入的 Minimal APIs 是一個令人興奮的功能,旨在革新您創建 API 的方式。

想像一下,使用最少的程式碼和零樣板代碼來建立強大的 API——不再需要與控制器、路由或中介軟體掙扎。這就是 minimal APIs 所能讓您做到的。這些 API 的理念是簡化開發過程,使其非常容易和高效。

在本文中,我們將深入探討.NET 8 中的 minimal APIs,並指導您如何創建一個完全功能的書店 API。您將學會如何獲取所有書籍、通過其 ID 檢索書籍、新增書籍,甚至刪除書籍。讓我們開始吧。

目錄

先決條件

在開始之前,請確保您的計算機上安裝了以下先決條件:

或者,您可以使用Visual Studio 2022,該版本內建支援.NET 8。但在這篇文章中,我們將使用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 路由映射到一個返回天氣預報清單的函數。我們將在接下來的章節中使用類似的方法來創建我們自己的端點。

在我們開始創建專案的資料夾結構之前,讓我們了解基於 Controller 和最小 API 的 HTTP 方法。

Controller 和最小 API 中的 HTTP 方法

在基於 Controller 的方法中,這是創建 Web API 的傳統方式,您需要創建一個控制器類別並為每個 HTTP 方法定義方法。例如:

  • 要創建一個GET方法,您需要使用[HttpGet]屬性。

  • 要創建一個POST方法,您需要使用[HttpPost]屬性。

  • 要創建一個PUT方法,您需要使用[HttpPut]屬性。

  • 要創建一個DELETE方法,您需要使用[HttpDelete]屬性。

這就是在基於控制器的方法中創建端點的方式。

相比之下,最小 API 使用方法如app.MapGetapp.MapPostapp.MapPutapp.MapDelete來創建端點。這就是兩種方法之間的主要區別:基於控制器的 API 使用屬性來定義端點,而最小 API 使用方法。

現在您了解了如何在基於控制器和最小 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類定義了代表一本書詳細信息的屬性,如標題作者描述類別語言總頁數。每個屬性都被設計來保存關於這本書的具體信息,使得在我們的應用程序內輕鬆管理和操作書籍數據。

現在我們已經創建了我們的模型,讓我們創建我們的數據庫上下文。

如何創建數據庫上下文

數據庫上下文是一個代表與數據庫的會話的類。它負責與數據庫交互並執行數據庫操作。在我們的應用程序中,我們將使用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;"
  }
  }

請確保用您實際的SQL Server密碼替換your_password

您的appsettings.json文件應該如下:


{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ConnectionStrings": {
    "sqlConnection": "Server=localhost\\SQLEXPRESS02;Database=BookAPIMinimalAPI;Integrated Security=true;TrustServerCertificate=true;"
  },
  "AllowedHosts": "*"
}

恭喜!您已成功為應用程序創建了數據庫上下文、擴展方法和連接字符串。在下一節中,我們將創建一個合約。

如何創建合約

合約是數據傳輸對象(DTO),定義了客戶端和服務器之間交換的數據結構。在我們的應用程序中,我們將創建合約來表示 API 端點發送和接收的數據。

這裡是我們將要創建的合約:

  • CreateBookRequest:這表示創建新書時發送的數據。

  • UpdateBookRequest:這表示更新現有書籍時發送的數據。

  • 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
{
     // Service class for managing books
   public class BookService : IBookService
   {
       // Method to add a new book to the database
       public Task<BookResponse> AddBookAsync(CreateBookRequest createBookRequest)
       {
           throw new NotImplementedException();
       }

      // Method to Delete a book from the database
       public Task<bool> DeleteBookAsync(Guid id)
       {
           throw new NotImplementedException();
       }

       // Method to Get a book from the database by its ID

       public Task<BookResponse> GetBookByIdAsync(Guid id)
       {
           throw new NotImplementedException();
       }

      // Method to Get all books from the database
       public Task<IEnumerable<BookResponse>> GetBooksAsync()
       {
           throw new NotImplementedException();
       }

       // Method to Update a book in the database
       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類別中為CREATE、READ、UPDATE和DELETE操作創建邏輯。

如何實現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方法:


// 服務/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 方法:

// 服務/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;
            }
        }

    }
}

恭喜你!你已成功在 BookService 類別中實現了 AddBookAsyncGetBookByIdAsyncGetBooksAsyncUpdateBookAsyncDeleteBookAsync 方法的業務邏輯。

有一件事我們需要做:我們需要在擴展方法中註冊服務。讓我們開始吧。

在你的 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 方法將 GlobalExceptionHandler 註冊到依賴注入容器中。 AddProblemDetails 方法將 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


//...
   // Endpoint to add a new book
      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 對象包含創建新書籍所需的數據。

  • 響應模型:該端點返回一個 Book 對象作為 JSON 載荷。 Book 對象包含新創建書籍的數據。

  • 返回值:該端點返回一個 Ok 結果。 Ok 結果包含 Book 對象。

如何創建 GetBookByIdAsync 書籍端點

在這個部分,我們將創建GetBookByIdAsync端點。該端點將接受書籍ID作為路由參數並返回具有指定ID的書籍。我們將使用MapGet方法來定義此端點的HTTP方法和路由。

將以下代碼添加到BookEndpoints類中:


// Endpoints/BookEndpoints.cs
//...
// 通過ID獲取書籍的端點

  app.MapGet("/books/{id:guid}", async (Guid id, IBookService bookService) =>
  {
    var result = await bookService.GetBookByIdAsync(id);
    return result != null ? Results.Ok(result) : Results.NotFound();
});

//...
  • 路由定義:MapGet方法定義了端點的路由為/books/{id:guid}。參數{id:guid}指定id參數應該是一個GUID。

  • 請求模型:該端點接受一個Book對象作為JSON載荷。 Book對象包含創建新書籍所需的數據。

  • 響應模型:該端點將一個Book對象作為JSON載荷返回。 Book對象包含了新創建書籍的數據。

  • 返回值:如果找到書籍,則該端點返回 Ok 結果。如果未找到書籍,則返回 NotFound 結果。

如何創建 UpdateBookAsync 書籍端點

在本節中,我們將創建 UpdateBookAsync 端點。此端點將接受一個書籍 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 結果。

現在我們已經定義了所有書籍端點的方法。因此,您的端點類應該如下所示:

// 端點/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端點之前,讓我們添加一些種子數據。

為此,我們將在Configuration文件夾中創建一個名為BookTypeConfigurations的新類,並添加以下代碼:



using bookapi_minimal.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace bookapi_minimal.Configurations
{
    public class BookTypeConfigurations : IEntityTypeConfiguration<BookModel>
    {
        public void Configure(EntityTypeBuilder<BookModel> builder)
        {
            // 配置表名
            builder.ToTable("Books");

            // 配置主鍵
            builder.HasKey(x => x.Id);

            // 配置屬性
            builder.Property(x => x.Id).ValueGeneratedOnAdd();
            builder.Property(x => x.Title).IsRequired().HasMaxLength(100);
            builder.Property(x => x.Author).IsRequired().HasMaxLength(100);
            builder.Property(x => x.Description).IsRequired().HasMaxLength(500);
            builder.Property(x => x.Category).IsRequired().HasMaxLength(100);
            builder.Property(x => x.Language).IsRequired().HasMaxLength(50);
            builder.Property(x => x.TotalPages).IsRequired();

            // 種子數據
            builder.HasData(
                new BookModel
                {
                    Id = Guid.NewGuid(),
                    Title = "The Alchemist",
                    Author = "Paulo Coelho",
                    Description = "The Alchemist follows the journey of an Andalusian shepherd",
                    Category = "Fiction",
                    Language = "English",
                    TotalPages = 208
                },
                new BookModel
                {
                    Id = Guid.NewGuid(),
                    Title = "To Kill a Mockingbird",
                    Author = "Harper Lee",
                    Description = "A novel about the serious issues of rape and racial inequality.",
                    Category = "Fiction",
                    Language = "English",
                    TotalPages = 281
                },
                new BookModel
                {
                    Id = Guid.NewGuid(),
                    Title = "1984",
                    Author = "George Orwell",
                    Description = "A dystopian social science fiction novel and cautionary tale about the dangers of totalitarianism. ",
                  Category = "Fiction",
                  Language = "English",
                  TotalPages = 328
                } 
            );
        }
    }
}

讓我們分解上面的代碼:

在 Entity Framework Core 中,你可以使用 IEntityTypeConfiguration 介面來配置實體類型和種子資料以儲存於資料庫中。 BookTypeConfigurations 類別實作了 IEntityTypeConfiguration<BookModel> 介面並為 BookModel 實體提供配置。

  • Configure 方法: 這個方法用於配置 BookModel 實體類型。它定義了 BookModel 實體的表名稱、主鍵和屬性。

    • 表名稱: ToTable 方法指定要在資料庫中創建的表名稱。在這個案例中,表名稱設定為 “Books”。

    • 主鍵: HasKey 方法指定了 BookModel 實體的主鍵。主鍵設定為 Id 屬性。

    • 屬性: Property 方法配置了 BookModel 實體的屬性。它為每個屬性指定了資料類型、長度和約束條件。

  • 種子數據HasData 方法用初始數據為資料庫填充。它創建了三個帶有測試API端點樣本數據的 BookModel 對象。

現在我們已經創建了 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。

  3. 點擊 Try it out 按鈕。

這將向 API 發送一個刪除指定 ID 書籍的請求。

下面的圖片顯示了 API 的回應:

恭喜!您已經實現了對書籍的所有 CRUD 操作,並使用 Swagger 測試了 API 端點,驗證它們按預期工作。您現在可以在此基礎上擴展,為您的 API 添加更多功能和功能。

結論

本手冊介紹了如何在 ASP.NET Core 中使用 .NET 8 創建一個最小的 API。我們建立了一個支持 CRUD 操作的全面書籍 API,實現了自定義異常,定義並註冊 API 端點,並啟用了 Swagger 文檔,以便輕鬆進行測試。

通過本教程,您已經掌握了使用 ASP.NET Core 創建最小 API 的堅實基礎。您現在可以應用這些知識,為各種領域和行業創建強大的 API。

希望您找到本教程既有幫助又具啟發性。感謝您的閱讀!

歡迎在社交媒體上與我聯繫: