iT邦幫忙

2025 iThome 鐵人賽

DAY 29
1

Middleware 執行鏈與 DI 生命週期

執行鏈模型 Use / Map / Run

  • Use:建立下一個委派 next,可在執行前後加入邏輯,包含前置與後置處理。
  • Map/MapWhen:用於分支路由,僅在符合條件時執行對應分支。
  • Run:終止管線,無 next 參數,通常置於管線末端或分支結尾。

範例(順序決定行為)

app.Use(async (ctx, next) => { /* 前置邏輯 */ await next(); /* 後置邏輯 */ });

app.Map("/health", branch =>
{
    branch.Run(async ctx => await ctx.Response.WriteAsync("OK"));
});

app.Run(async ctx => await ctx.Response.WriteAsync("Hello"));

建議順序(概念性)

  • 例外處理與觀察性 → HTTPS 與 HSTS → 靜態檔案 → 路由與端點 → CORS → 驗證與授權 → 自訂中介軟體 → 端點終結
  • Minimal API 的 Map 方法會註冊端點;驗證、授權與 CORS 需在端點執行前設定。

自訂 Middleware 寫法與作用域注意

慣用 Middleware:啟動時建立單一實例

  • 避免在建構子注入 Scoped 服務(因實例長期存在)。
  • 若需使用 Scoped 服務,改由 Invoke 或 InvokeAsync 方法參數注入(每次請求會提供新實例)。
public sealed class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimingMiddleware> _log; // 可建構子注入(非 Scoped)

    public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> log)
    { _next = next; _log = log; }

    public async Task InvokeAsync(HttpContext ctx, MyDbContext db) // Scoped 服務透過參數注入
    {
        var sw = ValueStopwatch.StartNew();
        await _next(ctx);
        _log.LogInformation("Path={Path} {Elapsed}ms", ctx.Request.Path, sw.GetElapsedTime().TotalMilliseconds);
        // 可在此使用 db(每請求獨立作用域)
    }
}
app.UseMiddleware<RequestTimingMiddleware>();

IMiddleware:每請求由 DI 容器建立新實例

  • 可直接在建構子注入 Scoped 服務。
public sealed class DbAuditMiddleware : IMiddleware
{
    private readonly MyDbContext _db; // 可建構子注入 Scoped
    public DbAuditMiddleware(MyDbContext db) => _db = db;

    public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
    {
        await next(ctx);
    }
}
// 註冊:services.AddTransient<DbAuditMiddleware>();
app.UseMiddleware<DbAuditMiddleware>();

選擇指引

  • 需使用 Scoped 服務且邏輯簡單:慣用 Middleware 配合方法參數注入。
  • 邏輯複雜或需類別層級相依性:IMiddleware。

DI 生命週期與規則

  • Singleton:應用程式存活期唯一實例,不可持有 Scoped 服務實例。
  • Scoped:每請求一個實例(背景工作無請求作用域)。
  • Transient:每次解析時建立新實例。

重要規則

  • 避免將 Scoped 服務注入 Singleton 建構子。解決方式:
    • 改用 IServiceScopeFactory 動態建立作用域取得 Scoped 服務。
    • 或透過 IMiddleware 與方法參數注入確保每請求解析。
  • 長期存在的物件欄位(如單例、快取、靜態欄位)不可參考 Scoped 服務。
  • 使用 HttpClient 時,應透過 IHttpClientFactory 建立,避免 Socket 與 DNS 問題。

背景工作與作用域

BackgroundService 無自動請求作用域,需手動建立:

public sealed class Worker : BackgroundService
{
    private readonly IServiceScopeFactory _sf;
    public Worker(IServiceScopeFactory sf) => _sf = sf;

    protected override async Task ExecuteAsync(CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            using var scope = _sf.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();
            await Task.Delay(TimeSpan.FromSeconds(10), token);
        }
    }
}

常見陷阱

  • 在慣用 Middleware 建構子注入 Scoped 服務。應改為方法參數注入或使用 IMiddleware。
  • 在 Singleton 或靜態欄位保存 Scoped 服務。應透過作用域取得,並限制使用範圍。
  • 中介軟體順序不當(如授權、CORS、例外處理位置錯誤)。需重新排列確保在端點前生效。
  • 在中介軟體中執行同步 I/O 或阻塞等待(可能導致資源飢餓)。應全面使用非同步 await。

簡單小結

  • 管線順序影響行為,Use、Map、Run 各有明確職責。
  • 根據中介軟體建構時機,配合 DI 生命週期設計注入方式。
  • 背景工作與端點請求的作用域機制不同,需正確建立與釋放資源。
  • 避免將生命週期短的物件泄漏至生命週期長的持有者。

上一篇
Minimal API 與程式啟動管線
下一篇
C# 12/13 新語言特性(必要成員、原始字串)
系列文
新 .NET & Azure & IoT & AI 開源技術實戰手冊 (含深入官方程式碼講解) 30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言