學習.NET 8 MiniApis入門

tokengo發表於2024-07-02

介紹篇

什麼是MiniApis?

MiniApis的特點和優勢

MiniApis的應用場景

環境搭建

系統要求

安裝MiniApis

配置開發環境

基礎概念

MiniApis架構概述

關鍵術語解釋(如Endpoint、Handler等)

MiniApis與其他API框架的對比

第一個MiniApis專案

建立一個簡單的MiniApis專案

專案結構介紹

[編寫第一個API Endpoint](##編寫第一個API Endpoint)

執行和測試你的第一個API

路由和請求處理

定義和管理路由

[處理不同型別的HTTP請求(GET, POST, PUT, DELETE)](##處理不同型別的HTTP請求(GET, POST, PUT, DELETE))

路由引數和查詢引數的使用

資料處理

處理請求資料(路徑引數、查詢引數、請求體)

返回響應資料

使用中介軟體處理資料

錯誤處理

捕獲和處理錯誤

自定義錯誤響應

使用中介軟體進行錯誤處理

資料驗證和安全

[資料驗證方法](##資料驗證方法)

保護API安全的最佳實踐

身份驗證和授權

與資料庫互動

連線資料庫

執行基本的CRUD操作

使用ORM(物件關係對映)工具

高階特性

中介軟體的使用和編寫

檔案上傳和下載

實現API版本控制

測試和除錯

編寫單元測試和整合測試

使用除錯工具

效能最佳化技巧

部署MiniApis應用

部署到本地伺服器

[部署到雲平臺(如AWS, Azure, Heroku等)](##部署到雲平臺(如AWS, Azure, Heroku等))

持續整合和持續部署(CI/CD)

實踐專案

專案一:構建一個簡單的任務管理API

專案二:構建一個使用者認證系統

專案三:構建一個部落格API

常見問題和解決方案

常見錯誤及其解決方法

MiniApis開發中的最佳實踐

資源和社群

官方文件和資源

社群論壇和討論組

進一步學習的推薦資料

1. 介紹篇:MiniAPIs的魔法世界

嘿,各位程式碼魔法師們!今天我們要一起探索一個令人興奮的新領域 —— MiniAPIs。準備好你的魔杖(鍵盤),我們即將開始一段奇妙的旅程!

什麼是MiniAPIs?

想象一下,如果你能用幾行程式碼就建立出一個功能強大的API,是不是很酷?這就是MiniAPIs的魔力所在!MiniAPIs是ASP.NET Core中的一個輕量級框架,它允許我們以最小的程式碼和配置來建立HTTP API。

簡單來說,MiniAPIs就像是給你的Web應用裝上了一個超級加速器。它讓你能夠快速構建高效能的API端點,而不需要處理傳統ASP.NET MVC應用中的大量樣板程式碼。

MiniAPIs的特點和優勢

  1. 簡潔明瞭:使用MiniAPIs,你可以用極少的程式碼就能建立一個完整的API。沒有控制器,沒有複雜的路由配置,一切都變得如此簡單。

  2. 效能卓越:由於其輕量級設計,MiniAPIs執行起來飛快。它減少了中間層,直接處理HTTP請求,讓你的API響應如閃電般迅速。

  3. 靈活多變:MiniAPIs支援各種HTTP方法(GET, POST, PUT, DELETE等),並且可以輕鬆處理不同型別的請求和響應。

  4. 易於學習和使用:如果你已經熟悉C#和ASP.NET Core,那麼掌握MiniAPIs將是輕而易舉的事。即使你是新手,其直觀的API也會讓你很快上手。

  5. 與ASP.NET Core生態系統完美整合:MiniAPIs可以無縫地與其他ASP.NET Core功能(如依賴注入、中介軟體等)協同工作。

MiniAPIs的應用場景

MiniAPIs簡直就是為以下場景量身打造的:

  1. 微服務:當你需要快速構建輕量級的微服務時,MiniAPIs是你的得力助手。它能幫你建立高效、獨立的服務元件。

  2. 原型開發:需要快速驗證一個API想法?MiniAPIs讓你能在幾分鐘內就搭建出一個可用的原型。

  3. 簡單的CRUD應用:對於那些不需要複雜業務邏輯的基本CRUD(建立、讀取、更新、刪除)操作,MiniAPIs提供了一種快速實現的方式。

  4. serverless函式:在serverless環境中,MiniAPIs的輕量級特性使其成為理想的選擇。你可以輕鬆建立響應迅速的API函式。

  5. IoT裝置通訊:對於需要與IoT裝置進行簡單資料交換的應用,MiniAPIs提供了一種低開銷的解決方案。

讓我們來看一個簡單的例子,感受一下MiniAPIs的魔力:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/hello", () => "Hello, MiniAPIs World!");

app.Run();

就這麼簡單!這幾行程式碼就建立了一個返回"Hello, MiniAPIs World!"的API端點。是不是感覺像變魔術一樣?

總的來說,MiniAPIs為我們提供了一種快速、高效、靈活的方式來構建現代Web API。無論你是在構建下一個大型專案,還是隻是想快速實現一個想法,MiniAPIs都能成為你強大的盟友。

在接下來的章節中,我們將深入探討如何充分利用MiniAPIs的魔力,建立出令人驚歎的API。準備好了嗎?讓我們繼續我們的魔法之旅吧!

2. 環境搭建:為MiniAPIs魔法之旅做好準備

歡迎來到我們的MiniAPIs魔法學院!在開始我們激動人心的編碼冒險之前,我們需要先準備好我們的魔法工具箱。就像一個優秀的魔法師需要合適的魔杖一樣,一個出色的開發者也需要正確的開發環境。讓我們一起來看看如何為MiniAPIs的學習和使用搭建完美的環境吧!

系統要求

首先,讓我們確保你的電腦具備執行MiniAPIs所需的基本條件:

  1. 作業系統

    • Windows 10或更高版本
    • macOS 10.15 (Catalina)或更高版本
    • 各種主流Linux發行版(如Ubuntu 18.04+, Fedora 33+, Debian 10+)
  2. 硬體要求

    • 至少4GB RAM(推薦8GB或更多)
    • 至少10GB可用硬碟空間
    • 64位處理器

雖然這些是最低要求,但記住,更強大的硬體配置會讓你的開發體驗更加流暢。畢竟,誰不想要一根反應更快的魔杖呢?

安裝MiniAPIs

好訊息是,MiniAPIs實際上是ASP.NET Core的一部分,所以我們只需要安裝.NET SDK就可以了。以下是安裝步驟:

  1. 下載.NET SDK
    訪問官方.NET下載頁面 (https://dotnet.microsoft.com/download) 並下載最新版本的.NET SDK。選擇適合你作業系統的版本。

  2. 安裝.NET SDK
    執行下載的安裝程式,按照提示完成安裝過程。安裝過程通常很直觀,只需要點選"下一步"幾次就可以了。

  3. 驗證安裝
    安裝完成後,開啟你的命令列工具(在Windows上是命令提示符或PowerShell,在macOS或Linux上是終端),然後執行以下命令:

    dotnet --version
    

    如果安裝成功,你應該能看到安裝的.NET版本號。

  4. 安裝開發工具
    雖然你可以使用任何文字編輯器來編寫MiniAPIs程式碼,但我強烈推薦使用Visual Studio或Visual Studio Code。這些IDE提供了強大的程式碼補全、除錯和其他開發工具,可以大大提高你的開發效率。

    • Visual Studio: https://visualstudio.microsoft.com/
    • Visual Studio Code: https://code.visualstudio.com/

配置開發環境

現在我們已經安裝了必要的工具,讓我們來配置一下我們的開發環境:

  1. 設定環境變數
    確保DOTNET_ROOT環境變數指向你的.NET安裝目錄。這通常在安裝過程中會自動完成,但如果你遇到問題,可能需要手動設定。

  2. 安裝有用的擴充套件
    如果你使用的是Visual Studio Code,我推薦安裝以下擴充套件:

    • C# for Visual Studio Code (powered by OmniSharp)
    • .NET Core Test Explorer
    • NuGet Package Manager
  3. 建立你的第一個專案
    開啟命令列,導航到你想建立專案的目錄,然後執行以下命令:

    dotnet new web -n MyFirstMiniAPI
    

    這將建立一個新的MiniAPIs專案。

  4. 開啟專案
    使用你選擇的IDE開啟剛剛建立的專案。如果使用Visual Studio Code,可以執行:

    code MyFirstMiniAPI
    
  5. 執行專案
    在專案目錄中,執行以下命令來啟動你的MiniAPIs應用:

    dotnet run
    

    你應該會看到一條訊息,告訴你應用正在執行,並顯示一個URL(通常是 http://localhost:5000)。

恭喜!你已經成功搭建了MiniAPIs的開發環境,並執行了你的第一個MiniAPIs應用!感覺像是剛剛施展了一個強大的魔法,對吧?

記住,就像學習任何新魔法一樣,熟能生巧。不要害怕嘗試和實驗。在接下來的章節中,我們將深入探討如何使用這個強大的工具建立令人驚歎的API。

準備好開始你的MiniAPIs魔法之旅了嗎?讓我們繼續前進,探索更多奇妙的魔法吧!

3. 基礎概念:揭開MiniAPIs的神秘面紗

歡迎來到我們MiniAPIs魔法課程的第三章!今天,我們將揭開MiniAPIs的神秘面紗,深入瞭解它的核心概念。就像學習任何新魔法一樣,理解基礎理論對於掌握高階技巧至關重要。所以,繫好安全帶,我們要開始一次深入MiniAPIs內部的奇妙旅程了!

MiniAPIs架構概述

MiniAPIs的設計理念是簡單、輕量和高效。它建立在ASP.NET Core的基礎之上,但去除了許多傳統MVC(模型-檢視-控制器)架構中的複雜性。想象一下,如果傳統的MVC是一個複雜的魔法儀式,那麼MiniAPIs就是一個簡潔有力的咒語。

MiniAPIs的核心架構包括以下幾個關鍵元件:

  1. WebApplication:這是整個應用的入口點和宿主。它負責配置服務、中介軟體和路由。

  2. Endpoints:這些是API的終點,也就是處理特定HTTP請求的地方。

  3. Handlers:這些是實際處理請求並生成響應的函式。

  4. Middleware:這些元件在請求到達handler之前和之後處理請求。

關鍵術語解釋

讓我們更詳細地瞭解一下這些關鍵概念:

  1. Endpoint(端點)
    在MiniAPIs中,endpoint是一個特定的URL路徑,與一個HTTP方法(如GET、POST、PUT、DELETE等)相關聯。每個endpoint都對映到一個特定的handler函式。例如:

    app.MapGet("/hello", () => "Hello, World!");
    

    這裡,"/hello"就是一個endpoint,它響應GET請求。

  2. Handler(處理器)
    Handler是一個函式,它接收HTTP請求並返回響應。在MiniAPIs中,handler可以是一個簡單的lambda表示式,也可以是一個單獨定義的方法。例如:

    app.MapGet("/users/{id}", (int id) => $"User ID: {id}");
    

    這裡,(int id) => $"User ID: {id}" 就是一個handler。

  3. Middleware(中介軟體)
    Middleware是一種可以處理請求和響應的元件。它們可以在請求到達handler之前執行操作,也可以在handler處理完請求後修改響應。例如,你可以使用中介軟體來處理身份驗證、日誌記錄或異常處理。

  4. Routing(路由)
    Routing是將incoming HTTP請求對映到相應handler的過程。在MiniAPIs中,路由通常是透過Map方法定義的,如MapGetMapPost等。

  5. Dependency Injection(依賴注入)
    MiniAPIs完全支援ASP.NET Core的依賴注入系統。這允許你輕鬆地管理服務的生命週期和依賴關係。

MiniAPIs與其他API框架的對比

讓我們來看看MiniAPIs與一些其他流行的API框架有何不同:

  1. vs. 傳統ASP.NET Core MVC

    • MiniAPIs更輕量級,沒有控制器的概念。
    • 程式碼更簡潔,適合小型專案或微服務。
    • 啟動時間更短,效能略高。
  2. vs. Express.js (Node.js)

    • MiniAPIs提供了類似的簡潔語法。
    • MiniAPIs benefited from .NET的強型別系統和更好的效能。
    • Express.js可能在生態系統和社群支援方面更豐富。
  3. vs. Flask (Python)

    • 兩者都提供了簡單直觀的API建立方式。
    • MiniAPIs在處理併發請求時通常效能更好。
    • Flask可能在快速原型開發方面略勝一籌。
  4. vs. FastAPI (Python)

    • 兩者都注重效能和簡潔性。
    • FastAPI有內建的OpenAPI(Swagger)支援,而MiniAPIs需要額外配置。
    • MiniAPIs benefited from .NET的強大型別系統和高效能。

總的來說,MiniAPIs在簡潔性和效能之間取得了很好的平衡。它特別適合那些需要快速開發、高效能,同時又不想被複雜框架束縛的專案。

現在,你已經瞭解了MiniAPIs的基本架構和核心概念。這些知識將為你在接下來的章節中深入學習MiniAPIs奠定堅實的基礎。記住,就像學習任何新魔法一樣,理解基礎理論對於掌握高階技巧至關重要。

準備好進入下一個階段了嗎?在下一章中,我們將建立我們的第一個MiniAPIs專案!讓我們繼續我們的魔法之旅吧!

4. 第一個MiniAPIs專案:開啟你的魔法之旅

歡迎來到我們的MiniAPIs魔法課程的第四章!現在我們已經瞭解了MiniAPIs的基本概念,是時候動手建立我們的第一個專案了。就像一個初學魔法的學徒第一次揮舞魔杖一樣,我們今天將建立一個簡單但功能完整的MiniAPIs應用。繫好安全帶,我們要開始編碼了!

建立一個簡單的MiniAPIs專案

首先,讓我們建立一個新的MiniAPIs專案。開啟你的命令列工具,導航到你想要建立專案的目錄,然後執行以下命令:

dotnet new web -n MyFirstMiniAPI
cd MyFirstMiniAPI

這個命令建立了一個新的MiniAPIs專案並進入專案目錄。現在,讓我們開啟你喜歡的程式碼編輯器(我個人推薦Visual Studio Code)來檢視專案結構。

專案結構介紹

開啟專案後,你會看到以下檔案結構:

MyFirstMiniAPI/
├── Properties/
│   └── launchSettings.json
├── appsettings.json
├── appsettings.Development.json
├── Program.cs
├── MyFirstMiniAPI.csproj

讓我們簡單瞭解一下每個檔案的作用:

  • Program.cs: 這是應用的入口點,包含主要的應用配置和路由定義。
  • appsettings.jsonappsettings.Development.json: 這些檔案包含應用的配置設定。
  • MyFirstMiniAPI.csproj: 這是專案檔案,定義了專案的依賴關係和其他設定。
  • Properties/launchSettings.json: 這個檔案定義瞭如何啟動應用的設定。

編寫第一個API Endpoint

現在,讓我們開啟 Program.cs 檔案。你會看到一些預生成的程式碼。我們將修改這個檔案來建立我們的第一個API endpoint。

Program.cs 的內容替換為以下程式碼:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Welcome to My First MiniAPI!");

app.MapGet("/hello/{name}", (string name) => $"Hello, {name}!");

app.MapPost("/echo", async (HttpRequest request) =>
{
    using var reader = new StreamReader(request.Body);
    var body = await reader.ReadToEndAsync();
    return $"You said: {body}";
});

app.Run();

讓我們來解釋一下這段程式碼:

  1. 我們建立了一個基本的 Web 應用程式。
  2. 我們定義了三個 endpoints:
    • GET "/": 返回歡迎訊息
    • GET "/hello/{name}": 接受一個名字引數並返回個性化問候
    • POST "/echo": 讀取請求體並將其內容作為響應返回

執行和測試你的第一個API

現在,讓我們執行我們的API並進行測試。在命令列中,確保你在專案目錄下,然後執行:

dotnet run

你應該會看到類似這樣的輸出:

info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:7001
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5000

現在,我們的API已經在執行了!讓我們使用curl來測試我們的endpoints(如果你沒有curl,你可以使用瀏覽器或者任何API測試工具):

  1. 測試根路徑:

    curl http://localhost:5000/
    

    預期輸出:Welcome to My First MiniAPI!

  2. 測試帶引數的GET請求:

    curl http://localhost:5000/hello/MiniAPIs
    

    預期輸出:Hello, MiniAPIs!

  3. 測試POST請求:

    curl -X POST -d "Learning MiniAPIs is fun!" http://localhost:5000/echo
    

    預期輸出:You said: Learning MiniAPIs is fun!

恭喜你!你已經成功建立並執行了你的第一個MiniAPIs應用。感覺像是剛剛成功施展了你的第一個魔法,對吧?

這個簡單的例子展示了MiniAPIs的強大和靈活性。只用了幾行程式碼,我們就建立了一個功能完整的API,可以處理不同型別的HTTP請求,接受引數,並返回響應。

在接下來的章節中,我們將深入探討更多高階特性,如路由、資料驗證、錯誤處理等。但現在,花點時間慶祝一下你的第一個成功吧!你已經邁出了成為MiniAPIs大師的第一步。

記住,就像學習任何新技能一樣,練習是關鍵。嘗試修改這個例子,新增新的endpoints,或者改變現有endpoints的行為。探索和實驗是掌握MiniAPIs魔法的最佳方式。

準備好更深入地探索MiniAPIs的世界了嗎?在下一章中,我們將學習如何處理更復雜的路由和請求。讓我們繼續我們的魔法之旅吧!

5. 路由和請求處理:MiniAPIs的魔法地圖

歡迎來到我們的MiniAPIs魔法課程的第五章!今天,我們將深入探討MiniAPIs中的路由和請求處理。想象一下,如果你的API是一座魔法城堡,那麼路由就是城堡中的魔法地圖,指引請求找到正確的目的地。讓我們一起來探索如何建立這張魔法地圖,並處理來自四面八方的請求吧!

定義和管理路由

在MiniAPIs中,路由定義瞭如何將incoming HTTP請求對映到相應的處理函式。讓我們看看如何定義各種路由:

基本路由

最簡單的路由定義如下:

app.MapGet("/hello", () => "Hello, World!");

這個路由會響應對 /hello 的GET請求。

帶引數的路由

你可以在路由中包含引數:

app.MapGet("/users/{id}", (int id) => $"User ID: {id}");

這個路由會匹配類似 /users/1, /users/42 等路徑,並將 id 作為引數傳遞給處理函式。

可選引數和預設值

你也可以定義可選引數和預設值:

app.MapGet("/greet/{name?}", (string? name = "Guest") => $"Hello, {name}!");

這個路由既可以匹配 /greet/Alice,也可以匹配 /greet(此時name預設為"Guest")。

使用查詢引數

除了路徑引數,你還可以使用查詢引數:

app.MapGet("/search", (string? q, int? page = 1) => 
    $"Searching for '{q}' on page {page}");

這個路由可以處理類似 /search?q=dotnet&page=2 的請求。

處理不同型別的HTTP請求

MiniAPIs提供了處理各種HTTP方法的簡便方式:

GET 請求

我們已經看到了GET請求的例子。它們通常用於檢索資料:

app.MapGet("/api/products", () => new[] { "Product1", "Product2", "Product3" });

POST 請求

POST請求通常用於建立新資源:

app.MapPost("/api/products", (Product product) => 
{
    // 新增產品到資料庫
    return Results.Created($"/api/products/{product.Id}", product);
});

PUT 請求

PUT請求用於更新現有資源:

app.MapPut("/api/products/{id}", (int id, Product product) => 
{
    // 更新產品
    return Results.NoContent();
});

DELETE 請求

DELETE請求用於刪除資源:

app.MapDelete("/api/products/{id}", (int id) => 
{
    // 刪除產品
    return Results.Ok();
});

路由引數和查詢引數的使用

我們已經看到了如何在路由中使用引數,但讓我們更深入地探討一下:

路由引數

路由引數是URL路徑的一部分:

app.MapGet("/api/users/{id}/posts/{postId}", (int id, int postId) => 
    $"Fetching post {postId} for user {id}");

這個路由會匹配類似 /api/users/5/posts/10 的請求。

查詢引數

查詢引數是URL中 ? 後面的部分:

app.MapGet("/api/products", (string? category, int? minPrice, int? maxPrice) => 
{
    // 使用這些引數過濾產品
    return $"Fetching products in category {category}, " +
           $"price range: {minPrice ?? 0} - {maxPrice ?? int.MaxValue}";
});

這個路由可以處理類似 /api/products?category=electronics&minPrice=100&maxPrice=500 的請求。

組合使用

你可以在同一個路由中組合使用路由引數和查詢引數:

app.MapGet("/api/users/{userId}/orders", (int userId, DateTime? from, DateTime? to) => 
{
    return $"Fetching orders for user {userId}, " +
           $"from {from?.ToString("yyyy-MM-dd") ?? "the beginning"} " +
           $"to {to?.ToString("yyyy-MM-dd") ?? "now"}";
});

這個路由可以處理類似 /api/users/42/orders?from=2023-01-01&to=2023-06-30 的請求。

實戰練習:構建一個簡單的圖書管理API

讓我們把學到的知識運用到實踐中,建立一個簡單的圖書管理API:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var books = new List<Book>();

app.MapGet("/api/books", () => books);

app.MapGet("/api/books/{id}", (int id) => 
    books.FirstOrDefault(b => b.Id == id) is Book book 
        ? Results.Ok(book) 
        : Results.NotFound());

app.MapPost("/api/books", (Book book) =>
{
    book.Id = books.Count + 1;
    books.Add(book);
    return Results.Created($"/api/books/{book.Id}", book);
});

app.MapPut("/api/books/{id}", (int id, Book updatedBook) =>
{
    var book = books.FirstOrDefault(b => b.Id == id);
    if (book == null) return Results.NotFound();
    
    book.Title = updatedBook.Title;
    book.Author = updatedBook.Author;
    return Results.NoContent();
});

app.MapDelete("/api/books/{id}", (int id) =>
{
    var book = books.FirstOrDefault(b => b.Id == id);
    if (book == null) return Results.NotFound();
    
    books.Remove(book);
    return Results.Ok();
});

app.Run();

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Author { get; set; }
}

這個API允許你執行基本的CRUD操作:列出所有圖書、獲取特定圖書、新增新圖書、更新現有圖書和刪除圖書。

透過這個例子,你可以看到MiniAPIs如何輕鬆地處理不同型別的HTTP請求和引數。這種簡潔而強大的方式使得建立RESTful API變得異常簡單。

記住,就像掌握任何魔法一樣,熟能生巧。嘗試修改這個例子,新增新的功能,或者建立你自己的API。探索和實驗是成為MiniAPIs大師的最佳途徑。

在下一章中,我們將深入探討資料處理和驗證。準備好了嗎?讓我們繼續我們的MiniAPIs魔法之旅!

6. 資料處理:MiniAPIs的魔法變形術

歡迎來到我們MiniAPIs魔法課程的第六章!今天,我們將深入探討MiniAPIs中的資料處理。想象一下,如果你的API是一個魔法工坊,那麼資料處理就是你的變形術,將原始資料轉化為有用的資訊。讓我們一起來學習如何掌握這門強大的魔法吧!

處理請求資料

在MiniAPIs中,處理來自客戶端的資料是一項常見任務。我們將探討如何處理不同型別的輸入資料。

路徑引數

我們已經在前面的章節中看到了如何處理路徑引數,但讓我們再深入一點:

app.MapGet("/api/users/{id:int}", (int id) => $"User ID: {id}");

這裡,:int 是一個路由約束,確保 id 必須是一個整數。你可以使用其他約束,如 :guid, :bool, :datetime 等。

查詢引數

查詢引數可以直接作為方法引數:

app.MapGet("/api/search", (string? query, int? page, int? pageSize) => 
{
    return $"Searching for '{query}', Page: {page ?? 1}, PageSize: {pageSize ?? 10}";
});

這個端點可以處理像 /api/search?query=dotnet&page=2&pageSize=20 這樣的請求。

請求體

對於POST和PUT請求,你通常需要處理請求體中的資料。MiniAPIs可以自動將JSON請求體繫結到C#物件:

app.MapPost("/api/users", (User user) => 
{
    // 處理使用者資料
    return Results.Created($"/api/users/{user.Id}", user);
});

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

當你傳送一個JSON請求體到這個端點時,MiniAPIs會自動將其反序列化為User物件。

檔案上傳

處理檔案上傳也很簡單:

app.MapPost("/api/upload", async (IFormFile file) => 
{
    if (file.Length > 0)
    {
        var path = Path.Combine(Directory.GetCurrentDirectory(), "uploads", file.FileName);
        using var stream = new FileStream(path, FileMode.Create);
        await file.CopyToAsync(stream);
        return Results.Ok($"File {file.FileName} uploaded successfully.");
    }
    return Results.BadRequest("File is empty.");
});

這個端點可以處理檔案上傳,並將檔案儲存到伺服器的"uploads"目錄。

返回響應資料

MiniAPIs提供了多種方式來返回資料給客戶端。

直接返回物件

最簡單的方式是直接返回一個物件,MiniAPIs會自動將其序列化為JSON:

app.MapGet("/api/users/{id}", (int id) => 
    new User { Id = id, Name = "John Doe", Email = "john@example.com" });

使用 IResult

對於更復雜的響應,你可以使用 IResult 介面:

app.MapGet("/api/users/{id}", (int id) => 
{
    if (id <= 0)
        return Results.BadRequest("Invalid user ID");

    var user = GetUserById(id);
    if (user == null)
        return Results.NotFound($"User with ID {id} not found");

    return Results.Ok(user);
});

Results 類提供了多種方法來建立不同型別的HTTP響應。

自定義響應

你還可以完全控制HTTP響應:

app.MapGet("/api/download", () => 
{
    var bytes = System.IO.File.ReadAllBytes("somefile.pdf");
    return Results.File(bytes, "application/pdf", "report.pdf");
});

這個例子展示瞭如何返回一個檔案下載。

使用中介軟體處理資料

中介軟體可以在請求到達你的處理程式之前或之後處理資料。這對於實現橫切關注點(如日誌記錄、身份驗證等)非常有用。

建立自定義中介軟體

這裡是一個簡單的日誌中介軟體示例:

app.Use(async (context, next) =>
{
    var start = DateTime.UtcNow;

    await next();

    var duration = DateTime.UtcNow - start;
    Console.WriteLine($"Request to {context.Request.Path} took {duration.TotalMilliseconds}ms");
});

這個中介軟體會記錄每個請求的處理時間。

使用內建中介軟體

ASP.NET Core提供了許多內建中介軟體,你可以在MiniAPIs中使用它們:

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

這些中介軟體分別用於HTTPS重定向、身份驗證和授權。

實戰練習:構建一個待辦事項API

讓我們把學到的知識應用到實踐中,建立一個簡單的待辦事項API:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var todos = new List<Todo>();

app.MapGet("/api/todos", () => todos);

app.MapGet("/api/todos/{id}", (int id) => 
    todos.FirstOrDefault(t => t.Id == id) is Todo todo 
        ? Results.Ok(todo) 
        : Results.NotFound());

app.MapPost("/api/todos", (Todo todo) =>
{
    todo.Id = todos.Count + 1;
    todos.Add(todo);
    return Results.Created($"/api/todos/{todo.Id}", todo);
});

app.MapPut("/api/todos/{id}", (int id, Todo updatedTodo) =>
{
    var todo = todos.FirstOrDefault(t => t.Id == id);
    if (todo == null) return Results.NotFound();
    
    todo.Title = updatedTodo.Title;
    todo.IsCompleted = updatedTodo.IsCompleted;
    return Results.NoContent();
});

app.MapDelete("/api/todos/{id}", (int id) =>
{
    var todo = todos.FirstOrDefault(t => t.Id == id);
    if (todo == null) return Results.NotFound();
    
    todos.Remove(todo);
    return Results.Ok();
});

app.Run();

public class Todo
{
    public int Id { get; set; }
    public string Title { get; set; }
    public bool IsCompleted { get; set; }
}

這個API允許你建立、讀取、更新和刪除待辦事項。它展示瞭如何處理不同型別的請求資料,以及如何返回適當的響應。

透過這個例子,你可以看到MiniAPIs如何輕鬆地處理各種資料處理任務。這種簡潔而強大的方式使得建立功能豐富的API變得異常簡單。

記住,就像掌握任何魔法一樣,練習是關鍵。嘗試擴充套件這個例子,新增新的功能,或者建立你自己的API。探索和實驗是成為MiniAPIs大師的最佳途徑。

在下一章中,我們將深入探討錯誤處理和異常管理。準備好了嗎?讓我們繼續我們的MiniAPIs魔法之旅!

7. 錯誤處理:MiniAPIs的防護咒語

歡迎來到我們MiniAPIs魔法課程的第七章!今天,我們將深入探討MiniAPIs中的錯誤處理。想象一下,如果你的API是一座堅固的城堡,那麼錯誤處理就是保護城堡的防護咒語,確保即使在面對意外情況時,你的API也能優雅地響應。讓我們一起來學習如何構建這些強大的防護咒語吧!

捕獲和處理錯誤

在MiniAPIs中,有幾種方法可以處理錯誤和異常。讓我們逐一探討:

使用try-catch塊

最基本的錯誤處理方法是使用try-catch塊:

app.MapGet("/api/users/{id}", (int id) => 
{
    try
    {
        var user = GetUserById(id); // 假設這個方法可能丟擲異常
        return Results.Ok(user);
    }
    catch (Exception ex)
    {
        return Results.Problem($"An error occurred: {ex.Message}");
    }
});

這種方法允許你捕獲並處理特定端點中的錯誤。

使用Results.Problem()

MiniAPIs提供了Results.Problem()方法,可以用來返回標準化的錯誤響應:

app.MapGet("/api/users/{id}", (int id) => 
{
    var user = GetUserById(id);
    if (user == null)
        return Results.Problem(
            statusCode: 404,
            title: "User not found",
            detail: $"No user with ID {id} exists.");

    return Results.Ok(user);
});

Results.Problem()會生成一個符合RFC7807規範的問題詳情(ProblemDetails)響應。

全域性異常處理

對於應用範圍內的錯誤處理,你可以使用中介軟體:

app.Use(async (context, next) =>
{
    try
    {
        await next(context);
    }
    catch (Exception ex)
    {
        context.Response.StatusCode = 500;
        await context.Response.WriteAsJsonAsync(new { error = "An unexpected error occurred." });
    }
});

這個中介軟體會捕獲所有未處理的異常,並返回一個通用的錯誤訊息。

自定義錯誤響應

有時,你可能想要返回自定義的錯誤響應。這裡有幾種方法:

建立自定義錯誤物件

你可以建立一個自定義的錯誤物件,並在需要時返回它:

public class ApiError
{
    public string Message { get; set; }
    public string[] Details { get; set; }
}

app.MapGet("/api/items/{id}", (int id) => 
{
    var item = GetItemById(id);
    if (item == null)
        return Results.NotFound(new ApiError 
        { 
            Message = "Item not found", 
            Details = new[] { $"No item with ID {id} exists." } 
        });

    return Results.Ok(item);
});

使用ProblemDetails類

ASP.NET Core提供了ProblemDetails類,你可以用它來建立符合RFC7807的錯誤響應:

app.MapGet("/api/orders/{id}", (int id) => 
{
    var order = GetOrderById(id);
    if (order == null)
        return Results.Problem(new ProblemDetails
        {
            Status = 404,
            Title = "Order not found",
            Detail = $"No order with ID {id} exists.",
            Instance = $"/api/orders/{id}"
        });

    return Results.Ok(order);
});

使用中介軟體進行錯誤處理

中介軟體是一種強大的方式來集中處理錯誤。以下是一個更復雜的例子,展示瞭如何使用中介軟體來處理不同型別的異常:

app.Use(async (context, next) =>
{
    try
    {
        await next(context);
    }
    catch (Exception ex)
    {
        var problemDetails = new ProblemDetails();

        switch (ex)
        {
            case NotFoundException notFound:
                problemDetails.Status = StatusCodes.Status404NotFound;
                problemDetails.Title = "Resource not found";
                problemDetails.Detail = notFound.Message;
                break;
            case UnauthorizedException unauthorized:
                problemDetails.Status = StatusCodes.Status401Unauthorized;
                problemDetails.Title = "Unauthorized";
                problemDetails.Detail = unauthorized.Message;
                break;
            default:
                problemDetails.Status = StatusCodes.Status500InternalServerError;
                problemDetails.Title = "An unexpected error occurred";
                problemDetails.Detail = "Please contact support if the problem persists.";
                break;
        }

        context.Response.StatusCode = problemDetails.Status.Value;
        await context.Response.WriteAsJsonAsync(problemDetails);
    }
});

這個中介軟體可以處理不同型別的自定義異常,並返回適當的錯誤響應。

實戰練習:增強待辦事項API的錯誤處理

讓我們回到我們的待辦事項API,並增強其錯誤處理能力:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var todos = new List<Todo>();

// 全域性錯誤處理中介軟體
app.Use(async (context, next) =>
{
    try
    {
        await next(context);
    }
    catch (Exception ex)
    {
        context.Response.StatusCode = 500;
        await context.Response.WriteAsJsonAsync(new { error = "An unexpected error occurred.", details = ex.Message });
    }
});

app.MapGet("/api/todos", () => todos);

app.MapGet("/api/todos/{id}", (int id) => 
{
    var todo = todos.FirstOrDefault(t => t.Id == id);
    if (todo == null)
        return Results.Problem(
            statusCode: 404,
            title: "Todo not found",
            detail: $"No todo item with ID {id} exists.");

    return Results.Ok(todo);
});

app.MapPost("/api/todos", (Todo todo) =>
{
    if (string.IsNullOrWhiteSpace(todo.Title))
        return Results.BadRequest(new { error = "Title is required." });

    todo.Id = todos.Count + 1;
    todos.Add(todo);
    return Results.Created($"/api/todos/{todo.Id}", todo);
});

app.MapPut("/api/todos/{id}", (int id, Todo updatedTodo) =>
{
    var todo = todos.FirstOrDefault(t => t.Id == id);
    if (todo == null)
        return Results.NotFound(new { error = $"No todo item with ID {id} exists." });
    
    if (string.IsNullOrWhiteSpace(updatedTodo.Title))
        return Results.BadRequest(new { error = "Title is required." });

    todo.Title = updatedTodo.Title;
    todo.IsCompleted = updatedTodo.IsCompleted;
    return Results.NoContent();
});

app.MapDelete("/api/todos/{id}", (int id) =>
{
    var todo = todos.FirstOrDefault(t => t.Id == id);
    if (todo == null)
        return Results.NotFound(new { error = $"No todo item with ID {id} exists." });
    
    todos.Remove(todo);
    return Results.Ok();
});

app.Run();

public class Todo
{
    public int Id { get; set; }
    public string Title { get; set; }
    public bool IsCompleted { get; set; }
}

這個增強版的API現在包含了更robust的錯誤處理:

  1. 我們新增了一個全域性錯誤處理中介軟體來捕獲未處理的異常。
  2. 對於"Not Found"情況,我們使用Results.Problem()返回標準化的錯誤響應。
  3. 對於驗證錯誤(如空標題),我們返回Bad Request響應。
  4. 我們為每個可能的錯誤情況提供了明確的錯誤訊息。

透過這些改進,我們的API現在能夠更優雅地處理各種錯誤情況,提供清晰的錯誤資訊給客戶端。

記住,良好的錯誤處理不僅能提高你的API的穩定性,還能大大改善開發者的體驗。就像一個優秀的魔法師總是為意外情況做好準備一樣,一個優秀的API也應該能夠優雅地處理各種錯誤情況。

在下一章中,我們將探討資料驗證和安全性。準備好了嗎?讓我們繼續我們的MiniAPIs魔法之旅!

8. 資料驗證和安全:MiniAPIs的守護咒語

歡迎來到我們MiniAPIs魔法課程的第八章!今天,我們將深入探討MiniAPIs中的資料驗證和安全性。想象一下,如果你的API是一個神奇的寶庫,那麼資料驗證和安全措施就是保護這個寶庫的守護咒語,確保只有合法的請求才能訪問和修改你的寶貴資料。讓我們一起來學習如何構建這些強大的守護咒語吧!

資料驗證方法

在MiniAPIs中,有幾種方法可以進行資料驗證。讓我們逐一探討:

1. 手動驗證

最簡單的方法是在處理程式中手動進行驗證:

app.MapPost("/api/users", (User user) =>
{
    if (string.IsNullOrEmpty(user.Name))
        return Results.BadRequest("Name is required.");
    if (user.Age < 0 || user.Age > 120)
        return Results.BadRequest("Age must be between 0 and 120.");

    // 處理有效使用者...
    return Results.Created($"/api/users/{user.Id}", user);
});

這種方法簡單直接,但對於複雜的驗證邏輯可能會導致程式碼膨脹。

2. 使用資料註解

你可以在模型類中使用資料註解進行驗證:

public class User
{
    public int Id { get; set; }

    [Required(ErrorMessage = "Name is required.")]
    [StringLength(100, MinimumLength = 2, ErrorMessage = "Name must be between 2 and 100 characters.")]
    public string Name { get; set; }

    [Range(0, 120, ErrorMessage = "Age must be between 0 and 120.")]
    public int Age { get; set; }
}

app.MapPost("/api/users", (User user) =>
{
    if (!ModelState.IsValid)
        return Results.ValidationProblem(ModelState);

    // 處理有效使用者...
    return Results.Created($"/api/users/{user.Id}", user);
});

這種方法將驗證邏輯與模型定義結合,使程式碼更加清晰。

3. 使用FluentValidation

對於更復雜的驗證邏輯,你可以使用FluentValidation庫:

public class UserValidator : AbstractValidator<User>
{
    public UserValidator()
    {
        RuleFor(x => x.Name).NotEmpty().Length(2, 100);
        RuleFor(x => x.Age).InclusiveBetween(0, 120);
        RuleFor(x => x.Email).NotEmpty().EmailAddress();
    }
}

// 在Program.cs中
builder.Services.AddValidatorsFromAssemblyContaining<UserValidator>();

app.MapPost("/api/users", async (User user, IValidator<User> validator) =>
{
    var validationResult = await validator.ValidateAsync(user);
    if (!validationResult.IsValid)
        return Results.ValidationProblem(validationResult.ToDictionary());

    // 處理有效使用者...
    return Results.Created($"/api/users/{user.Id}", user);
});

FluentValidation提供了一種強大而靈活的方式來定義複雜的驗證規則。

保護API安全的最佳實踐

保護你的API安全是至關重要的。以下是一些最佳實踐:

1. 使用HTTPS

始終使用HTTPS來加密傳輸中的資料:

app.UseHttpsRedirection();

2. 實現速率限制

使用速率限制來防止API濫用:

builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(),
            factory: partition => new FixedWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                PermitLimit = 10,
                QueueLimit = 0,
                Window = TimeSpan.FromMinutes(1)
            }));
});

app.UseRateLimiter();

3. 驗證和清理輸入資料

始終驗證和清理所有輸入資料,以防止注入攻擊:

app.MapPost("/api/comments", (CommentInput input) =>
{
    var sanitizedComment = System.Web.HttpUtility.HtmlEncode(input.Comment);
    // 處理清理後的評論...
});

4. 使用適當的HTTP狀態碼

使用正確的HTTP狀態碼來表示不同的錯誤情況:

app.MapGet("/api/users/{id}", (int id) =>
{
    var user = GetUserById(id);
    if (user == null)
        return Results.NotFound();
    if (!IsAuthorized(user))
        return Results.Forbid();
    return Results.Ok(user);
});

身份驗證和授權

MiniAPIs完全支援ASP.NET Core的身份驗證和授權功能。

1. 設定身份驗證

首先,新增身份驗證服務:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        };
    });

app.UseAuthentication();
app.UseAuthorization();

2. 保護端點

然後,你可以使用[Authorize]屬性來保護端點:

app.MapGet("/api/secure", [Authorize] (ClaimsPrincipal user) =>
{
    return $"Hello, {user.Identity.Name}!";
});

3. 基於角色的授權

你還可以實現基於角色的授權:

app.MapGet("/api/admin", [Authorize(Roles = "Admin")] () =>
{
    return "Welcome, Admin!";
});

實戰練習:增強待辦事項API的安全性

讓我們回到我們的待辦事項API,並增強其安全性和資料驗證:

using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using FluentValidation;

var builder = WebApplication.CreateBuilder(args);

// 新增JWT身份驗證
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        };
    });

// 新增FluentValidation
builder.Services.AddValidatorsFromAssemblyContaining<TodoValidator>();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

var todos = new List<Todo>();

// 全域性錯誤處理中介軟體
app.Use(async (context, next) =>
{
    try
    {
        await next(context);
    }
    catch (Exception ex)
    {
        context.Response.StatusCode = 500;
        await context.Response.WriteAsJsonAsync(new { error = "An unexpected error occurred.", details = ex.Message });
    }
});

app.MapGet("/api/todos", [Authorize] () => todos);

app.MapGet("/api/todos/{id}", [Authorize] (int id) => 
{
    var todo = todos.FirstOrDefault(t => t.Id == id);
    if (todo == null)
        return Results.NotFound(new { error = $"No todo item with ID {id} exists." });

    return Results.Ok(todo);
});

app.MapPost("/api/todos", [Authorize] async (Todo todo, IValidator<Todo> validator) =>
{
    var validationResult = await validator.ValidateAsync(todo);
    if (!validationResult.IsValid)
        return Results.ValidationProblem(validationResult.ToDictionary());

    todo.Id = todos.Count + 1;
    todos.Add(todo);
    return Results.Created($"/api/todos/{todo.Id}", todo);
});

app.MapPut("/api/todos/{id}", [Authorize] async (int id, Todo updatedTodo, IValidator<Todo> validator) =>
{
    var todo = todos.FirstOrDefault(t => t.Id == id);
    if (todo == null)
        return Results.NotFound(new { error = $"No todo item with ID {id} exists." });
    
    var validationResult = await validator.ValidateAsync(updatedTodo);
    if (!validationResult.IsValid)
        return Results.ValidationProblem(validationResult.ToDictionary());

    todo.Title = updatedTodo.Title;
    todo.IsCompleted = updatedTodo.IsCompleted;
    return Results.NoContent();
});

app.MapDelete("/api/todos/{id}", [Authorize] (int id) =>
{
    var todo = todos.FirstOrDefault(t => t.Id == id);
    if (todo == null)
        return Results.NotFound(new { error = $"No todo item with ID {id} exists." });
    
    todos.Remove(todo);
    return Results.Ok();
});

app.Run();

public class Todo
{
    public int Id { get; set; }
    public string Title { get; set; }
    public bool IsCompleted { get; set; }
}

public class TodoValidator : AbstractValidator<Todo>
{
    public TodoValidator()
    {
        RuleFor(x => x.Title).NotEmpty().MaximumLength(100);
    }
}

這個增強版的API現在包含了更強大的安全性和資料驗證:

  1. 我們新增了JWT身份驗證,所有端點都需要認證才能訪問。
  2. 我們使用FluentValidation進行資料驗證,確保Todo項的標題不為空且不超過100個字元。
  3. 我們使用HTTPS重定向來確保所有通訊都是加密的。
  4. 我們保留了全域性錯誤處理中介軟體來處理未預期的異常。

透過這些改進,我們的API現在更安全,更能抵禦潛在的攻擊和無效資料。記住,安全性是一個持續的過程,隨著你的API發展,你可能需要實施更多的安全措施。

在下一章中,我們將探討如何將MiniAPIs與資料庫整合。準備好了嗎?讓我們繼續我們的MiniAPIs魔法之旅!

9. 與資料庫互動:MiniAPIs的魔法儲存術

歡迎來到我們MiniAPIs魔法課程的第九章!今天,我們將探討如何讓MiniAPIs與資料庫進行互動。想象一下,如果你的API是一個魔法圖書館,那麼資料庫就是這個圖書館的魔法書架,儲存著所有珍貴的資訊。讓我們一起來學習如何使用MiniAPIs來操作這些魔法書架吧!

連線資料庫

在MiniAPIs中,我們通常使用Entity Framework Core (EF Core)來與資料庫互動。EF Core是一個強大的ORM(物件關係對映)工具,它允許我們使用C#程式碼來運算元據庫。

首先,我們需要安裝必要的NuGet包:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design

然後,我們需要建立一個資料庫上下文類:

using Microsoft.EntityFrameworkCore;

public class TodoDbContext : DbContext
{
    public TodoDbContext(DbContextOptions<TodoDbContext> options)
        : base(options)
    {
    }

    public DbSet<Todo> Todos { get; set; }
}

接下來,我們需要在Program.cs中配置資料庫連線:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<TodoDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

var app = builder.Build();

確保在你的appsettings.json檔案中新增連線字串:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=TodoDb;Trusted_Connection=True;"
  }
}

執行基本的CRUD操作

現在我們已經連線了資料庫,讓我們來看看如何執行基本的CRUD(建立、讀取、更新、刪除)操作。

建立(Create)

app.MapPost("/api/todos", async (Todo todo, TodoDbContext db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();
    return Results.Created($"/api/todos/{todo.Id}", todo);
});

讀取(Read)

app.MapGet("/api/todos", async (TodoDbContext db) =>
    await db.Todos.ToListAsync());

app.MapGet("/api/todos/{id}", async (int id, TodoDbContext db) =>
{
    var todo = await db.Todos.FindAsync(id);
    return todo is null ? Results.NotFound() : Results.Ok(todo);
});

更新(Update)

app.MapPut("/api/todos/{id}", async (int id, Todo inputTodo, TodoDbContext db) =>
{
    var todo = await db.Todos.FindAsync(id);
    if (todo is null) return Results.NotFound();

    todo.Title = inputTodo.Title;
    todo.IsCompleted = inputTodo.IsCompleted;

    await db.SaveChangesAsync();
    return Results.NoContent();
});

刪除(Delete)

app.MapDelete("/api/todos/{id}", async (int id, TodoDbContext db) =>
{
    var todo = await db.Todos.FindAsync(id);
    if (todo is null) return Results.NotFound();

    db.Todos.Remove(todo);
    await db.SaveChangesAsync();
    return Results.Ok();
});

使用ORM(物件關係對映)工具

我們已經在上面的例子中使用了Entity Framework Core,這是.NET生態系統中最流行的ORM工具。使用ORM有很多好處:

  1. 型別安全:ORM允許我們使用強型別的C#物件,而不是直接處理SQL字串。
  2. 抽象資料庫操作:ORM處理了與資料庫的低階互動,讓我們可以專注於業務邏輯。
  3. 資料庫無關性:透過更改配置,我們可以輕鬆地切換到不同的資料庫系統。
  4. 效能最佳化:許多ORM工具(包括EF Core)都有內建的效能最佳化功能。

然而,使用ORM也有一些注意事項:

  1. 學習曲線:理解和有效使用ORM可能需要一些時間。
  2. 效能開銷:在某些複雜查詢中,ORM可能不如直接的SQL查詢高效。
  3. 黑盒操作:有時候很難理解ORM在底層究竟執行了什麼SQL。

實戰練習:將待辦事項API與資料庫整合

讓我們將我們的待辦事項API與SQL Server資料庫整合。我們將使用Entity Framework Core作為ORM工具。

首先,確保你已經安裝了必要的NuGet包。然後,更新你的Program.cs檔案:

using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using FluentValidation;

var builder = WebApplication.CreateBuilder(args);

// 新增資料庫上下文
builder.Services.AddDbContext<TodoDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// 新增FluentValidation
builder.Services.AddValidatorsFromAssemblyContaining<TodoValidator>();

var app = builder.Build();

app.UseHttpsRedirection();

// 全域性錯誤處理中介軟體
app.Use(async (context, next) =>
{
    try
    {
        await next(context);
    }
    catch (Exception ex)
    {
        context.Response.StatusCode = 500;
        await context.Response.WriteAsJsonAsync(new { error = "An unexpected error occurred.", details = ex.Message });
    }
});

app.MapGet("/api/todos", async (TodoDbContext db) =>
    await db.Todos.ToListAsync());

app.MapGet("/api/todos/{id}", async (int id, TodoDbContext db) =>
{
    var todo = await db.Todos.FindAsync(id);
    return todo is null ? Results.NotFound(new { error = $"No todo item with ID {id} exists." }) : Results.Ok(todo);
});

app.MapPost("/api/todos", async (Todo todo, TodoDbContext db, IValidator<Todo> validator) =>
{
    var validationResult = await validator.ValidateAsync(todo);
    if (!validationResult.IsValid)
        return Results.ValidationProblem(validationResult.ToDictionary());

    db.Todos.Add(todo);
    await db.SaveChangesAsync();
    return Results.Created($"/api/todos/{todo.Id}", todo);
});

app.MapPut("/api/todos/{id}", async (int id, Todo inputTodo, TodoDbContext db, IValidator<Todo> validator) =>
{
    var todo = await db.Todos.FindAsync(id);
    if (todo is null)
        return Results.NotFound(new { error = $"No todo item with ID {id} exists." });

    var validationResult = await validator.ValidateAsync(inputTodo);
    if (!validationResult.IsValid)
        return Results.ValidationProblem(validationResult.ToDictionary());

    todo.Title = inputTodo.Title;
    todo.IsCompleted = inputTodo.IsCompleted;

    await db.SaveChangesAsync();
    return Results.NoContent();
});

app.MapDelete("/api/todos/{id}", async (int id, TodoDbContext db) =>
{
    var todo = await db.Todos.FindAsync(id);
    if (todo is null)
        return Results.NotFound(new { error = $"No todo item with ID {id} exists." });

    db.Todos.Remove(todo);
    await db.SaveChangesAsync();
    return Results.Ok();
});

app.Run();

public class Todo
{
    public int Id { get; set; }
    [Required]
    public string Title { get; set; }
    public bool IsCompleted { get; set; }
}

public class TodoValidator : AbstractValidator<Todo>
{
    public TodoValidator()
    {
        RuleFor(x => x.Title).NotEmpty().MaximumLength(100);
    }
}

public class TodoDbContext : DbContext
{
    public TodoDbContext(DbContextOptions<TodoDbContext> options)
        : base(options)
    {
    }

    public DbSet<Todo> Todos { get; set; }
}

這個版本的API現在完全整合了資料庫操作:

  1. 我們使用Entity Framework Core來與SQL Server資料庫互動。
  2. 所有的CRUD操作現在都是持久化的,資料會被儲存在資料庫中。
  3. 我們保留了之前的資料驗證和錯誤處理邏輯。
  4. 我們使用非同步方法來進行所有的資料庫操作,這有助於提高應用的效能和可伸縮性。

記住,在執行這個應用之前,你需要建立資料庫並應用遷移。你可以使用以下EF Core命令來做到這一點:

dotnet ef migrations add InitialCreate
dotnet ef database update

透過這些改進,我們的API現在不僅能處理HTTP請求,還能持久化資料到資料庫。這為構建更復雜、更實用的應用奠定了基礎。

在下一章中,我們將探討MiniAPIs的一些高階特性。準備好了嗎?讓我們繼續我們的MiniAPIs魔法之旅!

10. 高階特性:MiniAPIs的魔法進階

歡迎來到我們MiniAPIs魔法課程的第十章!今天,我們將探索MiniAPIs的一些高階特性。想象一下,如果基礎的MiniAPIs知識是你的魔法學徒期,那麼這些高階特性就是讓你成為真正的魔法大師的關鍵。讓我們一起來學習這些強大的高階魔法吧!

中介軟體的使用和編寫

中介軟體是ASP.NET Core應用程式管道中的軟體元件,用於處理請求和響應。在MiniAPIs中,我們可以使用現有的中介軟體,也可以建立自定義中介軟體。

使用內建中介軟體

ASP.NET Core提供了許多內建中介軟體,我們可以在MiniAPIs中使用它們:

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();

app.Run();

建立自定義中介軟體

我們還可以建立自定義中介軟體來處理特定的需求:

public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        _logger.LogInformation($"Request received: {context.Request.Method} {context.Request.Path}");
        await _next(context);
        _logger.LogInformation($"Response sent with status code: {context.Response.StatusCode}");
    }
}

// 在Program.cs中使用
app.UseMiddleware<RequestLoggingMiddleware>();

檔案上傳和下載

MiniAPIs可以輕鬆處理檔案上傳和下載操作。

檔案上傳

app.MapPost("/upload", async (IFormFile file) =>
{
    if (file.Length > 0)
    {
        var path = Path.Combine(Directory.GetCurrentDirectory(), "uploads", file.FileName);
        using var stream = new FileStream(path, FileMode.Create);
        await file.CopyToAsync(stream);
        return Results.Ok(new { file.FileName, file.Length });
    }
    return Results.BadRequest("No file uploaded.");
});

檔案下載

app.MapGet("/download/{fileName}", (string fileName) =>
{
    var path = Path.Combine(Directory.GetCurrentDirectory(), "uploads", fileName);
    if (!System.IO.File.Exists(path))
        return Results.NotFound($"File {fileName} not found.");

    return Results.File(path, "application/octet-stream", fileName);
});

實現API版本控制

API版本控制是一種重要的實踐,它允許你在不破壞現有客戶端的情況下evolve你的API。在MiniAPIs中,我們可以使用Asp.Versioning.Http包來實現版本控制。

首先,安裝必要的NuGet包:

dotnet add package Asp.Versioning.Http

然後,在你的Program.cs中配置API版本控制:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
});

var app = builder.Build();

app.MapGet("/api/v{version:apiVersion}/hello", (ApiVersion version) =>
    $"Hello from API version {version}!")
    .WithApiVersionSet(versionSet)
    .MapToApiVersion(1.0);

app.MapGet("/api/v{version:apiVersion}/hello", (ApiVersion version) =>
    $"Greetings from API version {version}!")
    .WithApiVersionSet(versionSet)
    .MapToApiVersion(2.0);

app.Run();

這個例子展示瞭如何為同一個路由建立不同的版本。

依賴注入和生命週期管理

MiniAPIs完全支援ASP.NET Core的依賴注入(DI)系統。這允許你輕鬆管理服務的生命週期和依賴關係。

註冊服務

Program.cs中,你可以註冊服務:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IMyService, MyService>();
builder.Services.AddScoped<IMyDbContext, MyDbContext>();
builder.Services.AddTransient<IMyHelper, MyHelper>();

var app = builder.Build();

使用注入的服務

在你的端點處理程式中,你可以直接使用這些服務:

app.MapGet("/api/data", (IMyService myService, IMyDbContext dbContext) =>
{
    var data = myService.GetData();
    dbContext.SaveData(data);
    return Results.Ok(data);
});

非同步程式設計

MiniAPIs完全支援非同步程式設計,這對於提高應用程式的效能和可伸縮性非常重要。

app.MapGet("/api/data", async (IMyAsyncService myService) =>
{
    var data = await myService.GetDataAsync();
    return Results.Ok(data);
});

實戰練習:高階待辦事項API

讓我們將這些高階特性應用到我們的待辦事項API中:

using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// 新增資料庫上下文
builder.Services.AddDbContext<TodoDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// 新增FluentValidation
builder.Services.AddValidatorsFromAssemblyContaining<TodoValidator>();

// 新增API版本控制
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
});

var app = builder.Build();

app.UseHttpsRedirection();

// 自定義中介軟體:請求日誌記錄
app.Use(async (context, next) =>
{
    var start = DateTime.UtcNow;
    await next();
    var end = DateTime.UtcNow;
    var duration = end - start;
    Console.WriteLine($"Request to {context.Request.Path} took {duration.TotalMilliseconds}ms");
});

// API v1
var v1 = app.NewApiVersion(1, 0);

app.MapGet("/api/v{version:apiVersion}/todos", async (TodoDbContext db) =>
    await db.Todos.ToListAsync())
    .WithApiVersionSet(v1);

app.MapGet("/api/v{version:apiVersion}/todos/{id}", async (int id, TodoDbContext db) =>
{
    var todo = await db.Todos.FindAsync(id);
    return todo is null ? Results.NotFound(new { error = $"No todo item with ID {id} exists." }) : Results.Ok(todo);
})
.WithApiVersionSet(v1);

app.MapPost("/api/v{version:apiVersion}/todos", async (Todo todo, TodoDbContext db, IValidator<Todo> validator) =>
{
    var validationResult = await validator.ValidateAsync(todo);
    if (!validationResult.IsValid)
        return Results.ValidationProblem(validationResult.ToDictionary());

    db.Todos.Add(todo);
    await db.SaveChangesAsync();
    return Results.Created($"/api/todos/{todo.Id}", todo);
})
.WithApiVersionSet(v1);

app.MapPut("/api/v{version:apiVersion}/todos/{id}", async (int id, Todo inputTodo, TodoDbContext db, IValidator<Todo> validator) =>
{
    var todo = await db.Todos.FindAsync(id);
    if (todo is null)
        return Results.NotFound(new { error = $"No todo item with ID {id} exists." });

    var validationResult = await validator.ValidateAsync(inputTodo);
    if (!validationResult.IsValid)
        return Results.ValidationProblem(validationResult.ToDictionary());

    todo.Title = inputTodo.Title;
    todo.IsCompleted = inputTodo.IsCompleted;

    await db.SaveChangesAsync();
    return Results.NoContent();
})
.WithApiVersionSet(v1);

app.MapDelete("/api/v{version:apiVersion}/todos/{id}", async (int id, TodoDbContext db) =>
{
    var todo = await db.Todos.FindAsync(id);
    if (todo is null)
        return Results.NotFound(new { error = $"No todo item with ID {id} exists." });

    db.Todos.Remove(todo);
    await db.SaveChangesAsync();
    return Results.Ok();
})
.WithApiVersionSet(v1);

// 檔案上傳和下載
app.MapPost("/api/v{version:apiVersion}/upload", async (IFormFile file) =>
{
    if (file.Length > 0)
    {
        var path = Path.Combine(Directory.GetCurrentDirectory(), "uploads", file.FileName);
        using var stream = new FileStream(path, FileMode.Create);
        await file.CopyToAsync(stream);
        return Results.Ok(new { file.FileName, file.Length });
    }
    return Results.BadRequest("No file uploaded.");
})
.WithApiVersionSet(v1);

app.MapGet("/api/v{version:apiVersion}/download/{fileName}", (string fileName) =>
{
    var path = Path.Combine(Directory.GetCurrentDirectory(), "uploads", fileName);
    if (!System.IO.File.Exists(path))
        return Results.NotFound($"File {fileName} not found.");

    return Results.File(path, "application/octet-stream", fileName);
})
.WithApiVersionSet(v1);

app.Run();

public class Todo
{
    public int Id { get; set; }
    [Required]
    public string Title { get; set; }
    public bool IsCompleted { get; set; }
}

public class TodoValidator : AbstractValidator<Todo>
{
    public TodoValidator()
    {
        RuleFor(x => x.Title).NotEmpty().MaximumLength(100);
    }
}

public class TodoDbContext : DbContext
{
    public TodoDbContext(DbContextOptions<TodoDbContext> options)
        : base(options)
    {
    }

    public DbSet<Todo> Todos { get; set; }
}

這個高階版本的API現在包含了以下特性:

  1. API版本控制:所有端點都有版本控制。
  2. 自定義中介軟體:用於記錄請求處理時間。
  3. 檔案上傳和下載功能。
  4. 保留了之前的資料驗證、錯誤處理和資料庫操作。

透過這些高階特性,我們的API變得更加強大和靈活。它現在可以處理版本控制、檔案操作,並提供了更好的效能監控。

記住,掌握這些高階特性需要時間和實踐。不要害怕實驗和嘗試新的東西。每一次嘗試都會讓你更接近成為一個真正的MiniAPIs魔法大師!

在下一章中,我們將探討如何測試和除錯你的MiniAPIs應用。準備好了嗎?讓我們繼續我們的MiniAPIs魔法之旅!

11. 測試和除錯:MiniAPIs的魔法檢測術

歡迎來到我們MiniAPIs魔法課程的第十一章!今天,我們將深入探討如何測試和除錯你的MiniAPIs應用。就像一個優秀的魔法師需要不斷練習和完善他的魔法一樣,一個出色的開發者也需要仔細測試和除錯他的程式碼。讓我們一起來學習如何使用這些強大的魔法檢測術吧!

編寫單元測試

單元測試是確保你的程式碼按預期工作的重要工具。在MiniAPIs中,我們可以使用xUnit、NUnit或MSTest等測試框架來編寫單元測試。

首先,讓我們為我們的待辦事項API建立一個測試專案:

dotnet new xunit -n TodoApi.Tests
dotnet add TodoApi.Tests reference TodoApi

現在,讓我們為我們的TodoService編寫一些單元測試:

using Xunit;
using Moq;
using TodoApi.Services;
using TodoApi.Models;
using Microsoft.EntityFrameworkCore;

namespace TodoApi.Tests
{
    public class TodoServiceTests
    {
        [Fact]
        public async Task GetAllTodos_ReturnsAllTodos()
        {
            // Arrange
            var mockSet = new Mock<DbSet<Todo>>();
            var mockContext = new Mock<TodoDbContext>();
            mockContext.Setup(m => m.Todos).Returns(mockSet.Object);

            var service = new TodoService(mockContext.Object);

            // Act
            var result = await service.GetAllTodosAsync();

            // Assert
            Assert.NotNull(result);
            mockSet.Verify(m => m.ToListAsync(It.IsAny<CancellationToken>()), Times.Once());
        }

        [Fact]
        public async Task CreateTodo_AddsTodoToDatabase()
        {
            // Arrange
            var mockSet = new Mock<DbSet<Todo>>();
            var mockContext = new Mock<TodoDbContext>();
            mockContext.Setup(m => m.Todos).Returns(mockSet.Object);

            var service = new TodoService(mockContext.Object);
            var todo = new Todo { Title = "Test Todo" };

            // Act
            await service.CreateTodoAsync(todo);

            // Assert
            mockSet.Verify(m => m.AddAsync(It.IsAny<Todo>(), It.IsAny<CancellationToken>()), Times.Once());
            mockContext.Verify(m => m.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once());
        }
    }
}

這些測試確保我們的TodoService正確地與資料庫互動。

編寫整合測試

整合測試檢查你的應用程式的不同部分是否能夠正確地協同工作。對於MiniAPIs,我們可以使用WebApplicationFactory來建立一個測試伺服器。

using Microsoft.AspNetCore.Mvc.Testing;
using System.Net.Http.Json;
using Xunit;

namespace TodoApi.Tests
{
    public class TodoApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
    {
        private readonly WebApplicationFactory<Program> _factory;

        public TodoApiIntegrationTests(WebApplicationFactory<Program> factory)
        {
            _factory = factory;
        }

        [Fact]
        public async Task GetTodos_ReturnsSuccessStatusCode()
        {
            // Arrange
            var client = _factory.CreateClient();

            // Act
            var response = await client.GetAsync("/api/v1/todos");

            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]
        public async Task CreateTodo_ReturnsCreatedStatusCode()
        {
            // Arrange
            var client = _factory.CreateClient();
            var todo = new Todo { Title = "Integration Test Todo" };

            // Act
            var response = await client.PostAsJsonAsync("/api/v1/todos", todo);

            // Assert
            Assert.Equal(System.Net.HttpStatusCode.Created, response.StatusCode);
        }
    }
}

這些測試確保我們的API端點正確響應請求。

使用除錯工具

.NET提供了強大的除錯工具,可以幫助你診斷和修復問題。

使用斷點

在Visual Studio或Visual Studio Code中,你可以透過點選程式碼行號左側來設定斷點。當程式執行到斷點時,它會暫停,讓你可以檢查變數的值和程式的狀態。

使用日誌

日誌是除錯的另一個重要工具。在MiniAPIs中,你可以使用內建的日誌記錄系統:

app.MapGet("/api/v1/todos", async (ILogger<Program> logger, TodoDbContext db) =>
{
    logger.LogInformation("Getting all todos");
    var todos = await db.Todos.ToListAsync();
    logger.LogInformation($"Retrieved {todos.Count} todos");
    return Results.Ok(todos);
});

你可以使用不同的日誌級別(如LogDebugLogWarningLogError等)來區分不同重要性的資訊。

使用異常處理

適當的異常處理可以幫助你更容易地診斷問題:

app.MapPost("/api/v1/todos", async (Todo todo, TodoDbContext db) =>
{
    try
    {
        db.Todos.Add(todo);
        await db.SaveChangesAsync();
        return Results.Created($"/api/v1/todos/{todo.Id}", todo);
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "Error occurred while creating a new todo");
        return Results.Problem("An error occurred while processing your request.");
    }
});

效能最佳化技巧

效能最佳化是開發過程中的一個重要方面。以下是一些最佳化MiniAPIs效能的技巧:

  1. 使用非同步程式設計:儘可能使用非同步方法,特別是在I/O操作中。

  2. 最佳化資料庫查詢:使用適當的索引,避免N+1查詢問題。

  3. 實現快取:對於頻繁訪問但不經常變化的資料,考慮使用快取。

  4. 使用壓縮:啟用響應壓縮可以減少傳輸的資料量。

  5. 最小化依賴注入的使用:雖然依賴注入很有用,但過度使用可能會影響效能。

  6. 使用適當的資料結構:選擇合適的資料結構可以大大提高效能。

實戰練習:最佳化和測試待辦事項API

讓我們對我們的待辦事項API進行一些最佳化,並新增一些測試:

  1. 首先,讓我們最佳化我們的TodoService
public class TodoService : ITodoService
{
    private readonly TodoDbContext _context;
    private readonly IMemoryCache _cache;
    private readonly ILogger<TodoService> _logger;

    public TodoService(TodoDbContext context, IMemoryCache cache, ILogger<TodoService> logger)
    {
        _context = context;
        _cache = cache;
        _logger = logger;
    }

    public async Task<IEnumerable<Todo>> GetAllTodosAsync()
    {
        return await _cache.GetOrCreateAsync("all_todos", async entry =>
        {
            entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(5));
            _logger.LogInformation("Fetching all todos from database");
            return await _context.Todos.ToListAsync();
        });
    }

    public async Task<Todo> GetTodoByIdAsync(int id)
    {
        return await _cache.GetOrCreateAsync($"todo_{id}", async entry =>
        {
            entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(5));
            _logger.LogInformation($"Fetching todo with id {id} from database");
            return await _context.Todos.FindAsync(id);
        });
    }

    public async Task<Todo> CreateTodoAsync(Todo todo)
    {
        _context.Todos.Add(todo);
        await _context.SaveChangesAsync();
        _cache.Remove("all_todos");
        _logger.LogInformation($"Created new todo with id {todo.Id}");
        return todo;
    }

    // 實現其他方法...
}
  1. 然後,讓我們為這個服務新增一些單元測試:
public class TodoServiceTests
{
    [Fact]
    public async Task GetAllTodos_ReturnsCachedResult()
    {
        // Arrange
        var mockContext = new Mock<TodoDbContext>();
        var mockCache = new Mock<IMemoryCache>();
        var mockLogger = new Mock<ILogger<TodoService>>();

        var cachedTodos = new List<Todo> { new Todo { Id = 1, Title = "Cached Todo" } };
        
        mockCache.Setup(c => c.TryGetValue("all_todos", out It.Ref<object>.IsAny))
            .Returns(true)
            .Callback(new OutDelegate<object>((string key, out object value) => value = cachedTodos));

        var service = new TodoService(mockContext.Object, mockCache.Object, mockLogger.Object);

        // Act
        var result = await service.GetAllTodosAsync();

        // Assert
        Assert.Equal(cachedTodos, result);
        mockContext.Verify(c => c.Todos, Times.Never);
    }

    [Fact]
    public async Task CreateTodo_InvalidatesCacheAndSavesToDatabase()
    {
        // Arrange
        var mockContext = new Mock<TodoDbContext>();
        var mockCache = new Mock<IMemoryCache>();
        var mockLogger = new Mock<ILogger<TodoService>>();

        var service = new TodoService(mockContext.Object, mockCache.Object, mockLogger.Object);
        var todo = new Todo { Title = "New Todo" };

        // Act
        await service.CreateTodoAsync(todo);

        // Assert
        mockCache.Verify(c => c.Remove("all_todos"), Times.Once);
        mockContext.Verify(c => c.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
    }
}
  1. 最後,讓我們新增一個整合測試:
public class TodoApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public TodoApiIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Fact]
    public async Task CreateAndGetTodo_ReturnsCreatedTodo()
    {
        // Arrange
        var client = _factory.CreateClient();
        var todo = new Todo { Title = "Integration Test Todo" };

        // Act
        var createResponse = await client.PostAsJsonAsync("/api/v1/todos", todo);
        createResponse.EnsureSuccessStatusCode();
        var createdTodo = await createResponse.Content.ReadFromJsonAsync<Todo>();

        var getResponse = await client.GetAsync($"/api/v1/todos/{createdTodo.Id}");
        getResponse.EnsureSuccessStatusCode();
        var retrievedTodo = await getResponse.Content.ReadFromJsonAsync<Todo>();

        // Assert
        Assert.NotNull(createdTodo);
        Assert.NotNull(retrievedTodo);
        Assert.Equal(createdTodo.Id, retrievedTodo.Id);
        Assert.Equal(createdTodo.Title, retrievedTodo.Title);
    }
}

透過這些最佳化和測試,我們的API現在更加健壯和高效:

  1. 我們使用了快取來減少資料庫查詢。
  2. 我們新增了詳細的日誌記錄,這將有助於除錯。
  3. 我們編寫了單元測試來確保我們的服務邏輯正確。
  4. 我們新增了整合測試來驗證我們的API端點是否按預期工作。

記住,測試和除錯是一個持續的過程。隨著你的API的發展,你應該不斷地新增新的測試,並使用除錯工具來診斷和修復問題。

在下一章中,我們將探討如何部署你的MiniAPIs應用。準備好了嗎?讓我們繼續我們的MiniAPIs魔法之旅!

12. 部署MiniAPIs應用:將魔法帶到現實世界

歡迎來到我們MiniAPIs魔法課程的第十二章!今天,我們將學習如何將你精心打造的MiniAPIs應用部署到現實世界中。就像一個魔法師需要在舞臺上展示他的魔法一樣,一個開發者也需要將他的應用部署到伺服器上,讓使用者能夠訪問。讓我們一起來學習如何將你的MiniAPIs魔法帶到更廣闊的舞臺上吧!

部署到本地伺服器

首先,讓我們看看如何將MiniAPIs應用部署到本地伺服器。

步驟1:釋出應用

在你的專案目錄中,執行以下命令:

dotnet publish -c Release -o ./publish

這將建立一個 publish 資料夾,其中包含了你的應用及其所有依賴項。

步驟2:配置IIS

  1. 在Windows伺服器上安裝IIS。
  2. 安裝 .NET Core Hosting Bundle。
  3. 在IIS中建立一個新的網站,並將其物理路徑指向你的 publish 資料夾。

步驟3:配置應用程式池

  1. 為你的應用建立一個新的應用程式池。
  2. 將應用程式池設定為"No Managed Code"。

步驟4:配置web.config

在你的 publish 資料夾中建立一個 web.config 檔案:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="dotnet" arguments=".\YourAppName.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess" />
    </system.webServer>
  </location>
</configuration>

確保將 YourAppName.dll 替換為你的應用的實際DLL名稱。

部署到雲平臺

現在,讓我們看看如何將MiniAPIs應用部署到一些流行的雲平臺。

部署到Azure App Service

  1. 在Visual Studio中,右鍵點選你的專案,選擇"Publish"。
  2. 選擇"Azure"作為目標。
  3. 選擇"Azure App Service (Windows)"。
  4. 建立一個新的App Service或選擇一個現有的。
  5. 點選"Publish"。

或者,你可以使用Azure CLI:

az webapp up --sku F1 --name <app-name> --os-type windows

部署到AWS Elastic Beanstalk

  1. 安裝AWS Toolkit for Visual Studio。
  2. 右鍵點選你的專案,選擇"Publish to AWS"。
  3. 選擇"AWS Elastic Beanstalk"。
  4. 建立一個新的環境或選擇一個現有的。
  5. 點選"Publish"。

部署到Heroku

  1. 建立一個 Procfile 檔案在你的專案根目錄:

    web: cd $HOME/heroku_output && dotnet YourAppName.dll --urls=http://+:$PORT
    
  2. 安裝Heroku CLI並登入。

  3. 建立一個新的Heroku應用:

    heroku create your-app-name
    
  4. 設定構建包:

    heroku buildpacks:set jincod/dotnetcore
    
  5. 部署你的應用:

    git push heroku main
    

持續整合和持續部署(CI/CD)

設定CI/CD管道可以自動化你的測試和部署過程。讓我們看看如何使用GitHub Actions來設定一個基本的CI/CD管道。

在你的專案根目錄建立一個 .github/workflows/ci-cd.yml 檔案:

name: CI/CD

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    - name: Setup .NET
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: '7.0.x'
    - name: Restore dependencies
      run: dotnet restore
    - name: Build
      run: dotnet build --no-restore
    - name: Test
      run: dotnet test --no-build --verbosity normal

  deploy:
    needs: build-and-test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v2
    - name: Setup .NET
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: '7.0.x'
    - name: Publish
      run: dotnet publish -c Release -o ./publish
    - name: Deploy to Azure Web App
      uses: azure/webapps-deploy@v2
      with:
        app-name: 'your-app-name'
        publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
        package: ./publish

這個工作流程會在每次推送到main分支時執行測試,如果測試透過,它會將應用部署到Azure Web App。

實戰練習:部署待辦事項API

讓我們將我們的待辦事項API部署到Azure App Service。

  1. 首先,確保你有一個Azure賬戶。如果沒有,可以建立一個免費賬戶。

  2. 在Visual Studio中,右鍵點選你的專案,選擇"Publish"。

  3. 選擇"Azure"作為目標。

  4. 選擇"Azure App Service (Windows)"。

  5. 點選"Create New"建立一個新的App Service。

  6. 填寫必要的資訊:

    • App Name: 選擇一個唯一的名稱,如 "your-name-todo-api"
    • Subscription: 選擇你的Azure訂閱
    • Resource Group: 建立一個新的或選擇現有的
    • Hosting Plan: 建立一個新的或選擇現有的(可以選擇免費層F1)
  7. 點選"Create"來建立App Service。

  8. 建立完成後,點選"Publish"來部署你的應用。

  9. 部署完成後,Visual Studio會開啟一個瀏覽器視窗,顯示你的API的URL。

  10. 使用Postman或任何API測試工具來測試你的API。例如,你可以傳送一個GET請求到 https://your-name-todo-api.azurewebsites.net/api/v1/todos 來獲取所有的待辦事項。

恭喜!你已經成功地將你的MiniAPIs應用部署到了雲端。現在,你的API可以被世界上任何地方的使用者訪問了。

記住,部署是一個持續的過程。隨著你的應用的發展,你可能需要更新你的部署策略,可能包括設定更復雜的CI/CD管道,實施藍綠部署或金絲雀釋出等高階策略。

在下一章中,我們將探討一些常見問題和解決方案,以及MiniAPIs開發中的最佳實踐。準備好了嗎?讓我們繼續我們的MiniAPIs魔法之旅!

13. 實踐專案:將MiniAPIs魔法付諸實踐

歡迎來到我們MiniAPIs魔法課程的第十三章!現在,你已經掌握了MiniAPIs的核心概念和高階特性,是時候將這些知識付諸實踐了。在這一章中,我們將透過三個實際的專案來鞏固你的技能,讓你真正成為一名MiniAPIs魔法大師。準備好開始這場魔法冒險了嗎?讓我們開始吧!

專案一:構建一個簡單的任務管理API

我們的第一個專案是一個任務管理API。這個API將允許使用者建立、讀取、更新和刪除任務。

步驟1:建立專案

首先,建立一個新的MiniAPIs專案:

dotnet new web -n TaskManagerApi
cd TaskManagerApi

步驟2:新增必要的包

dotnet add package Microsoft.EntityFrameworkCore.InMemory

步驟3:建立模型和資料上下文

建立一個 Task.cs 檔案:

public class Task
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public bool IsCompleted { get; set; }
    public DateTime DueDate { get; set; }
}

建立一個 TaskDbContext.cs 檔案:

using Microsoft.EntityFrameworkCore;

public class TaskDbContext : DbContext
{
    public TaskDbContext(DbContextOptions<TaskDbContext> options)
        : base(options) { }

    public DbSet<Task> Tasks { get; set; }
}

步驟4:配置服務和中介軟體

更新 Program.cs 檔案:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<TaskDbContext>(options =>
    options.UseInMemoryDatabase("TaskList"));

var app = builder.Build();

app.UseHttpsRedirection();

// API endpoints will be added here

app.Run();

步驟5:新增API端點

Program.cs 檔案中新增以下端點:

// Get all tasks
app.MapGet("/api/tasks", async (TaskDbContext db) =>
    await db.Tasks.ToListAsync());

// Get a specific task
app.MapGet("/api/tasks/{id}", async (int id, TaskDbContext db) =>
    await db.Tasks.FindAsync(id) is Task task
        ? Results.Ok(task)
        : Results.NotFound());

// Create a new task
app.MapPost("/api/tasks", async (Task task, TaskDbContext db) =>
{
    db.Tasks.Add(task);
    await db.SaveChangesAsync();
    return Results.Created($"/api/tasks/{task.Id}", task);
});

// Update a task
app.MapPut("/api/tasks/{id}", async (int id, Task inputTask, TaskDbContext db) =>
{
    var task = await db.Tasks.FindAsync(id);
    if (task is null) return Results.NotFound();

    task.Title = inputTask.Title;
    task.Description = inputTask.Description;
    task.IsCompleted = inputTask.IsCompleted;
    task.DueDate = inputTask.DueDate;

    await db.SaveChangesAsync();
    return Results.NoContent();
});

// Delete a task
app.MapDelete("/api/tasks/{id}", async (int id, TaskDbContext db) =>
{
    if (await db.Tasks.FindAsync(id) is Task task)
    {
        db.Tasks.Remove(task);
        await db.SaveChangesAsync();
        return Results.Ok(task);
    }

    return Results.NotFound();
});

現在,你有了一個功能完整的任務管理API!你可以使用Postman或任何其他API測試工具來測試這些端點。

專案二:構建一個使用者認證系統

我們的第二個專案將為我們的API新增使用者認證功能。我們將使用JWT(JSON Web Tokens)來實現這一點。

步驟1:新增必要的包

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package System.IdentityModel.Tokens.Jwt

步驟2:建立使用者模型

建立一個 User.cs 檔案:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string PasswordHash { get; set; }
}

步驟3:更新資料上下文

更新 TaskDbContext.cs

public class TaskDbContext : DbContext
{
    public TaskDbContext(DbContextOptions<TaskDbContext> options)
        : base(options) { }

    public DbSet<Task> Tasks { get; set; }
    public DbSet<User> Users { get; set; }
}

步驟4:配置JWT認證

Program.cs 中新增以下程式碼:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

// ... existing code ...

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        };
    });

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

// ... existing code ...

步驟5:新增使用者註冊和登入端點

Program.cs 中新增以下端點:

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using BCrypt.Net;

// Register a new user
app.MapPost("/api/register", async (User user, TaskDbContext db) =>
{
    var existingUser = await db.Users.FirstOrDefaultAsync(u => u.Username == user.Username);
    if (existingUser != null)
        return Results.BadRequest("Username already exists");

    user.PasswordHash = BCrypt.HashPassword(user.PasswordHash);
    db.Users.Add(user);
    await db.SaveChangesAsync();
    return Results.Created($"/api/users/{user.Id}", user);
});

// Login
app.MapPost("/api/login", async (LoginModel model, TaskDbContext db, IConfiguration config) =>
{
    var user = await db.Users.FirstOrDefaultAsync(u => u.Username == model.Username);
    if (user == null || !BCrypt.Verify(model.Password, user.PasswordHash))
        return Results.BadRequest("Invalid username or password");

    var token = GenerateJwtToken(user, config);
    return Results.Ok(new { token });
});

string GenerateJwtToken(User user, IConfiguration config)
{
    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]));
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

    var claims = new[]
    {
        new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
        new Claim(ClaimTypes.Name, user.Username)
    };

    var token = new JwtSecurityToken(
        issuer: config["Jwt:Issuer"],
        audience: config["Jwt:Audience"],
        claims: claims,
        expires: DateTime.Now.AddMinutes(15),
        signingCredentials: credentials);

    return new JwtSecurityTokenHandler().WriteToken(token);
}

public class LoginModel
{
    public string Username { get; set; }
    public string Password { get; set; }
}

步驟6:保護任務管理API

更新任務管理API的端點,新增 [Authorize] 屬性:

app.MapGet("/api/tasks", [Authorize] async (TaskDbContext db) =>
    await db.Tasks.ToListAsync());

// ... 對其他端點也做同樣的修改 ...

現在,你的API有了使用者認證系統!使用者需要先註冊,然後登入獲取JWT令牌,最後使用該令牌來訪問受保護的任務管理API。

專案三:構建一個部落格API

我們的第三個專案是一個部落格API。這個API將允許使用者建立、讀取、更新和刪除部落格文章,以及新增評論。

步驟1:建立模型

建立 BlogPost.csComment.cs 檔案:

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime CreatedAt { get; set; }
    public List<Comment> Comments { get; set; } = new List<Comment>();
}

public class Comment
{
    public int Id { get; set; }
    public string Content { get; set; }
    public DateTime CreatedAt { get; set; }
    public int BlogPostId { get; set; }
    public BlogPost BlogPost { get; set; }
}

步驟2:更新資料上下文

更新 TaskDbContext.cs

public class BlogDbContext : DbContext
{
    public BlogDbContext(DbContextOptions<BlogDbContext> options)
        : base(options) { }

    public DbSet<BlogPost> BlogPosts { get; set; }
    public DbSet<Comment> Comments { get; set; }
}

步驟3:新增API端點

Program.cs 中新增以下端點:

// Get all blog posts
app.MapGet("/api/posts", async (BlogDbContext db) =>
    await db.BlogPosts.Include(p => p.Comments).ToListAsync());

// Get a specific blog post
app.MapGet("/api/posts/{id}", async (int id, BlogDbContext db) =>
    await db.BlogPosts.Include(p => p.Comments).FirstOrDefaultAsync(p => p.Id == id) is BlogPost post
        ? Results.Ok(post)
        : Results.NotFound());

// Create a new blog post
app.MapPost("/api/posts", async (BlogPost post, BlogDbContext db) =>
{
    post.CreatedAt = DateTime.UtcNow;
    db.BlogPosts.Add(post);
    await db.SaveChangesAsync();
    return Results.Created($"/api/posts/{post.Id}", post);
});

// Update a blog post
app.MapPut("/api/posts/{id}", async (int id, BlogPost inputPost, BlogDbContext db) =>
{
    var post = await db.BlogPosts.FindAsync(id);
    if (post is null) return Results.NotFound();

    post.Title = inputPost.Title;
    post.Content = inputPost.Content;

    await db.SaveChangesAsync();
    return Results.NoContent();
});

// Delete a blog post
app.MapDelete("/api/posts/{id}", async (int id, BlogDbContext db) =>
{
    if (await db.BlogPosts.FindAsync(id) is BlogPost post)
    {
        db.BlogPosts.Remove(post);
        await db.SaveChangesAsync();
        return Results.Ok(post);
    }

    return Results.NotFound();
});

// Add a comment to a blog post
app.MapPost("/api/posts/{postId}/comments", async (int postId, Comment comment, BlogDbContext db) =>
{
    var post = await db.BlogPosts.FindAsync(postId);
    if (post is null) return Results.NotFound();

    comment.BlogPostId = postId;
    comment.CreatedAt = DateTime.UtcNow;
    db.Comments.Add(comment);
    await db.SaveChangesAsync();
    return Results.Created($"/api/posts/{postId}/comments/{comment.Id}", comment);
});

// Get all comments for a blog post
app.MapGet("/api/posts/{postId}/comments", async (int postId, BlogDbContext db) =>
{
    var post = await db.BlogPosts.FindAsync(postId);
    if (post is null) return Results.NotFound();

    var comments = await db.Comments
        .Where(c => c.BlogPostId == postId)
        .ToListAsync();
    return Results.Ok(comments);
});

// Delete a comment
app.MapDelete("/api/comments/{id}", async (int id, BlogDbContext db) =>
{
    if (await db.Comments.FindAsync(id) is Comment comment)
    {
        db.Comments.Remove(comment);
        await db.SaveChangesAsync();
        return Results.Ok(comment);
    }

    return Results.NotFound();
});

現在,你有了一個功能完整的部落格API!這個API允許使用者建立、讀取、更新和刪除部落格文章,以及新增和刪除評論。

步驟4:新增分頁功能

為了最佳化效能,我們可以為獲取部落格文章的端點新增分頁功能:

// Get all blog posts with pagination
app.MapGet("/api/posts", async (int page = 1, int pageSize = 10, BlogDbContext db) =>
{
    var totalPosts = await db.BlogPosts.CountAsync();
    var totalPages = (int)Math.Ceiling(totalPosts / (double)pageSize);

    var posts = await db.BlogPosts
        .Include(p => p.Comments)
        .Skip((page - 1) * pageSize)
        .Take(pageSize)
        .ToListAsync();

    return Results.Ok(new 
    {
        Posts = posts,
        CurrentPage = page,
        TotalPages = totalPages,
        PageSize = pageSize,
        TotalPosts = totalPosts
    });
});

步驟5:新增搜尋功能

我們還可以新增一個搜尋功能,允許使用者根據標題或內容搜尋部落格文章:

// Search blog posts
app.MapGet("/api/posts/search", async (string query, BlogDbContext db) =>
{
    var posts = await db.BlogPosts
        .Where(p => p.Title.Contains(query) || p.Content.Contains(query))
        .Include(p => p.Comments)
        .ToListAsync();

    return Results.Ok(posts);
});

步驟6:新增標籤功能

讓我們為部落格文章新增標籤功能:

首先,建立一個 Tag 模型:

public class Tag
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<BlogPost> BlogPosts { get; set; } = new List<BlogPost>();
}

然後,更新 BlogPost 模型:

public class BlogPost
{
    // ... existing properties ...
    public List<Tag> Tags { get; set; } = new List<Tag>();
}

更新資料上下文:

public class BlogDbContext : DbContext
{
    // ... existing DbSet properties ...
    public DbSet<Tag> Tags { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<BlogPost>()
            .HasMany(p => p.Tags)
            .WithMany(t => t.BlogPosts)
            .UsingEntity(j => j.ToTable("BlogPostTags"));
    }
}

新增標籤相關的端點:

// Add tags to a blog post
app.MapPost("/api/posts/{postId}/tags", async (int postId, List<string> tagNames, BlogDbContext db) =>
{
    var post = await db.BlogPosts.FindAsync(postId);
    if (post is null) return Results.NotFound();

    foreach (var tagName in tagNames)
    {
        var tag = await db.Tags.FirstOrDefaultAsync(t => t.Name == tagName);
        if (tag is null)
        {
            tag = new Tag { Name = tagName };
            db.Tags.Add(tag);
        }
        post.Tags.Add(tag);
    }

    await db.SaveChangesAsync();
    return Results.Ok(post.Tags);
});

// Get all tags
app.MapGet("/api/tags", async (BlogDbContext db) =>
    await db.Tags.ToListAsync());

// Get posts by tag
app.MapGet("/api/posts/bytag/{tagName}", async (string tagName, BlogDbContext db) =>
{
    var posts = await db.BlogPosts
        .Where(p => p.Tags.Any(t => t.Name == tagName))
        .Include(p => p.Comments)
        .Include(p => p.Tags)
        .ToListAsync();

    return Results.Ok(posts);
});

這個增強版的部落格API現在包含了以下功能:

  1. 基本的CRUD操作用於部落格文章和評論
  2. 分頁功能,以便更有效地處理大量部落格文章
  3. 搜尋功能,允許使用者根據標題或內容搜尋部落格文章
  4. 標籤功能,允許為部落格文章新增標籤,並根據標籤檢索文章

這個專案展示瞭如何使用MiniAPIs構建一個相對複雜的API,包括關聯資料(部落格文章和評論)、多對多關係(部落格文章和標籤)以及更高階的查詢操作。

透過完成這三個專案,你已經獲得了使用MiniAPIs構建各種型別API的實際經驗。你已經處理了資料持久化、身份驗證、關聯資料、分頁、搜尋和標籤等常見需求。這些技能將使你能夠處理各種實際的API開發場景。

記住,實踐是掌握任何技術的關鍵。繼續練習,嘗試為這些專案新增新功能,或者開始你自己的專案。隨著你的經驗增加,你將成為一個真正的MiniAPIs魔法大師!

在下一章中,我們將討論一些常見問題和它們的解決方案,以及MiniAPIs開發中的最佳實踐。準備好了嗎?讓我們繼續我們的MiniAPIs魔法之旅!

14. 常見問題和解決方案:MiniAPIs的魔法疑難解答

歡迎來到我們MiniAPIs魔法課程的第十四章!即使是最熟練的魔法師也會遇到一些棘手的問題。在這一章中,我們將探討使用MiniAPIs時可能遇到的一些常見問題,以及如何解決這些問題。我們還將討論一些最佳實踐,以幫助你避免這些問題。讓我們開始我們的MiniAPIs疑難解答之旅吧!

常見錯誤及其解決方法

1. 路由衝突

問題:當你有兩個或多個端點使用相同的HTTP方法和路由模板時,可能會發生路由衝突。

解決方案

  • 確保每個端點的路由是唯一的。
  • 使用不同的HTTP方法來區分相似的路由。
  • 使用路由約束來進一步區分路由。

例如:

app.MapGet("/api/items/{id:int}", (int id) => $"Get item by ID: {id}");
app.MapGet("/api/items/{name}", (string name) => $"Get item by name: {name}");

2. 跨域資源共享(CORS)問題

問題:當前端應用嘗試從不同域訪問你的API時,可能會遇到CORS錯誤。

解決方案

  • 在你的應用中配置CORS。
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowSpecificOrigin",
        builder => builder.WithOrigins("http://example.com")
                          .AllowAnyHeader()
                          .AllowAnyMethod());
});

var app = builder.Build();

app.UseCors("AllowSpecificOrigin");

// ... 其他中介軟體和路由配置

3. 資料庫連線問題

問題:無法連線到資料庫或執行資料庫操作。

解決方案

  • 檢查連線字串是否正確。
  • 確保資料庫伺服器正在執行並且可以訪問。
  • 使用異常處理來捕獲和記錄資料庫錯誤。
app.MapGet("/api/items", async (MyDbContext db) =>
{
    try
    {
        return await db.Items.ToListAsync();
    }
    catch (Exception ex)
    {
        // 記錄錯誤
        Console.WriteLine($"Database error: {ex.Message}");
        return Results.Problem("An error occurred while fetching data.");
    }
});

4. 身份驗證和授權問題

問題:使用者無法正確地進行身份驗證或訪問受保護的資源。

解決方案

  • 確保正確配置了身份驗證中介軟體。
  • 檢查JWT令牌的簽名和宣告是否正確。
  • 使用適當的授權屬性來保護端點。
app.MapGet("/api/protected", [Authorize] (ClaimsPrincipal user) =>
{
    var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    return $"Hello, user {userId}!";
});

5. 模型繫結問題

問題:API無法正確繫結複雜的請求體或查詢引數。

解決方案

  • 確保請求體或查詢引數與你的模型結構匹配。
  • 使用自定義模型繫結器來處理複雜的繫結場景。
app.MapPost("/api/complex", (ComplexModel model) =>
{
    if (!ModelState.IsValid)
    {
        return Results.ValidationProblem(ModelState);
    }
    // 處理模型...
});

public class ComplexModel
{
    public string Name { get; set; }
    public int Age { get; set; }
    public List<string> Tags { get; set; }
}

MiniAPIs開發中的最佳實踐

  1. 使用依賴注入:利用ASP.NET Core的依賴注入系統來管理服務的生命週期和依賴關係。
builder.Services.AddScoped<IMyService, MyService>();
  1. 實現適當的錯誤處理:使用全域性異常處理中介軟體來捕獲和處理未處理的異常。
app.Use(async (context, next) =>
{
    try
    {
        await next(context);
    }
    catch (Exception ex)
    {
        context.Response.StatusCode = 500;
        await context.Response.WriteAsJsonAsync(new { error = "An unexpected error occurred." });
    }
});
  1. 使用非同步程式設計:儘可能使用非同步方法來提高應用程式的效能和可伸縮性。
app.MapGet("/api/items", async (MyDbContext db) => await db.Items.ToListAsync());
  1. 實現適當的日誌記錄:使用內建的日誌記錄系統來記錄重要的事件和錯誤。
app.MapGet("/api/items", (ILogger<Program> logger) =>
{
    logger.LogInformation("Fetching all items");
    // ... 獲取專案的邏輯
});
  1. 使用模型驗證:利用資料註解或FluentValidation來驗證輸入資料。
app.MapPost("/api/items", async (Item item, IValidator<Item> validator) =>
{
    var validationResult = await validator.ValidateAsync(item);
    if (!validationResult.IsValid)
    {
        return Results.ValidationProblem(validationResult.ToDictionary());
    }
    // ... 處理有效專案的邏輯
});
  1. 使用適當的HTTP狀態碼:確保你的API返回正確的HTTP狀態碼。
app.MapGet("/api/items/{id}", (int id) =>
{
    var item = GetItemById(id);
    if (item == null)
        return Results.NotFound();
    return Results.Ok(item);
});
  1. 實現API版本控制:從一開始就考慮API版本控制,以便將來可以輕鬆地進行更改。
app.MapGet("/api/v1/items", () => "Version 1 items");
app.MapGet("/api/v2/items", () => "Version 2 items");
  1. 使用適當的命名約定:為你的端點、模型和服務使用清晰和一致的命名約定。

  2. 實現快取:對於頻繁訪問但不經常更改的資料,考慮實現快取以提高效能。

app.MapGet("/api/items", async (IMemoryCache cache) =>
{
    return await cache.GetOrCreateAsync("all_items", async entry =>
    {
        entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(10));
        // ... 從資料庫獲取專案的邏輯
    });
});
  1. 使用健康檢查:實現健康檢查端點以監控你的API的健康狀況。
builder.Services.AddHealthChecks();

// ...

app.MapHealthChecks("/health");

透過遵循這些最佳實踐並瞭解如何解決常見問題,你將能夠構建更加健壯、高效和可維護的MiniAPIs應用程式。記住,成為一個真正的MiniAPIs魔法大師需要時間和實踐。繼續練習,不斷學習,你會發現自己能夠輕鬆應對各種API開發挑戰。

在下一章中,我們將探討MiniAPIs的未來發展方向,以及如何持續提升你的MiniAPIs技能。準備好了嗎?讓我們繼續我們的MiniAPIs魔法之旅!

15. 資源和社群:MiniAPIs的魔法圈子

歡迎來到我們MiniAPIs魔法課程的最後一章!就像每個優秀的魔法師都需要一個支援的社群一樣,作為一個MiniAPIs開發者,你也需要知道在哪裡可以找到資源和支援。在這一章中,我們將探討一些valuable的資源,社群論壇,以及進一步學習的推薦材料。讓我們一起來探索MiniAPIs的魔法圈子吧!

官方文件和資源

  1. Microsoft官方文件
    這是你的首選資源。Microsoft提供了全面且不斷更新的MiniAPIs文件。
    https://docs.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis

  2. ASP.NET Core GitHub倉庫
    這裡你可以看到最新的開發進展,報告問題,甚至貢獻程式碼。
    https://github.com/dotnet/aspnetcore

  3. .NET YouTube頻道
    Microsoft的官方.NET YouTube頻道經常釋出關於新特性和最佳實踐的影片。
    https://www.youtube.com/dotnet

  4. Microsoft Learn
    這個平臺提供了許多免費的互動式學習路徑和模組。
    https://docs.microsoft.com/en-us/learn/

社群論壇和討論組

  1. Stack Overflow
    這是開發者提問和回答問題的最popular平臺之一。使用"asp.net-core"和"minimal-api"標籤來查詢相關問題。
    https://stackoverflow.com/questions/tagged/asp.net-core+minimal-api

  2. Reddit的r/dotnet和r/csharp社群
    這些subreddits是討論.NET相關話題的活躍社群。
    https://www.reddit.com/r/dotnet/
    https://www.reddit.com/r/csharp/

  3. ASP.NET Core Gitter聊天室
    這是一個實時聊天平臺,你可以在這裡與其他開發者討論ASP.NET Core相關的話題。
    https://gitter.im/aspnet/Home

  4. Discord的.NET社群
    Discord上有許多活躍的.NET開發者社群。

  5. Microsoft Tech Community
    這是Microsoft官方支援的社群平臺,你可以在這裡找到許多關於.NET和ASP.NET Core的討論。
    https://techcommunity.microsoft.com/t5/net/ct-p/dotnet

部落格和新聞源

  1. Scott Hanselman的部落格
    Scott是Microsoft的首席專案經理,他的部落格經常包含有關ASP.NET Core的深入文章。
    https://www.hanselman.com/blog/

  2. Andrew Lock的部落格
    Andrew的部落格專注於ASP.NET Core,包含許多深入的技術文章。
    https://andrewlock.net/

  3. .NET Blog
    這是Microsoft的官方.NET部落格,經常釋出新特性和更新的公告。
    https://devblogs.microsoft.com/dotnet/

  4. C# Digest
    這是一個weekly newsletter,彙總了C#和.NET社群的最新新聞和文章。
    https://csharpdigest.net/

書籍和線上課程

  1. "ASP.NET Core in Action" by Andrew Lock
    這本書深入探討了ASP.NET Core,包括MiniAPIs。

  2. Pluralsight的ASP.NET Core課程
    Pluralsight提供了許多高質量的ASP.NET Core影片課程。

  3. Udemy上的ASP.NET Core MiniAPIs課程
    Udemy上有許多關於MiniAPIs的實踐課程。

  4. LinkedIn Learning的.NET課程
    LinkedIn Learning(前Lynda.com)提供了許多.NET和ASP.NET Core的課程。

工具和擴充套件

  1. Visual Studio
    Microsoft的主力IDE,對.NET開發提供了excellent支援。

  2. Visual Studio Code
    一個輕量級但功能強大的編輯器,配合C#擴充套件可以很好地支援MiniAPIs開發。

  3. JetBrains Rider
    另一個popular的.NET IDE,提供了許多智慧功能。

  4. Postman
    一個強大的API測試工具,對開發和測試MiniAPIs非常有用。

  5. Swagger/OpenAPI
    用於API文件和測試的工具,可以很容易地整合到MiniAPIs專案中。

進一步學習的建議

  1. 深入學習C#
    MiniAPIs建立在C#之上,深入理解C#語言會讓你成為更好的MiniAPIs開發者。

  2. 學習Entity Framework Core
    作為.NET生態系統中最popular的ORM,EF Core經常與MiniAPIs一起使用。

  3. 探索設計模式
    瞭解常見的設計模式可以幫助你設計更好的API結構。

  4. 學習RESTful API設計原則
    雖然MiniAPIs提供了靈活性,但遵循RESTful原則可以讓你的API更加一致和易用。

  5. 關注效能最佳化
    學習如何最佳化你的MiniAPIs應用可以讓你的API更快、更高效。

  6. 探索微服務架構
    MiniAPIs非常適合構建微服務,瞭解微服務架構可以開啟新的可能性。

記住,成為一個MiniAPIs魔法大師是一個持續學習的過程。技術世界總是在變化,保持好奇心和學習的熱情是關鍵。利用這些資源,參與社群討論,不斷實踐和實驗。

你已經完成了我們的MiniAPIs魔法課程!但這只是你的魔法之旅的開始。繼續探索,繼續創造,用你的MiniAPIs魔法為世界帶來驚喜吧!

祝你在MiniAPIs的魔法世界中玩得開心,創造出令人驚歎的API!如果你有任何問題,記得社群和這些資源都是你的後盾。現在,去施展你的MiniAPIs魔法吧!

相關文章