在ASP.NET Core中,中间件是组成请求处理管道的核心单元。每一个中间件都像一个接力选手,可以选择把请求传给下一位,也可以直接结束响应。理解中间件的运行机制,是构建鉴权、日志、异常处理等横切功能的基础。

中间件的基本工作原理
ASP.NET Core的请求管道由一组有序的中间件构成,框架使用RequestDelegate来表示下一个中间件的委托。当你注册一个中间件时,实际上就是向管道中插入一段逻辑,这段逻辑接收HttpContext并决定是否调用后续委托。
最基础的中间件写法是通过app.Use传入一个接收HttpContext和RequestDelegate的匿名方法。下面代码展示了一个打印请求路径后再调用后续中间件的最小示例:
app.Use(async (context, next) =>
{
// 在调用下一个中间件前记录请求路径
var path = context.Request.Path.Value;
Console.WriteLine($"请求进入:{path}");
// 交给管道中的下一个中间件
await next();
// 后续中间件执行完毕后回到这里
Console.WriteLine($"响应完成:{path}");
});
这种写法适合逻辑简单、仅在某处使用一次的场景。它的优势是直观,不需要单独建类;缺点是如果逻辑复杂,匿名方法会让Program.cs变得臃肿,且不利于单元测试。
使用约定类封装中间件
当处理逻辑需要复用或变得复杂时,推荐按照约定创建一个中间件类。该类需包含公共构造函数接收RequestDelegate,并暴露返回Task的InvokeAsync方法。
以下示例实现了一个统计请求耗时的中间件。它在调用next之前记录开始时间,在之后计算并写入响应头。注意_next字段保存了管道后续委托,不能在构造之外修改。
public class TimingMiddleware
{
private readonly RequestDelegate _next;
public TimingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var start = DateTime.UtcNow;
// 先执行后续管道
await _next(context);
var cost = DateTime.UtcNow - start;
// 将耗时写入响应头,方便前端或网关观测
context.Response.Headers.Add("X-Process-Time", cost.TotalMilliseconds.ToString());
}
}
// 注册扩展方法
public static class TimingMiddlewareExtensions
{
public static IApplicationBuilder UseTiming(this IApplicationBuilder builder)
{
return builder.UseMiddleware<TimingMiddleware>();
}
}
使用约定类后,注册只需一行app.UseTiming();。这种结构清晰,能把字段注入(如配置、日志器)通过构造函数完成。但要小心:中间件实例在应用生命周期内是单例的,因此不要在其中保存请求级状态,否则会造成数据串号。
在中间件中读取与修改请求
很多业务需要在中间件里读取请求正文,例如签名验证或报文转换。由于HTTP请求体是一个只进流,直接读取会导致后续模型绑定失败,必须借助EnableBuffering允许重读。
下面的代码演示了如何在中间件中安全地读取JSON正文并原样归还流位置:
app.Use(async (context, next) =>
{
// 允许请求体可重读
context.Request.EnableBuffering();
using var reader = new StreamReader(
context.Request.Body,
encoding: System.Text.Encoding.UTF8,
detectEncodingFromByteOrderMarks: false,
leaveOpen: true);
var body = await reader.ReadToEndAsync();
// 此处可做签名校验或日志落盘
Console.WriteLine($"正文长度:{body.Length}");
// 将流位置重置,供后续中间件或控制器读取
context.Request.Body.Position = 0;
await next();
});
如果不调用EnableBuffering,ReadToEndAsync会消耗流且无法复位,MVC控制器将无法再绑定参数。这是中间件处理请求正文时最容易踩的坑之一。
短路请求与返回自定义响应
中间件不一定总要调用next。当检测到未授权或参数非法时,可直接写入响应并结束管道,这称为短路。短路能减少不必要的下游开销。
下面示例在请求头缺少令牌时直接返回401和JSON错误信息,不再执行后续任何中间件:
app.Use(async (context, next) =>
{
if (!context.Request.Headers.ContainsKey("X-Token"))
{
context.Response.StatusCode = 401;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(
"{"error":"缺少访问令牌"}");
// 不调用 next,直接结束
return;
}
await next();
});
短路逻辑应放在管道靠前位置,例如放在路由和端点之前。但要注意,如果前面已有身份验证中间件,应优先使用框架内置方案,避免重复造轮子。自定义短路适合做轻量的网关层拦截。
中间件注册顺序的影响
中间件的执行顺序严格遵循注册顺序。先注册的中间件先接触请求,后接触响应。例如异常处理中间件必须放在最前,才能捕获后续所有错误。
常见推荐顺序为:异常处理、HTTPS重定向、静态文件、路由、鉴权、端点。若把日志记录放最前,它能记录所有请求;若放鉴权之后,则未通过鉴权的请求不会被记录。下面用表格说明两种顺序差异:
| 注册顺序 | 请求阶段可见性 | 适用场景 |
|---|---|---|
| 日志在最前 | 全部请求含被拒请求 | 安全审计、流量统计 |
| 日志在鉴权后 | 仅合法请求 | 业务性能分析 |
理解顺序差异能避免“为什么日志没打”之类的排查盲区。在Program.cs中调整app.UseXxx的先后即可控制行为,无需改动中间件内部代码。
依赖注入与作用域注意事项
中间件构造函数只能注入单例或瞬态服务,不能直接注入作用域服务(如EF Core的DbContext)。如果需要在中间件中使用作用域服务,应通过InvokeAsync的参数或context.RequestServices解析。
以下代码展示通过方法参数注入作用域服务的安全写法:
public class ScopedMiddleware
{
private readonly RequestDelegate _next;
public ScopedMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, IMyScopedService svc)
{
// svc 每次请求都是新的作用域实例
var data = await svc.GetAsync();
context.Items["data"] = data;
await _next(context);
}
}
这样框架会自动创建对应请求的作用域并释放资源,避免内存泄漏。若错误地在构造函数中捕获作用域服务,会导致服务被提升为单例,引发并发与数据不一致问题。
总结与实践建议
掌握ASP.NET Core中间件请求处理,关键在理解RequestDelegate链条、HttpContext的读写时机以及注册顺序。简单逻辑用app.Use匿名委托,复杂功能用约定类并配合扩展方法注册。
在真实项目中,建议把鉴权短路、请求日志、异常捕获做成独立中间件,并放在管道头部;请求正文读取务必启用缓冲并复位流;作用域服务走方法参数注入。遵循这些准则,你的中间件将既灵活又稳定。
C#ASP.NET_Core中间件修改时间:2026-07-31 14:42:35