ASP.NET Core 中间件管道深入解析
2018.07.11
C#
3 min
1.3k 字
// 目录 · contents
ASP.NET Core
的中间件管道是整个框架的核心,理解它对于排查请求处理问题非常关键。相比
ASP.NET MVC 5 的
HttpModule/HttpHandler,新的管道模型更加直观和灵活。
1. 管道执行模型
中间件是一个嵌套的委托链,每个中间件可以选择是否调用
next 来把请求传递给下一个:
1 2 3
| Request → MW1 → MW2 → MW3 → [Handler] ↓ Response ← MW1 ← MW2 ← MW3 ←
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| app.Use(async (context, next) => { Console.WriteLine("MW1 before");
await next();
Console.WriteLine("MW1 after"); });
app.Use(async (context, next) => { Console.WriteLine("MW2 before"); await next(); Console.WriteLine("MW2 after"); });
app.Run(async context => { Console.WriteLine("Terminal handler"); await context.Response.WriteAsync("Hello World"); });
|
2. 自定义中间件类
对于复杂的中间件,建议封装成独立类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| public class RequestLoggingMiddleware { private readonly RequestDelegate _next; private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware( RequestDelegate next, ILogger<RequestLoggingMiddleware> logger) { _next = next; _logger = logger; }
public async Task InvokeAsync(HttpContext context, IUserContext userContext) { var sw = Stopwatch.StartNew(); var requestId = context.TraceIdentifier;
_logger.LogInformation( "Request [{RequestId}] {Method} {Path} started by {User}", requestId, context.Request.Method, context.Request.Path, userContext.UserId);
try { await _next(context); } finally { sw.Stop(); _logger.LogInformation( "Request [{RequestId}] completed {StatusCode} in {ElapsedMs}ms", requestId, context.Response.StatusCode, sw.ElapsedMilliseconds); } } }
public static class MiddlewareExtensions { public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder app) => app.UseMiddleware<RequestLoggingMiddleware>(); }
app.UseRequestLogging();
|
注意:中间件实例本身是单例(整个应用生命周期只创建一次),所以构造函数里只能注入
Singleton 服务。需要 Scoped 服务时,通过 InvokeAsync
方法参数注入。
3. 短路中间件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| app.Use(async (context, next) => { if (context.Request.Path == "/health") { context.Response.StatusCode = 200; await context.Response.WriteAsync("OK"); return; } await next(); });
app.UseWhen( context => context.Request.Path.StartsWithSegments("/api"), apiApp => { apiApp.UseAuthentication(); apiApp.UseAuthorization(); });
|
4. 全局异常处理中间件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public class GlobalExceptionMiddleware { private readonly RequestDelegate _next; private readonly ILogger<GlobalExceptionMiddleware> _logger;
public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger) { _next = next; _logger = logger; }
public async Task InvokeAsync(HttpContext context) { try { await _next(context); } catch (ValidationException ex) { _logger.LogWarning("Validation failed: {Message}", ex.Message); context.Response.StatusCode = 400; context.Response.ContentType = "application/json"; await context.Response.WriteAsync( JsonSerializer.Serialize(new { error = ex.Message })); } catch (NotFoundException ex) { context.Response.StatusCode = 404; context.Response.ContentType = "application/json"; await context.Response.WriteAsync( JsonSerializer.Serialize(new { error = ex.Message })); } catch (Exception ex) { _logger.LogError(ex, "Unhandled exception"); context.Response.StatusCode = 500; context.Response.ContentType = "application/json"; await context.Response.WriteAsync( JsonSerializer.Serialize(new { error = "Internal server error" })); } } }
|
5. 中间件顺序非常重要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseMiddleware<GlobalExceptionMiddleware>();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors("DefaultPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.UseRequestLogging();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
|
顺序错了会导致诡异的问题:比如 UseAuthorization 在
UseAuthentication 之前,所有请求都会 401。
踩坑记录
有一次我们的 CORS 配置一直不生效,OPTIONS 预检请求返回
403。排查了很久才发现:UseCors 放在了
UseAuthorization
之后,导致预检请求先被授权中间件拦截了。
浏览器的 OPTIONS 请求没有携带认证
token(这是标准行为),所以授权中间件直接拒绝,CORS
头根本没有机会被添加。
正确的顺序:UseRouting → UseCors →
UseAuthentication → UseAuthorization。OPTIONS
请求需要在授权检查之前就被 CORS 中间件处理并返回。
总结
- 中间件是洋葱模型:请求进入时按注册顺序执行,响应返回时逆序执行
- 中间件实例是单例,Scoped 服务通过
InvokeAsync 参数注入
- 顺序至关重要:异常处理最外层,CORS
在授权之前,静态文件在认证之前
UseWhen/MapWhen
用于条件分支,避免在每个中间件里写 if 判断
- 全局异常处理中间件是最佳实践,统一错误响应格式