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
| Mistake | Hậu quả | Fix |
|---|---|---|
| Auth trước AuthN | Mọi request unauthorized | Đúng thứ tự: AuthN → AuthZ |
| StaticFiles sau Auth | CSS/JS cần login | StaticFiles trước Auth |
| Exception handler cuối pipeline | Không bắt exceptions | Đặt đầu tiên |
| Inject Scoped vào middleware constructor | Captive dependency | Inject vào InvokeAsync |
Quên await next() | Request hẫng, client chờ mãi | Luô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?"