自從發現 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 存資料等技巧,拼成一個範例方便日後參考。

背景知識補充:

  1. 內嵌 .html/.css/.js 靜態檔整成單一 EXE
  2. JavaScript 快顯通知套件 - NOTY
  3. EF Core 與 SQLite
  4. 使用 TypedResults 限定回傳型別

在示範專案中,原本 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 建議的四項改善包含:

  1. 依據功能或性質分類將 Program.cs 中的 .MapGet()、.MapPost() 移到專屬型別,改用 app.RegisterXXXEndPoints() 註冊該類別 API。
  2. 使用 TypedResults 取代 Reulsts,實現編譯期間的回傳型別檢查。
  3. 用 MapGroup() 將 API 分群,在群組設定 CORS、身分認證等可一次套用該群所有 API。
  4. 將 .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

Be the first to post a comment

Post a comment