aspnetcore中间件
ASP.NET Core 中间件知识体系全面梳理
系统性梳理ASP.NET Core中间件的完整知识体系,包括管道注入顺序、内置中间件的设计逻辑、自定义业务中间件的开发方法及注意事项,我们从核心概念到实战细节逐层拆解,确保逻辑闭环。
一、中间件核心基础(先立框架)
1. 本质定义
中间件是构成ASP.NET Core请求处理管道的可重用组件,每个中间件负责处理请求的一个特定环节(如认证、日志、路由),并决定是否将请求传递给下一个中间件(洋葱模型)。
-
核心关键词:管道(Pipeline)、洋葱模型、顺序执行、短路(Short-circuit)。
-
核心方法:
await next(context)是请求向下传递的唯一入口,其前后分别对应“请求进入阶段”和“响应返回阶段”。
2. 管道执行流程(可视化)
app.Use(async(context, next) =>
{
Console.WriteLine("第一个中间件开始执行111");
await next(context);
Console.WriteLine("第一个中间件的后续代码");
});
app.Use(async(context, next) =>
{
Console.WriteLine("第二个中间件开始执行111");
await next(context);
Console.WriteLine("第二个中间件的后续代码");
});
-
示例执行顺序:
- 中间件1请求阶段 → 中间件2请求阶段 → 终端逻辑 → 中间件2响应阶段 → 中间件1响应阶段。
二、中间件的注册顺序(核心重点)
1. 注册顺序的核心原则
中间件的注册顺序 = 管道执行顺序(请求阶段),且 = 响应阶段的逆序,这是ASP.NET Core最关键的设计之一,所有内置中间件的注册顺序都遵循这个原则。
2. 内置中间件的标准注册顺序(官方推荐)
官方对内置中间件的顺序有严格规范,核心逻辑是“通用逻辑在前,业务逻辑在后”,以下是最简核心顺序及设计原因:
| 顺序 | 中间件 | 作用 | 为什么放这个位置 |
|---|---|---|---|
| 1 | 异常处理(UseExceptionHandler) | 捕获后续所有中间件的异常 | 必须最前,否则无法捕获前面中间件的异常 |
| 2 | HTTPS重定向(UseHsts/UseHttpsRedirection) | 强制HTTPS | 早于业务逻辑,避免非HTTPS请求进入后续管道 |
| 3 | 静态文件(UseStaticFiles) | 处理静态资源(CSS/JS/图片) | 路由前处理静态资源,避免路由拦截静态文件 |
| 4 | 路由(UseRouting) | 解析请求路由(匹配Controller/Action) | 静态文件后、认证前,先确定路由再做权限校验 |
| 5 | 认证(UseAuthentication) | 验证用户身份(Token/Cookie) | 路由后、授权前,先确认“是谁”再判断“能做什么” |
| 6 | 授权(UseAuthorization) | 验证用户权限 | 认证后、业务前,权限不通过直接短路 |
| 7 | 端点映射(UseEndpoints) | 执行业务接口(Controller/Action) | 终端中间件,管道最后一步处理业务 |
设计逻辑总结:
-
异常处理放最前:确保所有后续中间件的异常都能被捕获;
-
静态文件放路由前:避免路由把静态文件请求误判为业务接口;
-
认证/授权在路由后:先知道请求的是哪个接口,再校验该接口的权限;
-
终端中间件(UseEndpoints)放最后:处理完所有通用逻辑后,执行核心业务。
3. 错误顺序的坑(举例)
-
若把授权中间件放在端点映射后:业务接口已经执行完,授权校验失去意义;
-
若把异常处理放在路由后:路由前的中间件(如静态文件)抛出的异常无法被捕获;
-
若把静态文件放在端点映射后:静态文件请求会被路由拦截,返回404。
三、自定义业务中间件的开发(从简单到复杂)
1. 三种开发方式(按场景选择)
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 内联中间件(Use) | 简单逻辑(如日志、临时校验) | 快速、无需额外类 | 代码冗余、不易复用 |
| 强类型中间件(IMiddleware) | 复杂逻辑(需依赖注入、复用) | 可注入、可测试、复用 | 稍繁琐、需注册服务 |
| 工厂中间件(IMiddlewareFactory) | 自定义中间件创建逻辑 | 高度定制化 | 极少使用(超出常规场景) |
方式1:内联中间件(快速实现)
// Program.cs
var app = builder.Build();
// 示例:接口耗时统计中间件
app.Use(async (context, next) =>
{
// 请求阶段:记录开始时间
var startTime = DateTime.Now;
Console.WriteLine($"请求开始:{context.Request.Path}");
// 传递请求到下一个中间件
await next(context);
// 响应阶段:计算耗时
var elapsed = (DateTime.Now - startTime).TotalMilliseconds;
Console.WriteLine($"请求结束:{context.Request.Path},耗时:{elapsed}ms");
});
// 后续中间件(路由、端点等)
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", () => "Hello World!");
});
app.Run();
方式2:强类型中间件(推荐用于业务逻辑)
步骤1:定义中间件类(实现IMiddleware)
// 日志中间件(依赖注入ILogger)
public class LoggingMiddleware : IMiddleware
{
private readonly ILogger<LoggingMiddleware> _logger;
// 构造函数注入依赖(核心优势)
public LoggingMiddleware(ILogger<LoggingMiddleware> logger)
{
_logger = logger;
}
// 核心执行方法
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// 请求阶段
_logger.LogInformation($"请求进入:{context.Request.Method} {context.Request.Path}");
// 传递请求
await next(context);
// 响应阶段
_logger.LogInformation($"请求完成:状态码 {context.Response.StatusCode}");
}
}
步骤2:注册中间件服务
// Program.cs
builder.Services.AddTransient<LoggingMiddleware>(); // 必须注册为Transient
步骤3:使用中间件
// Program.cs
var app = builder.Build();
app.UseMiddleware<LoggingMiddleware>(); // 注册到管道
// 后续中间件...
2. 自定义中间件的核心开发技巧
技巧1:实现“短路”逻辑
当满足特定条件时,不调用next(context),直接返回响应(如权限校验失败):
app.Use(async (context, next) =>
{
// 模拟权限校验
var token = context.Request.Headers["Authorization"].FirstOrDefault();
if (string.IsNullOrEmpty(token))
{
// 短路:直接返回401,不传递请求
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync("未授权");
return; // 关键:不执行await next(context)
}
await next(context);
});
技巧2:条件执行中间件(UseWhen)
只对特定请求执行中间件(如只对/api路径执行权限校验):
// 只对/api开头的请求执行权限中间件
app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), appBuilder =>
{
appBuilder.Use(async (context, next) =>
{
// 权限校验逻辑...
await next(context);
});
});
技巧3:分支中间件(Map)
不同路径走不同管道(如/admin和/api分开处理):
// /admin路径走独立管道
app.Map("/admin", adminApp =>
{
// admin专属中间件(如管理员权限校验)
adminApp.Use(async (context, next) =>
{
Console.WriteLine("管理员请求");
await next(context);
});
adminApp.Run(async context =>
{
await context.Response.WriteAsync("管理员页面");
});
});
// 其他路径走默认管道
app.Run(async context =>
{
await context.Response.WriteAsync("默认页面");
});
四、自定义中间件的注意事项(避坑指南)
1. 线程安全
-
中间件默认是单例(内联/UseMiddleware方式),类中的字段会被所有请求共享;
-
禁止在中间件类中存储请求相关的状态(如用户ID、请求参数),如需存储需用
HttpContext.Items; -
若需全局状态(如“是否已初始化”),必须加锁(如
SemaphoreSlim)保证线程安全。
2. 依赖注入生命周期
-
内联中间件:通过
context.RequestServices.GetService<T>()获取范围生命周期服务(如DbContext); -
强类型中间件(IMiddleware):构造函数注入的服务默认是范围生命周期(推荐),避免注入单例服务依赖范围服务;
-
禁止在单例中间件中直接持有DbContext(范围服务),会导致数据库连接泄漏。
3. 性能与资源
-
避免在中间件中做耗时操作(如大文件读写、远程API调用),如需做需异步执行(
await); -
及时释放资源(如数据库连接、文件流),建议用
using语句; -
避免重复创建对象(如每次请求都new一个大对象),可缓存复用。
4. 异常处理
-
中间件内的异常需自行捕获,否则会向上抛到异常处理中间件;
-
关键逻辑(如数据操作、权限校验)必须加
try-catch,并记录日志; -
避免在响应阶段抛出异常(此时响应可能已开始发送,无法修改状态码)。
5. 环境适配
-
区分开发/生产环境(
IWebHostEnvironment),如开发环境打印详细日志,生产环境返回友好提示; -
生产环境禁止暴露敏感信息(如异常堆栈、数据库连接字符串)。
五、中间件的最佳实践(总结)
-
单一职责:一个中间件只做一件事(如日志中间件只记录日志,权限中间件只做校验);
-
可测试性:强类型中间件通过构造函数注入依赖,方便单元测试;
-
可配置化:通过
IConfiguration读取配置(如权限白名单、日志级别),避免硬编码; -
文档化:关键中间件需注释说明作用、注册顺序、依赖服务;
-
复用性:通用中间件(如日志、异常处理)封装成类库,跨项目复用。
总结
-
中间件的核心是管道顺序+洋葱模型,内置中间件的注册顺序遵循“通用逻辑在前、业务逻辑在后”,异常处理必须最前,终端中间件最后;
-
自定义中间件有内联(简单)和强类型(复用)两种方式,开发时需注意线程安全、依赖生命周期、异常处理;
-
关键避坑点:单例中间件不存储请求状态、避免短路逻辑误用、区分环境适配、保证线程安全。
- ps:个人经验总结,目前仅限个人参考。如果能帮到你我也会很开心
浙公网安备 33010602011771号