aspnetcore中间件

ASP.NET Core 中间件知识体系全面梳理

系统性梳理ASP.NET Core中间件的完整知识体系,包括管道注入顺序、内置中间件的设计逻辑、自定义业务中间件的开发方法及注意事项,我们从核心概念到实战细节逐层拆解,确保逻辑闭环。

一、中间件核心基础(先立框架)

1. 本质定义

中间件是构成ASP.NET Core请求处理管道的可重用组件,每个中间件负责处理请求的一个特定环节(如认证、日志、路由),并决定是否将请求传递给下一个中间件(洋葱模型)。

  • 核心关键词:管道(Pipeline)、洋葱模型、顺序执行、短路(Short-circuit)。

  • 核心方法:await next(context) 是请求向下传递的唯一入口,其前后分别对应“请求进入阶段”和“响应返回阶段”。

2. 管道执行流程(可视化)

graph TD A[请求进入] --> B[中间件1(请求阶段)] B --> C[中间件2(请求阶段)] C --> D[终端中间件(如路由/业务接口)] D --> E[中间件2(响应阶段)] E --> F[中间件1(响应阶段)] F --> G[响应返回]
    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. 中间件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),如开发环境打印详细日志,生产环境返回友好提示;

  • 生产环境禁止暴露敏感信息(如异常堆栈、数据库连接字符串)。

五、中间件的最佳实践(总结)

  1. 单一职责:一个中间件只做一件事(如日志中间件只记录日志,权限中间件只做校验);

  2. 可测试性:强类型中间件通过构造函数注入依赖,方便单元测试;

  3. 可配置化:通过IConfiguration读取配置(如权限白名单、日志级别),避免硬编码;

  4. 文档化:关键中间件需注释说明作用、注册顺序、依赖服务;

  5. 复用性:通用中间件(如日志、异常处理)封装成类库,跨项目复用。

总结

  1. 中间件的核心是管道顺序+洋葱模型,内置中间件的注册顺序遵循“通用逻辑在前、业务逻辑在后”,异常处理必须最前,终端中间件最后;

  2. 自定义中间件有内联(简单)和强类型(复用)两种方式,开发时需注意线程安全、依赖生命周期、异常处理;

  3. 关键避坑点:单例中间件不存储请求状态、避免短路逻辑误用、区分环境适配、保证线程安全。

  • ps:个人经验总结,目前仅限个人参考。如果能帮到你我也会很开心
posted on 2026-01-21 09:59  刘怀日  阅读(1)  评论(0)    收藏  举报