Bạn đã từng nghe về middleware trong ASP.NET Core nhưng vẫn chưa thực sự hiểu rõ nó là gì? Bài viết này hướng dẫn cách middleware hoạt động, cách tự viết middleware và những lỗi thường khiến lập trình viên gặp sự cố.
Mục lục
Viết middleware đầu tiên
Bạn thêm middleware trong Program.cs bằng app.Use:
// Program.cs
app.Use(async (context, next) =>
{
Console.WriteLine($"Request: {context.Request.Path}");
await next(context);
Console.WriteLine($"Response: {context.Response.StatusCode}");
});
Middleware chạy trên mỗi HTTP request. Mỗi request đi qua middleware theo chiều vào, sau đó response đi ngược lại theo chiều ra.
Lệnh gọi next rất quan trọng. Lệnh này lấy middleware tiếp theo trong pipeline. Nếu quên gọi nó, phần còn lại của pipeline sẽ không chạy; mọi middleware được đăng ký phía sau cũng bị bỏ qua.
Cách viết bằng lambda ở trên gây khó khăn khi viết unit test. Một cách tốt hơn là chuyển logic vào một class riêng.
Đưa middleware vào một class
// RequestLoggingMiddleware.cs
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
public RequestLoggingMiddleware(RequestDelegate next) =>
_next = next;
public async Task InvokeAsync(HttpContext context)
{
Console.WriteLine($"Request: {context.Request.Path}");
await _next(context);
Console.WriteLine($"Response: {context.Response.StatusCode}");
}
}
Bạn cần nhớ nhận next trong constructor để có thể gọi middleware tiếp theo trong pipeline.
Sau đó đăng ký middleware trong Program.cs. Bạn có thể xóa phiên bản dùng app.Use vì class mới đã thay thế nó:
// Program.cs
// START - Có thể xóa phần này
app.Use(async (context, next) =>
{
Console.WriteLine($"Request: {context.Request.Path}");
await next(context);
Console.WriteLine($"Response: {context.Response.StatusCode}");
});
// END - Có thể xóa phần này
// Đăng ký middleware tùy chỉnh
app.UseMiddleware<RequestLoggingMiddleware>();
Nếu không đăng ký, middleware sẽ không bao giờ được chạy.
Quên gọi next
Thứ tự middleware trong Program.cs rất quan trọng. Vì RequestLoggingMiddleware được đăng ký đầu tiên, nó sẽ xử lý request trước và ghi log response sau cùng.
// Program.cs
app.UseMiddleware<RequestLoggingMiddleware>(); // CHẠY ĐẦU TIÊN
// Ánh xạ các endpoint Minimal API
app.MapMinimalApiEndpoints();
// Cấu hình HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/openapi/v1.json", "Api");
});
app.MapOpenApi();
}
// MinimalApiEndpoints.cs
public static class MinimalApiEndpoints
{
extension (WebApplication app)
{
public WebApplication MapMinimalApiEndpoints()
{
var group = app.MapGroup("/api");
group.MapGet("test", Test);
return app;
}
}
public static ContentHttpResult Test()
=> TypedResults.Content("This is a test", "text/plain");
}
Bây giờ hãy chú thích lệnh gọi _next trong RequestLoggingMiddleware:
// RequestLoggingMiddleware.cs
public class RequestLoggingMiddleware
{
...
public async Task InvokeAsync(HttpContext context)
{
Console.WriteLine($"Request: {context.Request.Path}");
//await _next(context);
Console.WriteLine($"Response: {context.Response.StatusCode}");
}
}
Chạy ứng dụng và bạn sẽ thấy trang không tải được.
Khi gọi endpoint API bằng Postman, thay vì nhận nội dung This is a test, bạn nhận response 200 nhưng không có dữ liệu.
Nguyên nhân là RequestLoggingMiddleware được đăng ký đầu tiên. Khi middleware này không gọi next, mọi middleware phía sau, bao gồm endpoint Minimal API và Swagger, đều không được chạy. Cách sửa là thêm lại lệnh gọi next trong RequestLoggingMiddleware.cs.
Middleware là singleton, còn InvokeAsync có phạm vi theo request
Trước tiên, đăng ký một service có vòng đời scoped trong Program.cs:
// Program.cs
builder.Services.AddScoped<IMyScopedService, MyScopedService>();
Sau đó tiêm service này vào constructor của RequestLoggingMiddleware:
// RequestLoggingMiddleware.cs
public class RequestLoggingMiddleware
{
...
public RequestLoggingMiddleware(
RequestDelegate next,
IMyScopedService myScopedService)
{
...
}
...
}
Khi chạy ứng dụng, bạn sẽ nhận lỗi:
Cannot resolve scoped service 'IMyScopedService' from root provider
Điều này xảy ra vì UseMiddleware tạo class middleware một lần bằng service provider gốc của ứng dụng, sau đó tái sử dụng cùng một instance cho mọi request. Service scoped không thể được lấy trực tiếp từ service provider gốc.
Cách sửa là chuyển IMyScopedService ra khỏi constructor và tiêm nó vào InvokeAsync:
// RequestLoggingMiddleware.cs
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
public RequestLoggingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(
HttpContext context,
IMyScopedService myScopedService) // Tiêm tại đây
{
Console.WriteLine($"Request: {context.Request.Path}");
await _next(context);
Console.WriteLine($"Response: {context.Response.StatusCode}");
}
}
InvokeAsync được chạy mới cho từng request, vì vậy ASP.NET Core có thể lấy IMyScopedService từ scope của request hiện tại mỗi lần.
Thay đổi response sau khi response đã bắt đầu
Một lỗi khác là cố thay đổi response sau khi response đã bắt đầu. Trong ví dụ dưới đây, middleware cập nhật mã trạng thái sau khi lệnh gọi next hoàn tất:
// RequestLoggingMiddleware.cs
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
public RequestLoggingMiddleware(RequestDelegate next) =>
_next = next;
public async Task InvokeAsync(HttpContext context)
{
Console.WriteLine($"Request: {context.Request.Path}");
await _next(context);
// THAY ĐỔI MÃ TRẠNG THÁI VÀ RESPONSE
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await context.Response.WriteAsync("<p>This page is currently unavailable</p>");
Console.WriteLine($"Response: {context.Response.StatusCode}");
}
}
Đoạn mã này gây ra exception:
System.InvalidOperationException: StatusCode cannot be set because the response has already started.
Sau khi response đã bắt đầu, bạn không thể thay đổi mã trạng thái hoặc ghi thêm dữ liệu vào response. Thay vào đó, có thể đăng ký logic chạy ngay trước khi response bắt đầu bằng context.Response.OnStarting:
// RequestLoggingMiddleware.cs
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private bool MaintenancePage => true;
public RequestLoggingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
Console.WriteLine($"Request: {context.Request.Path}");
context.Response.OnStarting(async () =>
{
if (context.Response.HasStarted)
{
return;
}
if (MaintenancePage)
{
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await context.Response.WriteAsync("<p>This page is currently unavailable</p>");
}
});
await _next(context);
Console.WriteLine($"Response: {context.Response.StatusCode}");
}
}
Bên trong OnStarting, trước tiên hãy kiểm tra HasStarted và thoát sớm nếu response đã bắt đầu. Nếu chưa, với MaintenancePage có giá trị true, response sẽ hiển thị thông báo trang hiện không khả dụng.
Giữ service scoped sau khi request đã hoàn tất
Một lỗi dễ gặp khác là tiếp tục giữ và sử dụng service scoped sau khi response đã hoàn tất.
// MyScopedService.cs
public class MyScopedService : IMyScopedService, IDisposable
{
public bool Disposed { get; private set; }
public void Test()
{
_logger.LogInformation(
"MyScopedService.Test() runs successfully");
}
public void Dispose()
{
Disposed = true;
}
}
Thêm một delegate OnCompleted vào middleware và tạo background task chạy sau 5 giây:
public class RequestLoggingMiddleware
{
...
public async Task InvokeAsync(
HttpContext context,
IMyScopedService myScopedService)
{
_logger.LogInformation($"Request: {context.Request.Path}");
...
context.Response.OnCompleted(() =>
{
var _ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(5));
try
{
myScopedService.Test();
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
}
});
return Task.CompletedTask;
});
...
}
}
Thoạt nhìn, mọi thứ có vẻ hoạt động bình thường. Trang vẫn hiển thị chính xác và log cho thấy MyScopedService.Test() được chạy cho từng request. Tuy nhiên, cách làm này có thể phát sinh lỗi ẩn tùy theo cách bạn sử dụng service.
Vấn đề là sau khi response hoàn tất, service scoped đã bị giải phóng vì nó được tiêm vào InvokeAsync. Để kiểm tra, hãy thêm logic ném lỗi khi service đã bị giải phóng:
// MyScopedService.cs
public class MyScopedService : IMyScopedService, IDisposable
{
...
public void Test()
{
ObjectDisposedException.ThrowIf(Disposed, this); // Thêm dòng này.
_logger.LogInformation(
"MyScopedService.Test() runs successfully");
}
...
}
Trang vẫn hiển thị bình thường, nhưng log sẽ xuất hiện exception:
Cannot access a disposed object.
Cách an toàn hơn là tạo một scope mới cho công việc chạy nền, thay vì tái sử dụng instance scoped của request:
// RequestLoggingMiddleware.cs
public class RequestLoggingMiddleware
{
...
public async Task InvokeAsync(
HttpContext context,
IMyScopedService myScopedService)
{
...
context.Response.OnCompleted(() =>
{
var _ = Task.Run(async () =>
{
// Tạo scope mới
using var scope = _serviceScopeFactory.CreateScope();
var myScopedService = scope.ServiceProvider
.GetRequiredService<IMyScopedService>();
await Task.Delay(TimeSpan.FromSeconds(5));
try
{
myScopedService.Test();
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
}
_logger.LogInformation("Task completed");
});
return Task.CompletedTask;
});
await _next(context);
...
}
}
Bằng cách tiêm IServiceScopeFactory vào middleware và tạo scope mới bên trong background task, myScopedService được lấy từ scope mới thay vì scope gắn với request ban đầu. Vì vậy service không bị giải phóng trước khi công việc trì hoãn được thực hiện.
Tóm tắt các lỗi cần lưu ý
- Luôn gọi
nextnếu middleware cần cho phép pipeline tiếp tục chạy. - Không tiêm service scoped trực tiếp vào constructor của middleware.
- Không thay đổi response sau khi response đã bắt đầu.
- Không giữ service scoped của request để dùng trong công việc chạy nền sau khi request hoàn tất.
- Hãy tạo scope mới bằng
IServiceScopeFactorykhi background task cần dùng service scoped.
Xem video
Video hướng dẫn lần lượt các lỗi thường gặp khi làm việc với middleware trong dự án ASP.NET Core, bao gồm quên gọi next, tiêm service scoped không đúng cách, thay đổi response sau khi response bắt đầu và tiếp tục sử dụng service scoped sau khi request hoàn tất.
Bài hướng dẫn liên quan
- Cách tạo và thêm custom middleware trong ASP.NET Core — Tìm hiểu cách tạo middleware bằng C# và thêm nó vào ứng dụng .NET để bổ sung HTTP response header cho request của ASP.NET Core Web API.
- Thêm log request vào database trong ASP.NET Core Web API — Tìm hiểu cách lưu log request vào database bằng Entity Framework Core để theo dõi và phân tích lưu lượng API hiệu quả.



