EngineeringSenior DeveloperTraining Sessions

Session 03 - Middleware Pipeline & Request Lifecycle

Mỗi HTTP request trong ASP.NET Core đi qua một "ống" middleware. Thứ tự middleware quyết định app chạy đúng hay sai. Nhiều senior "add middleware mà không biết nó đứng ở đâu trong pipeline" - đó là nguồn gốc của nhiều bugs khó hiểu.


Mục tiêu

  • Hiểu request đi qua đâu, theo thứ tự nào
  • Tại sao thứ tự middleware quan trọng
  • Short-circuit là gì và khi nào xảy ra
  • Custom middleware đúng cách

1) Request Lifecycle - Toàn Cảnh

Client gửi HTTP Request
        │
        ▼
┌──── Kestrel (Web Server) ────┐
│  Nhận raw bytes, parse HTTP  │
└──────────┬───────────────────┘
           ▼
┌──── Middleware Pipeline ─────────────────────────────────────┐
│                                                               │
│  ┌─────────────┐   ┌───────┐   ┌──────┐   ┌──────────────┐ │
│  │ Exception   │──→│ HTTPS │──→│ Auth │──→│ Authorization│ │
│  │ Handler     │   │Redirect│   │      │   │              │ │
│  └──────┬──────┘   └───┬───┘   └──┬───┘   └──────┬───────┘ │
│         │              │          │               │          │
│         │              │          │               ▼          │
│         │              │          │     ┌─────────────────┐  │
│         │              │          │     │   Routing       │  │
│         │              │          │     └────────┬────────┘  │
│         │              │          │              ▼           │
│         │              │          │     ┌─────────────────┐  │
│         │              │          │     │  Endpoint       │  │
│         │              │          │     │  (Controller/   │  │
│         │              │          │     │   MinimalAPI)   │  │
│         │              │          │     └────────┬────────┘  │
│         │              │          │              │           │
│         ◄──────────────◄──────────◄──────────────◄           │
│              Response đi ngược qua từng middleware           │
└──────────────────────────────────────────────────────────────┘
        │
        ▼
Client nhận HTTP Response

Key insight: Request đi vào từ trên xuống, response đi ra từ dưới lên. Mỗi middleware có cơ hội xử lý cả request LẪN response.


2) Tại Sao Thứ Tự Middleware Quan Trọng?

csharp
// ⚠️ THỨ TỰ ĐÚNG (như Microsoft recommend)
app.UseExceptionHandler("/error");  // 1. Bắt exception TẤT CẢ middleware bên trong
app.UseHsts();                       // 2. Security headers
app.UseHttpsRedirection();           // 3. Redirect HTTP → HTTPS
app.UseStaticFiles();                // 4. Serve static files (short-circuit nếu match)
app.UseRouting();                    // 5. Routing quyết định endpoint nào match
app.UseCors();                       // 6. CORS phải SAU routing, TRƯỚC auth
app.UseAuthentication();             // 7. Xác định user là ai
app.UseAuthorization();              // 8. Kiểm tra user CÓ QUYỀN không
app.MapControllers();                // 9. Endpoint execution

Sai thứ tự = Bug

csharp
// ❌ Bug 1: Authorization TRƯỚC Authentication
app.UseAuthorization();   // Kiểm quyền... nhưng chưa biết user là ai!
app.UseAuthentication();  // Quá muộn - authorization đã chạy với user = null
// → Mọi request đều "unauthorized"

// ❌ Bug 2: ExceptionHandler ở SAU custom middleware
app.UseMyCustomMiddleware();  // Nếu throw exception ở đây...
app.UseExceptionHandler();    // Middleware này KHÔNG bắt được! (ở dưới)
// → Kestrel trả 500 generic, không có error handling

// ❌ Bug 3: StaticFiles SAU Authorization
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();  // CSS, JS, images yêu cầu login! → 401 cho mọi asset

3) Short-Circuit - Middleware Dừng Pipeline

csharp
// Short-circuit: middleware KHÔNG gọi next() → pipeline dừng tại đây

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/health")
    {
        // Short-circuit: trả response ngay, KHÔNG đi tiếp pipeline
        context.Response.StatusCode = 200;
        await context.Response.WriteAsync("OK");
        return; // ← KHÔNG gọi next()!
    }

    await next(); // Request khác → đi tiếp pipeline bình thường
});

// UseStaticFiles() cũng short-circuit:
// Nếu file tồn tại (style.css) → trả file, KHÔNG đi xuống controllers
// Nếu file không tồn tại → gọi next(), đi tiếp pipeline

Tại sao quan trọng?

UseStaticFiles() ĐẶT TRƯỚC UseAuthentication():
  → Static files (CSS, JS) được serve KHÔNG cần đăng nhập ✅
  → Short-circuit trước khi auth middleware chạy

UseStaticFiles() ĐẶT SAU UseAuthentication():
  → Phải đăng nhập mới xem được CSS! ❌
  → Auth middleware chạy trước, reject request nếu chưa login

4) Middleware Anatomy - Request + Response

csharp
// Middleware nhìn thấy CẢ request và response
app.Use(async (context, next) =>
{
    // ═══ PHASE 1: Request đi vào ═══
    var sw = Stopwatch.StartNew();
    var requestId = Guid.NewGuid().ToString();
    context.Response.Headers["X-Request-Id"] = requestId;

    Console.WriteLine($"[{requestId}] → {context.Request.Method} {context.Request.Path}");

    // ═══ Gọi middleware tiếp theo (và chờ toàn bộ pipeline phía trong) ═══
    await next();

    // ═══ PHASE 2: Response đi ra ═══
    sw.Stop();
    Console.WriteLine($"[{requestId}] ← {context.Response.StatusCode} ({sw.ElapsedMilliseconds}ms)");

    // ⚠️ Tại đây, response BODY có thể đã gửi → KHÔNG sửa headers/body được nữa!
});

Hiểu sâu: await next() là ranh giới. Code trước next() = xử lý request. Code sau next() = xử lý response. Mọi middleware bên trong đã chạy xong khi next() return.


5) Custom Middleware Đúng Cách

csharp
// ✅ Class-based middleware (recommended cho production)
public class CorrelationIdMiddleware(RequestDelegate next)
{
    // Constructor inject: được gọi 1 LẦN khi app start
    // → SINGLETON lifetime! Không inject Scoped services ở đây!

    public async Task InvokeAsync(HttpContext context, ILogger<CorrelationIdMiddleware> logger)
    {
        // Method inject: được gọi MỖI REQUEST
        // → CÓ THỂ inject Scoped services ở đây (InvokeAsync param)

        var correlationId = context.Request.Headers["X-Correlation-Id"].FirstOrDefault()
            ?? Guid.NewGuid().ToString();

        context.Items["CorrelationId"] = correlationId;
        context.Response.Headers["X-Correlation-Id"] = correlationId;

        using (logger.BeginScope(new Dictionary<string, object>
        {
            ["CorrelationId"] = correlationId
        }))
        {
            await next(context);
        }
    }
}

// Registration
app.UseMiddleware<CorrelationIdMiddleware>();

// hoặc extension method (clean API)
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseCorrelationId(this IApplicationBuilder app)
        => app.UseMiddleware<CorrelationIdMiddleware>();
}

app.UseCorrelationId();

Sai lầm phổ biến

csharp
// ❌ Inject Scoped service vào constructor
public class BadMiddleware
{
    private readonly AppDbContext _db; // 💀 SINGLETON DbContext!

    public BadMiddleware(RequestDelegate next, AppDbContext db)
    {
        _db = db; // Middleware là Singleton, DbContext là Scoped → Captive dependency!
    }
}

// ✅ Inject Scoped service vào InvokeAsync 
public class GoodMiddleware(RequestDelegate next)
{
    public async Task InvokeAsync(HttpContext context, AppDbContext db)
    {
        // db ở đây là Scoped - mới mỗi request ✅
        await next(context);
    }
}

6) Endpoint Routing - Hai Giai Đoạn

app.UseRouting();       ← Giai đoạn 1: MATCH endpoint (tìm route nào match)
// ... middleware ở giữa có thể đọc endpoint metadata
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();   ← Giai đoạn 2: EXECUTE endpoint (chạy controller/handler)
csharp
// Middleware ở giữa có thể đọc endpoint metadata
app.Use(async (context, next) =>
{
    var endpoint = context.GetEndpoint();
    if (endpoint is not null)
    {
        // Biết request sẽ đi đến endpoint nào
        var authAttr = endpoint.Metadata.GetMetadata<AuthorizeAttribute>();
        if (authAttr is not null)
        {
            // Endpoint yêu cầu auth → log warning nếu chưa login
        }
    }
    await next();
});

Master Checklist ✅

□ ExceptionHandler/HSTS đầu tiên?
□ StaticFiles TRƯỚC Authentication (nếu public)?
□ Authentication TRƯỚC Authorization?
□ CORS SAU Routing, TRƯỚC Auth?
□ Custom middleware KHÔNG inject Scoped service vào constructor?
□ Short-circuit có cần thiết? (response trả đúng status code?)
□ Middleware có handle CẢ request và response?

Common Mistakes Recap

MistakeHậu quảFix
Auth trước AuthNMọi request unauthorizedĐúng thứ tự: AuthN → AuthZ
StaticFiles sau AuthCSS/JS cần loginStaticFiles trước Auth
Exception handler cuối pipelineKhông bắt exceptionsĐặt đầu tiên
Inject Scoped vào middleware constructorCaptive dependencyInject vào InvokeAsync
Quên await next()Request hẫng, client chờ mãiLuôn forward hoặc short-circuit rõ ràng

Tư duy master: Khi thêm middleware mới, luôn tự hỏi: "Nó đứng ở đâu trong pipeline? Request phải đi qua những gì TRƯỚC khi đến middleware này? Response đi qua gì SAU?"