⚡ Minimal API
📌 什麼是 Minimal API?
Minimal API 是 ASP.NET Core 6 引入的一種輕量級 API 開發方式,不需要 Controller、不需要一堆檔案,直接在 Program.cs 裡就能定義 API 端點。
想像你經營兩種餐廳:
- Controller-based API 像是大型連鎖餐廳——有經理(Controller)、服務生(Action Method)、菜單系統(Routing),分工明確但架構龐大
- Minimal API 像是路邊攤——老闆一個人搞定點餐和出餐,快速、簡單、直接
如果你用過 Node.js 的 Express.js,Minimal API 的風格會讓你感到非常熟悉!
🚀 基本用法:MapGet / MapPost / MapPut / MapDelete
// Program.cs - 這就是你的整個 API!
var builder = WebApplication.CreateBuilder(args); // 建立應用程式建構器
var app = builder.Build(); // 建構應用程式
// GET:取得資料(像點菜單上的餐點)
app.MapGet("/api/hello", () => "你好,世界!"); // 最簡單的 GET 端點
// GET:取得所有商品
app.MapGet("/api/products", () =>
{
// 回傳商品清單(實際上會從資料庫取得)
var products = new[]
{
new { Id = 1, Name = "筆電", Price = 30000 }, // 第一個商品
new { Id = 2, Name = "滑鼠", Price = 500 }, // 第二個商品
new { Id = 3, Name = "鍵盤", Price = 2000 } // 第三個商品
};
return Results.Ok(products); // 回傳 200 OK 和商品清單
});
// GET:根據 ID 取得單一商品
app.MapGet("/api/products/{id}", (int id) =>
{
// 用 id 去查找商品(這裡用假資料示範)
if (id == 1) // 如果找到了
return Results.Ok(new { Id = 1, Name = "筆電", Price = 30000 }); // 回傳商品
return Results.NotFound(new { Message = $"找不到 ID 為 {id} 的商品" }); // 回傳 404
});
// POST:建立新資料(像填寫點餐單送到廚房)
app.MapPost("/api/products", (Product product) =>
{
// product 參數會自動從 Request Body 的 JSON 反序列化
Console.WriteLine($"收到新商品:{product.Name}"); // 印出商品名稱
return Results.Created($"/api/products/{product.Id}", product); // 回傳 201 Created
});
// PUT:更新資料(像修改已經送出的訂單)
app.MapPut("/api/products/{id}", (int id, Product product) =>
{
// id 從路由參數來,product 從 body 來
Console.WriteLine($"更新商品 {id}:{product.Name}"); // 印出更新資訊
return Results.NoContent(); // 回傳 204 No Content(更新成功,不需要回傳內容)
});
// DELETE:刪除資料(像取消訂單)
app.MapDelete("/api/products/{id}", (int id) =>
{
Console.WriteLine($"刪除商品 {id}"); // 印出刪除資訊
return Results.NoContent(); // 回傳 204 No Content
});
app.Run(); // 啟動應用程式
🔗 參數繫結:資料從哪裡來?
// [FromQuery]:從 URL 的查詢字串取得(像在網址列輸入搜尋條件)
// 請求:GET /api/search?keyword=筆電&page=1
app.MapGet("/api/search", ([FromQuery] string keyword, [FromQuery] int page) =>
{
// keyword = "筆電",page = 1(自動從 URL 取得)
return Results.Ok(new { Keyword = keyword, Page = page }); // 回傳搜尋條件
});
// [FromRoute]:從路由取得(像從地址中取出門牌號碼)
// 請求:GET /api/users/42
app.MapGet("/api/users/{userId}", ([FromRoute] int userId) =>
{
return Results.Ok(new { UserId = userId }); // 回傳使用者 ID
});
// [FromBody]:從請求主體取得(像打開包裹取出裡面的東西)
// 請求:POST /api/orders,Body 是 JSON
app.MapPost("/api/orders", ([FromBody] Order order) =>
{
// order 物件會自動從 JSON 反序列化
return Results.Created($"/api/orders/{order.Id}", order); // 回傳建立結果
});
// [FromHeader]:從 HTTP 標頭取得(像看信封上的寄件人資訊)
app.MapGet("/api/protected", ([FromHeader(Name = "X-Api-Key")] string apiKey) =>
{
if (apiKey != "my-secret-key") // 驗證 API Key
return Results.Unauthorized(); // 未授權
return Results.Ok("歡迎!"); // 授權成功
});
📦 使用 MapGroup 分組
// MapGroup 讓你把相關的 API 端點分組(像把同一類的菜放在菜單的同一頁)
var productGroup = app.MapGroup("/api/products"); // 建立 /api/products 分組
// 以下所有路由都會自動加上 /api/products 前綴
productGroup.MapGet("/", () => Results.Ok("取得所有商品")); // GET /api/products/
productGroup.MapGet("/{id}", (int id) => Results.Ok($"取得商品 {id}")); // GET /api/products/{id}
productGroup.MapPost("/", (Product p) => Results.Created($"/api/products/{p.Id}", p)); // POST /api/products/
productGroup.MapDelete("/{id}", (int id) => Results.NoContent()); // DELETE /api/products/{id}
// 巢狀分組(像菜單裡的子分類)
var adminGroup = app.MapGroup("/api/admin") // 管理員 API 分組
.RequireAuthorization(); // 這個分組下的所有端點都需要授權
adminGroup.MapGet("/users", () => Results.Ok("管理員:取得所有使用者")); // 需要授權才能存取
adminGroup.MapDelete("/users/{id}", (int id) => Results.NoContent()); // 需要授權才能刪除
🔧 Minimal API 的 Filters
// 端點篩選器(像餐廳門口的安檢,進去前先檢查一下)
app.MapGet("/api/items", () => Results.Ok("通過檢查!"))
.AddEndpointFilter(async (context, next) =>
{
// 在端點執行「之前」做的事
Console.WriteLine("進入端點前...\n"); // 記錄日誌
var result = await next(context); // 執行端點處理(像放行讓客人進去)
// 在端點執行「之後」做的事
Console.WriteLine("離開端點後...\n"); // 記錄日誌
return result; // 回傳結果
});
// 自訂驗證篩選器(像門口的保鏢,檢查你的證件)
app.MapPost("/api/items", (Item item) => Results.Ok(item))
.AddEndpointFilter(async (context, next) =>
{
var item = context.GetArgument<Item>(0); // 取得第一個參數
if (string.IsNullOrEmpty(item.Name)) // 如果名稱為空
{
return Results.BadRequest("商品名稱不可為空"); // 回傳 400 錯誤
}
return await next(context); // 驗證通過,繼續執行
});
📊 Minimal API vs Controller-based API 比較
| 項目 | Minimal API | Controller-based API |
|---|---|---|
| 程式碼量 | 少,適合小型 API | 多,但結構清晰 |
| 學習曲線 | 低,快速上手 | 較高,需理解 MVC 模式 |
| 檔案結構 | 可以全部寫在 Program.cs | 需要 Controller 資料夾和檔案 |
| 適用場景 | 微服務、小型 API、原型 | 大型企業應用、複雜 API |
| 模型驗證 | 需手動或用 Filter | 內建 [ApiController] 自動驗證 |
| Swagger | 支援,但需額外設定 | 自動整合 |
| 可測試性 | 可測試,但需要技巧 | 容易透過 DI 測試 |
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:沒有做輸入驗證
// ❌ 錯誤寫法:直接信任使用者輸入(像不檢查就讓所有人進門)
app.MapPost("/api/users", (User user) =>
{
// 沒有驗證 user 的內容就直接存入資料庫
db.Users.Add(user); // 如果 user.Name 是 null 呢?如果 email 格式不對呢?
db.SaveChanges(); // 存入垃圾資料!
return Results.Created($"/api/users/{user.Id}", user); // 回傳
});
// ✅ 正確寫法:先驗證再處理
app.MapPost("/api/users", (User user) =>
{
// 驗證必填欄位
if (string.IsNullOrWhiteSpace(user.Name)) // 名稱不可為空
return Results.BadRequest(new { Error = "名稱為必填" }); // 回傳 400
if (string.IsNullOrWhiteSpace(user.Email)) // Email 不可為空
return Results.BadRequest(new { Error = "Email 為必填" }); // 回傳 400
// 驗證通過才存入
db.Users.Add(user); // 存入資料庫
db.SaveChanges(); // 儲存變更
return Results.Created($"/api/users/{user.Id}", user); // 回傳 201
});
解釋: 不驗證輸入就像不鎖門就出門——遲早會出問題。使用者可能送來空值、超長字串、或惡意內容,永遠不要信任來自外部的資料。
❌ 錯誤 2:沒有處理例外
// ❌ 錯誤寫法:沒有 try-catch(像在高速公路上不繫安全帶)
app.MapGet("/api/data/{id}", (int id) =>
{
var data = db.Items.Find(id); // 如果資料庫連線失敗?
return Results.Ok(data); // data 可能是 null!
});
// ✅ 正確寫法:處理各種可能的錯誤情況
app.MapGet("/api/data/{id}", (int id) =>
{
try
{
var data = db.Items.Find(id); // 嘗試查找資料
if (data == null) // 資料不存在
return Results.NotFound(new { Error = $"找不到 ID={id} 的資料" }); // 404
return Results.Ok(data); // 200 OK
}
catch (Exception ex) // 捕捉所有例外
{
return Results.Problem($"伺服器錯誤:{ex.Message}"); // 回傳 500
}
});
解釋: API 是對外的窗口,任何未處理的例外都會導致回傳 500 錯誤,還可能洩漏程式內部資訊。就像餐廳廚房失火了,不能直接讓客人看到火焰,要先處理好再告知客人「抱歉,暫時無法供餐」。