2.2 中间件管线:请求经过的每一站


文档摘要

2.2 中间件管线:请求经过的每一站 中间件管线是 ASP.NET Core 的主动脉:每个中间件都有机会在请求进站时做预处理、调用下一站、在出站时做后处理。注册顺序即执行顺序,这条规则解释了框架里一大半的"灵异现象"。 上一节主机把路修好了,这一节看路上的车。观察哨本节蹲在管线中段,用实验验证一条规则、复盘三个真实故障。 U 型执行模型 每个中间件本质是一个函数:拿到当前请求上下文和一个 next 委托。调用 next,请求流向下一站;next 返回后,响应正从下游流回。整条管线是一个嵌套调用链——先进后出,像一只 U 型管。 访问根路径,控制台输出: 顺序完全可预测:A 先进站后出站,B 后进站先出站。MapGet 登记的端点在最内层,是 U 型的底。

2.2 中间件管线:请求经过的每一站

中间件管线是 ASP.NET Core 的主动脉:每个中间件都有机会在请求进站时做预处理、调用下一站、在出站时做后处理。注册顺序即执行顺序,这条规则解释了框架里一大半的"灵异现象"。

上一节主机把路修好了,这一节看路上的车。观察哨本节蹲在管线中段,用实验验证一条规则、复盘三个真实故障。

U 型执行模型

每个中间件本质是一个函数:拿到当前请求上下文和一个 next 委托。调用 next,请求流向下一站;next 返回后,响应正从下游流回。整条管线是一个嵌套调用链——先进后出,像一只 U 型管。

// 一个中间件的标准形状:next 之前是进站段,之后是出站段 app.Use(async (ctx, next) => { Console.WriteLine("A 进站"); // 1. 请求到达本站 await next(); // 2. 放行,等下游各站处理完 Console.WriteLine("A 出站"); // 3. 响应回流经过本站 }); app.Use(async (ctx, next) => { Console.WriteLine("B 进站"); await next(); Console.WriteLine("B 出站"); }); app.MapGet("/", () => "终点站");

访问根路径,控制台输出:

A 进站 B 进站 A 出站 B 出站

顺序完全可预测:A 先进站后出站,B 后进站先出站。MapGet 登记的端点在最内层,是 U 型的底。这就是"注册顺序即执行顺序"的全部含义——UseMap 阶段的代码顺序,直接翻译成嵌套深度。

图 2.2-1 U 型管线执行模型

图 2.2-1 U 型管线执行模型

短路:不调用 next 的中间件

短路是管线的"截停权"。典型如静态文件中间件:找到文件就写响应、不再放行;没找到才交给路由。写一个实验版本:

// 短路示例:/block 路径被截停,永远到不了后面的端点 app.Use(async (ctx, next) => { if (ctx.Request.Path.StartsWithSegments("/block")) { ctx.Response.StatusCode = 403; // 就地写状态码 await ctx.Response.WriteAsync("被观察哨拦截"); // 写响应体并短路 return; // 不调用 next,下游全部跳过 } await next(); }); app.MapGet("/block", () => "你永远看不到这句话");

访问 block 路径,返回 403 与"被观察哨拦截",日志里不会出现终点站痕迹。认证、静态文件、响应缓存中间件都靠短路工作。

三个顺序故障复盘

顺序规则的价值在故障里体现。三个我都亲历过。

故障一:异常处理失效。 开发者写了全局异常兜底,却放在了路由之后。下游抛出的异常先经过路由站,路由站本身没有兜底逻辑,异常直接冒泡到服务器层,返回裸 500。修复:UseExceptionHandler 挪到管线靠前位置。铁律是"越兜底的越靠前"。

// 正确顺序:异常兜底必须在最前,才能包住后续所有站 app.UseExceptionHandler("/error"); app.UseStatusCodePages(); // ... 其余中间件

故障二:认证拦截了静态文件。 UseAuthorization 放在 UseStaticFiles 之前,结果登录页的样式文件也要求登录,页面裸奔。修复:静态文件站提前。铁律是"不需要鉴权的资源放在鉴权站之前"。

故障三:响应已开始还改头。 短路中间件先写了响应体,出站段又想补一个自定义头,抛出"响应已启动"异常。修复:改头必须发生在任何 WriteAsync 之前,或用 OnStarting 回调挂到最后时刻。

四类封装中间件与短路口方法

内联 lambda 适合小段逻辑,可复用的中间件应封装成类,通过 UseMiddleware 或扩展方法挂载:

// 类封装:请求耗时观察哨的正式版 public class RequestLogMiddleware { private readonly RequestDelegate _next; // 下一站 private readonly ILogger<RequestLogMiddleware> _logger; // 日志通道,2.3 节展开 public RequestLogMiddleware(RequestDelegate next, ILogger<RequestLogMiddleware> logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext ctx) { var sw = System.Diagnostics.Stopwatch.StartNew(); try { await _next(ctx); } // 进站放行 finally { sw.Stop(); // 出站记录:结构化日志,级别 Information _logger.LogInformation("{Method} {Path} -> {Code} in {Ms}ms", ctx.Request.Method, ctx.Request.Path.Value, ctx.Response.StatusCode, sw.ElapsedMilliseconds); } } } // 挂载方式:扩展方法让管线编排保持整洁 public static class RequestLogExtensions { public static IApplicationBuilder UseRequestLog(this IApplicationBuilder app) => app.UseMiddleware<RequestLogMiddleware>(); } // 使用:app.UseRequestLog();

注意构造函数注入发生在启动期(一次),InvokeAsync 的参数注入每请求发生——这是类封装中间件与内联写法在注入时机上的细微差别。

💡 关键直觉:把常用中间件组合想象成"管网标准件"。UseRouting 之后才有端点信息,UseAuthentication 之后才有用户身份,UseAuthorization 依赖前两者——框架对少数相邻站有强制次序,其余靠你按语义排。

本节要点回顾

  • U 型模型:next 前处理请求、next 后处理响应,注册顺序即嵌套深度;
  • 短路:不调 next 即截停,静态文件与认证的核心机制;
  • 顺序三故障:异常兜底要最前、静态文件在鉴权前、响应开始后不可改头;
  • 类封装中间件:构造注入一次、Invoke 注入每请求;
  • 强制次序对:Routing → Authentication → Authorization → Endpoints,其余按语义排。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U