.NET Middleware Pipeline — request đi qua đâu trước Controller?
Hồi mới chuyển từ Node.js (Express) qua .NET, mình hơi bị choáng với middleware pipeline. Trong Express thì nó đơn giản — request đi qua từng hàm theo thứ tự, ai gọi next() là đi tiếp. Còn .NET thì sao? Cũng là pipeline, nhưng mà có cấu trúc rõ ràng và gọn gàng hơn nhiều.
Ảnh: olia danilevich — Pexels
Middleware Pipeline là gì?
Đơn giản mà nói, middleware pipeline trong .NET là một cái chuỗi các delegate, mỗi thằng có cơ hội xử lý request TRƯỚC khi nó tới endpoint, và xử lý response SAU KHI endpoint trả về. Cơ chế này gọi là Russian Doll model — request chui vô từng lớp, tới endpoint rồi chui ra theo chiều ngược lại.
Request → Logger → Auth → ExceptionHandler → Endpoint → Response
← ← ← ←
Thằng middleware đầu tiên nhận request, nó làm gì đó (VD: log), rồi gọi next() — request được pass cho thằng kế. Cứ thế cho tới endpoint. Sau đó response chạy ngược ra ngoài, cũng qua mấy thằng middleware đó nhưng theo thứ tự ngược lại.
Mấy cái Middleware built-in thường dùng
.NET có sẵn một đống middleware hay ho:
var app = builder.Build();
// Thứ tự RẤT quan trọng
app.UseExceptionHandler("/error"); // 1. Bắt exception sớm nhất
app.UseHttpsRedirection(); // 2. Redirect sang HTTPS
app.UseStaticFiles(); // 3. Serve file tĩnh
app.UseRouting(); // 4. Xác định route
app.UseCors(); // 5. CORS
app.UseAuthentication(); // 6. Xác thực
app.UseAuthorization(); // 7. Phân quyền
app.MapControllers(); // 8. Controller cuối cùng
Thứ tự này không phải ngẫu nhiên đâu. Nếu bạn để UseExceptionHandler() ở cuối, exception từ mấy middleware trước nó sẽ không được bắt — app crash mất.
Ảnh: Eduardo Rosas — Pexels
Custom Middleware — viết như thế nào?
Có 2 cách:
Cách 1: Lambda inline (nhanh gọn)
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
// Trước khi vào pipeline
Console.WriteLine($"[{DateTime.UtcNow}] {context.Request.Path}");
await next(); // Gọi middleware kế
// Sau khi response về
stopwatch.Stop();
Console.WriteLine($"Took {stopwatch.ElapsedMilliseconds}ms");
});
Cách 2: Middleware class (dùng cho phức tạp)
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
public RequestTimingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();
await _next(context);
stopwatch.Stop();
// Ghi timing vào response header
context.Response.Headers["X-Response-Time"] =
$"{stopwatch.ElapsedMilliseconds}ms";
}
}
// Extension method cho đẹp
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseRequestTiming(
this IApplicationBuilder app)
{
return app.UseMiddleware<RequestTimingMiddleware>();
}
}
Xong rồi dùng: app.UseRequestTiming();
Branching — tách pipeline riêng
Đôi khi bạn muốn một nhóm middleware chỉ chạy cho một đường dẫn cụ thể. .NET có Map() và MapWhen():
// Toàn bộ request tới /healthz chỉ qua pipeline này
app.Map("/healthz", healthApp =>
{
healthApp.UseMiddleware<HealthCheckMiddleware>();
healthApp.Run(async context =>
{
await context.Response.WriteAsync("OK");
});
});
// Request tới /api qua pipeline khác
app.Map("/api", apiApp =>
{
apiApp.UseAuthentication();
apiApp.UseAuthorization();
apiApp.Run(async context =>
{
// Xử lý API
});
});
Cái này cực kỳ hữu ích khi bạn muốn health check endpoint không cần auth, trong khi API endpoints thì cần.
Exception Handling Middleware — tự xử lý lỗi
Đây là middleware mình thấy được dùng nhiều nhất trong thực tế. Thay vì try-catch khắp nơi, bạn chỉ cần 1 middleware ở đầu pipeline:
app.Use(async (context, next) =>
{
try
{
await next();
}
catch (NotFoundException)
{
context.Response.StatusCode = 404;
await context.Response.WriteAsJsonAsync(new
{ error = "Không tìm thấy" });
}
catch (ValidationException ex)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsJsonAsync(new
{ error = ex.Message });
}
catch (Exception ex)
{
// Log lỗi + trả về 500
_logger.LogError(ex, "Unhandled exception");
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new
{ error = "Internal server error" });
}
});
Performance — thứ tự middleware ảnh hưởng ra sao?
Một lần mình deploy lên staging thấy response time tăng vọt. Sau một hồi debug, phát hiện ra mình để static files middleware SAU authentication middleware — mỗi request tới file tĩnh (JS, CSS) cũng phải qua auth, rất tốn kém.
Quy tắc vàng:
- Static files, health checks → cho qua sớm, tránh tốn auth
- Exception handling → đầu pipeline
- Auth → sau routing, trước controller
- Những thứ nặng (rate limiting, request logging) → càng gần endpoint càng tốt, để không ảnh hưởng tới static files
Nói chung, middleware pipeline là một trong những cái mình thích nhất ở .NET — nó rõ ràng, predictable, và cực kỳ dễ extend. Nếu mới học .NET, dành chút thời gian ngồi hiểu pipeline trước khi viết code, mình cam đoan sẽ đỡ đau đầu hơn rất nhiều.
Ảnh: Markus Spiske — Pexels
📋 Phụ lục thuật ngữ
- Middleware — phần mềm trung gian, xử lý request/response theo pipeline
- Pipeline — chuỗi các middleware xử lý request theo thứ tự
- Russian Doll model — mô hình xử lý lồng nhau, request vào trước ra sau
- RequestDelegate — delegate đại diện cho middleware kế tiếp trong pipeline
- Branching — kỹ thuật tách pipeline riêng cho từng nhóm đường dẫn