四個小技巧讓 ASP.NET Core Minimal API 更有條理好維護
1 | 3,027 |
自從發現 ASP.NET Core Minimal API,我所有用到 ASP.NET Web 的小專案(小於一個人月)清一色都是用 Minimal API 寫,省去建 Controller,另開 MVC View 的麻煩,加上 UI 走 HTML + Vue.js 輕前端,小專案往往三五個檔案寫完網站,符合我的 KISS 極簡美學。
不過,當 Min API 再複雜一點,所有邏輯都塞在 Program.cs 寫起來開始有違和感。看到保哥分享一篇好文章 - Debugger Lady (典故), Tess, 分享如何讓 Minimal API 程式碼更有條理的四則小技巧 - Organizing ASP.NET Core Minimal APIs,改動幅度不大,但能有效提升程式可讀性,降低維護複雜度,深得我心。
閱畢,花了點時間整理成展示專案加強印象,順便納入我常用的單檔部署、JavaScript 快顯通知、SQLite EF Core 存資料等技巧,拼成一個範例方便日後參考。
背景知識補充:
在示範專案中,原本 Minimal API 的 Program.cs 長這樣:
using System.ComponentModel.DataAnnotations;
using System.Xml.Linq;
using Microsoft.EntityFrameworkCore;
using MinApiDemo;
var builder = WebApplication.CreateBuilder(args);
var sqliteFileName = "bookmark.sqlite";
if (File.Exists(sqliteFileName)) File.Delete(sqliteFileName);
var cs = $"Data Source={sqliteFileName}";
builder.Services.AddDbContext<AppDbContext>(options => {
options.UseSqlite(cs);
});
var app = builder.Build();
// create scope to create dbcontext
using (var scope = app.Services.CreateScope()) {
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.EnsureCreated();
if (!db.Bookmarks.Any()) {
db.Bookmarks.AddRange(new[] {
new Bookmark { Title = "Google", Url = "https://www.google.com" },
new Bookmark { Title = "Facebook", Url = "https://www.facebook.com" },
new Bookmark { Title = "YouTube", Url = "https://www.youtube.com" }
});
db.SaveChanges();
}
}
app.UseFileServer(new FileServerOptions {
RequestPath = "",
FileProvider = new Microsoft.Extensions.FileProviders
.ManifestEmbeddedFileProvider(
typeof(Program).Assembly, "ui"
)
});
app.MapGet("/bookmarks/list", async (AppDbContext db) => {
return Results.Json(await db.Bookmarks.ToListAsync());
});
app.MapPost("/bookmarks/add", async (AppDbContext db, Bookmark bookmark) => {
var results = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(bookmark, new ValidationContext(bookmark), results, true);
if (!isValid) return Results.BadRequest(results);
db.Bookmarks.Add(bookmark);
await db.SaveChangesAsync();
return Results.Json(bookmark);
});
app.MapPost("/bookmarks/remove/{id}", async (AppDbContext db, int id) => {
var bookmark = await db.Bookmarks.FindAsync(id);
if (bookmark == null) return Results.NotFound();
db.Bookmarks.Remove(bookmark);
await db.SaveChangesAsync();
return Results.Ok("OK");
});
app.MapGet("/export/json", async (AppDbContext db) => {
var bookmarks = await db.Bookmarks.ToListAsync();
return Results.Json(bookmarks);
});
app.MapGet("/export/xml", async (AppDbContext db) => {
var bookmarks = await db.Bookmarks.ToListAsync();
var xd = XDocument.Parse("<bookmarks></bookmarks>");
var root = xd.Root!;
foreach (var bookmark in bookmarks) {
var xe = new XElement("bookmark");
xe.Add(new XElement("id", bookmark.Id));
xe.Add(new XElement("title", bookmark.Title));
xe.Add(new XElement("url", bookmark.Url));
root.Add(xe);
}
return Results.Content(xd.ToString(), "application/xml");
});
app.Run();
它是個簡單的 URL 書籤維護,前端則是用 Vue.js 簡單搭個展示 UI:
而 Tess 建議的四項改善包含:
- 依據功能或性質分類將 Program.cs 中的 .MapGet()、.MapPost() 移到專屬型別,改用 app.RegisterXXXEndPoints() 註冊該類別 API。
- 使用 TypedResults 取代 Reulsts,實現編譯期間的回傳型別檢查。
- 用 MapGroup() 將 API 分群,在群組設定 CORS、身分認證等可一次套用該群所有 API。
- 將 .MapGet()/.MapPost() 邏輯移為型別獨立方法,方便查找維護。
順便分享我怎麼請 Copilot 幫我重構:
薑!薑!薑!薑~~~
或者用 Chat 功能跟 Copilot 許願也行:
重構完 Program.cs 原本一堆 MapGet()/MapPost() 程式碼改成兩行,清爽多了:
app.RegisterCrudEndPoints();
app.RegisterExportApiEndPoints();
而每個 API 寫成獨立方法,尋找跟管理也會方便很多:
展示專案我放上 Github,有需要的同學請自行下載參考。
題外話,常被問到:面對網站需求如何決定該用 MVC/Razor Page 還是用 Minimal API ?坦白說,我沒有量化標準但有個心法:這差不多像是「該騎機車去還是要開車」的抉擇,關鍵其實是「看是要去哪裡」,靠直覺就會有答案,就算不一定是對的,選錯只差在時間早晚及成本,最終都會到目的地。而且換架構只需重構調整不必砍掉重練,代價比折返回家換交通工具低多了,故做這個決定不該比選騎車或開車困難,沒什麼好糾結的,憑直覺動手再說。若直覺是錯了就當繳學費,也是種成長。
This article introduces small tips for organizing and maintaining code more effectively when developing more complex Minimal APIs.
Comments
# by 小黑
讚嘆黑大