首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >攻克 ASP.NET Core Minimal API:中间件与过滤器的极致实战指南

攻克 ASP.NET Core Minimal API:中间件与过滤器的极致实战指南

原创
作者头像
步步为营DotNet
发布2026-08-07 13:46:20
发布2026-08-07 13:46:20
1260
举报

攻克 ASP.NET Core Minimal API:中间件与过滤器的极致实战指南

在 ASP.NET Core Minimal API 中,中间件(Middleware)Endpoint 过滤器(Endpoint Filters) 是处理 HTTP 请求管道(Pipeline)的两大核心利器。

很多初学者容易混淆它们:

  • 什么时候该用中间件?什么时候该用过滤器?
  • 为什么它们都有 context 参数,这两个 context 到底有什么本质区别?
  • 它们的各个参数到底代表什么意思?

读完这篇文章,你将彻底掌握 Minimal API 中间件与过滤器的每一个细节、Context 参数核心差异与最佳实践

一、 核心概念对比:中间件 vs Endpoint 过滤器

在深入代码之前,我们先搞清楚二者的界限与分工:

代码语言:javascript
复制
 客户端请求 ---> [中间件 1] ---> [中间件 2] ---> [路由匹配] ---> [过滤器 1] ---> [过滤器 2] ---> [Endpoint Handler]
                                                                                                     |
 客户端响应 <--- [中间件 1] <--- [中间件 2] <------------------- [过滤器 1] <--- [过滤器 2] <--------+

维度

中间件 (Middleware)

Endpoint 过滤器 (Endpoint Filter)

作用层级

全局管道(在路由匹配之前或之后运行)。

Endpoint / 路由组层级(路由匹配成功后运行)。

感知能力

无法感知具体匹配到了哪个 Controller/Action 的元数据或强类型参数。

精准感知已被框架解析好的路由参数(Arguments)与 EndpointMetadata。

核心 Context

HttpContext(底层 HTTP 协议上下文)。

EndpointFilterInvocationContext(高级调用上下文)。

典型场景

全局异常捕获、CORS、身份认证、静态文件响应、全局日志。

特定接口的强类型参数校验(Validation)、业务鉴权、响应格式统一包装。

二、 核心痛点拆解:HttpContext vs InvocationContext 参数异同

同样是接收 context 参数,中间件与过滤器的上下文设计维度截然不同:

1. 对比表

对比维度

中间件的 HttpContext

过滤器的 EndpointFilterInvocationContext

本质定位

原始 HTTP 协议的封装。

目标 API Handler 执行过程的上下文封装。

参数获取方式

原始字符串/流。只能拿到 Request.Query["id"] 或读取 Request.Body 字节流(需自己反序列化)。

强类型 C# 对象。可以直接通过 GetArgument<T>(index) 拿到框架已反序列化好的 DTO 或参数。

对 Route 的感知

早期阶段无法感知(发生在路由匹配前)。后期阶段虽能拿到 Endpoint 终节点,但操作繁琐。

原生天然感知。直接绑定在特定 Endpoint 上,清楚知道即将执行哪个方法、哪些参数。

返回值处理

直接写 Response 流,通过设置 StatusCode 或 WriteAsync 做出响应。

拦截并返回 IResult(如 Results.BadRequest()),对 Minimal API 原生类型更友好。

2. 直观代码对比例子:拦截并校验请求参数 id

假设有一个接口 POST /orders/{id},如果 id <= 0,需要拦截并返回 400 Bad Request

❌ 用中间件来实现:
代码语言:javascript
复制
 app.Use(async (HttpContext context, RequestDelegate next) =>
 {
     // 中间件拿到的是原始 Path 字符串,需要自己写正则或手动拆分字符串解析 id!
     var path = context.Request.Path.Value; // "/orders/0"
     
     if (path != null && path.StartsWith("/orders/"))
     {
         var idSegment = path.Split('/').LastOrDefault();
         if (int.TryParse(idSegment, out int id) && id <= 0)
         {
             // 中间件无法使用 Results.BadRequest(),必须手写 Response 流
             context.Response.StatusCode = StatusCodes.Status400BadRequest;
             context.Response.ContentType = "application/json";
             await context.Response.WriteAsJsonAsync(new { Error = "ID 必须大于 0" });
             return; // 短路,不调用 next
         }
     }
 ​
     await next(context);
 });
✅ 用 Endpoint 过滤器来实现(优雅、强类型、开箱即用):
代码语言:javascript
复制
 app.MapPost("/orders/{id:int}", (int id) => Results.Ok($"Order {id}"))
    .AddEndpointFilter(async (EndpointFilterInvocationContext context, EndpointFilterDelegate next) =>
    {
        // 1. 直接从 context.Arguments 中强类型提取 id,框架已经帮你解析并转成了 int!
        var id = context.GetArgument<int>(0);
 ​
        if (id <= 0)
        {
            // 2. 直接返回 Minimal API 熟悉的 Results 对象打断请求
            return Results.BadRequest(new { Error = "ID 必须大于 0" });
        }
 ​
        return await next(context);
    });

核心结论中间件看的是“HTTP 请求流”,过滤器看的是“C# 方法与强类型参数”。需要处理底层网络协议/全局事件用中间件;需要校验、处理业务参数用过滤器。

三、 彻底讲透:Minimal API 中的中间件 (Middleware)

中间件在 Minimal API 中主要通过 app.Use(...) 来定义。我们重点拆解最核心的 app.Use(Func<HttpContext, RequestDelegate, Task>)

1. 代码模板与参数全解

代码语言:javascript
复制
 var builder = WebApplication.CreateBuilder(args);
 var app = builder.Build();
 ​
 // 自定义中间件
 app.Use(async (HttpContext context, RequestDelegate next) =>
 {
     // 【1. 请求前置处理】
     Console.WriteLine($"[Middleware IN] Path: {context.Request.Path}");
 ​
     // 【2. 调用管道中的下一个中间件】
     await next(context);
 ​
     // 【3. 响应后置处理】
     Console.WriteLine($"[Middleware OUT] Status: {context.Response.StatusCode}");
 });
 ​
 app.MapGet("/hello", () => "Hello World!");
 ​
 app.Run();

2. 每个参数的深度拆解

HttpContext context(HTTP 上下文对象)

它是整个请求生命周期中最核心的对象,包含了与当前 HTTP 请求相关的所有数据

  • context.Request:请求信息。
    • context.Request.Path(请求路径,如 /api/orders
    • context.Request.Headers(请求头,可读取 Authorization 等)
    • context.Request.Query(查询参数,如 ?id=123
    • context.Request.Body(请求体 Stream,读取时注意 Seek/Position 指针
  • context.Response:响应信息。
    • context.Response.StatusCode(状态码,如 200, 401, 500)
    • context.Response.Headers(设置响应头,如 X-Process-Time
    • context.Response.WriteAsync(...)(直接向客户端写入响应内容)
  • context.RequestServices当前请求的作用域 DI 容器(IServiceProvider。如果需要在中间件中解析 Scoped 服务(如 DbContext),必须通过 context.RequestServices.GetRequiredService<T>() 获取!
  • context.Items:一个字典(IDictionary<object, object?>),用于在当前请求的各个中间件/过滤器之间传递临时自定义数据
RequestDelegate next(下一个管道节点的委托)

next 是一个异步委托,签名等同于 Func<HttpContext, Task>

  • await next(context);:将请求控制权交给管道中的下一个中间件。
  • 短路(Short-circuiting):如果你不调用 await next(context),请求就会在此中断,后续的中间件和 Endpoint Handler 均不会执行,而是直接原路返回(例如未授权时直接返回 401)。

3. 中间件提取与模块化封装

直接在 Program.csapp.Use 会导致代码臃肿。推荐通过类和扩展方法进行标准封装。

示例:封装一个全局耗时统计中间件
代码语言:javascript
复制
 // 1. 定义中间件类
 public class RequestTimingMiddleware
 {
     private readonly RequestDelegate _next;
     private readonly ILogger<RequestTimingMiddleware> _logger;
 ​
     // 构造函数传入 next 委托和 Singleton/Transient 服务
     public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
     {
         _next = next;
         _logger = logger;
     }
 ​
     // 必须包含名为 InvokeAsync 或 Invoke 的方法,第一个参数必须是 HttpContext
     public async Task InvokeAsync(HttpContext context)
     {
         var stopwatch = System.Diagnostics.Stopwatch.StartNew();
 ​
         // 执行下一个中间件
         await _next(context);
 ​
         stopwatch.Stop();
         _logger.LogInformation("请求 {Path} 耗时: {Elapsed} ms", context.Request.Path,          stopwatch.ElapsedMilliseconds);
     }
 }
 ​
 // 2. 编写扩展方法
 public static class MiddlewareExtensions
 {
     public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder app)
     {
         return app.UseMiddleware<RequestTimingMiddleware>();
     }
 }
 ​
 // 3. 在 Program.cs 中使用
 app.UseRequestTiming();

四、 彻底讲透:Minimal API Endpoint 过滤器 (Endpoint Filters)

Minimal API 引入了原生的 Endpoint Filter,它专门作用于特定 Endpoint 或路由组(Route Group),可以轻松拿到已被解析好的强类型参数

通过 .AddEndpointFilter(...) 为 Endpoint 绑定过滤器。

1. 代码模板与参数全解

我们在一个创建用户的接口上添加过滤器:

代码语言:javascript
复制
 app.MapPost("/users", (UserDto user) => Results.Created($"/users/{user.Name}", user))
    .AddEndpointFilter(async (EndpointFilterInvocationContext context, EndpointFilterDelegate next) =>
    {
        // 【1. 前置逻辑】
        Console.WriteLine("过滤器前置校验开启...");
 ​
        // 【2. 执行下一个过滤器或真正 Handler】
        var result = await next(context);
 ​
        // 【3. 后置逻辑】
        Console.WriteLine("过滤器后置处理完成...");
 ​
        return result;
    });

2. 参数与返回值深度拆解

EndpointFilterInvocationContext context(过滤器调用上下文)

这是过滤器的灵魂所在,提供了获取强类型参数的直接途径:

  • context.Arguments(核心参数列表)
    • 类型为 IList<object?>。它包含了框架已经帮我们解析好的 Handler 参数(比如从 Body 反序列化出来的 UserDto,或者从 Route 拿到的 id)。
    • 顺序与你的 Endpoint Handler 参数声明顺序完全一致
    • 读取方式: // 假设你的 Handler 是 (int id, UserDto dto) => ... var id = context.GetArgument<int>(0); // 方式 1:泛型强类型获取 var dto = context.GetArgument<UserDto>(1); // 方式 2
  • context.HttpContext:当前的 HttpContext 对象,方便获取 Header、URL、DI 容器等。
EndpointFilterDelegate next(下一个过滤器委托)
  • 签名:ValueTask<object?> EndpointFilterDelegate(EndpointFilterInvocationContext context)
  • 执行 var result = await next(context); 会触发后续的过滤器;如果没有后续过滤器,则触发执行具体的 Endpoint Handler。
③ 返回值 ValueTask<object?>
  • 如果前置校验失败,你可以直接返回自定义的 Result,不再调用 next(context),从而打断执行链: if (string.IsNullOrEmpty(dto.Name)) { // 直接返回 BadHttpRequest,打断后续逻辑 return Results.BadRequest(new { Error = "Name 不能为空" }); }
  • 你甚至可以在 await next(context) 之后修改返回结果(例如包装统一的 API Response)。

3. 实现 IEndpointFilter 接口(强类型模块化)

在大型项目中,强烈建议将过滤器拆分为独立的类,实现 IEndpointFilter 接口。

实战场景:自动参数校验过滤器 (Validation Filter)

假设我们结合 FluentValidation 写一个通用的参数校验过滤器:

代码语言:javascript
复制
 using FluentValidation;
 ​
 public class ValidationFilter<T> : IEndpointFilter where T : class
 {
     public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
     {
         // 1. 在参数列表中寻找类型为 T 的待校验对象
         var argToValidate = context.Arguments.OfType<T>().FirstOrDefault();
 ​
         if (argToValidate is not null)
         {
             // 2. 从 HttpContext 的 DI 容器中拿到对应的 Validator
             var validator = context.HttpContext.RequestServices.GetService<IValidator<T>>();
             if (validator is not null)
             {
                 var validationResult = await validator.ValidateAsync(argToValidate);
                 if (!validationResult.IsValid)
                 {
                     // 校验失败,直接拦截并返回 400
                     var errors = validationResult.ToDictionary();
                     return Results.ValidationProblem(errors);
                 }
             }
         }
 ​
         // 3. 校验通过,放行
         return await next(context);
     }
 }
在 Minimal API 中使用:
代码语言:javascript
复制
 // 单个 Endpoint 链式调用
 app.MapPost("/products", (ProductDto product) => Results.Ok(product))
    .AddEndpointFilter<ValidationFilter<ProductDto>>();

五、 高级进阶:路由组过滤器 (Route Group Filters)

如果有几十个 API 都需要应用相同的过滤器(比如 /api/v1/admin/* 下的所有接口都需要管理员鉴权过滤器),一个个写 .AddEndpointFilter 显然不可取。

Minimal API 提供了 Route Group(路由组),可以一次性给一组 Endpoint 挂载过滤器!

代码语言:javascript
复制
 var app = WebApplication.CreateBuilder(args).Build();
 ​
 // 1. 创建路由组,并统一定义前缀与过滤器
 var adminGroup = app.MapGroup("/api/admin")
                     .RequireAuthorization("AdminPolicy") // 授权策略
                     .AddEndpointFilter<AdminAuditLogFilter>(); // 统一挂载审计日志过滤器
 ​
 // 2. 在组内注册具体的 Endpoint,它们会自动继承路由组的所有过滤器!
 adminGroup.MapGet("/users", () => "Admin: All Users");
 adminGroup.MapDelete("/users/{id}", (int id) => $"Deleted User {id}");
 ​
 app.Run();

六、 终极实战:从头打造一个全功能 API 管道

我们把中间件、过滤器、路由组融会贯通,写一段包含完整异常捕获、审计日志和参数校验的现代 Minimal API 应用:

代码语言:javascript
复制
 var builder = WebApplication.CreateBuilder(args);
 var app = builder.Build();
 ​
 // ---------------- 1. 全局中间件阶段 ----------------
 ​
 // 1.1 全局未捕获异常处理中间件
 app.Use(async (context, next) =>
 {
     try
     {
         await next(context);
     }
     catch (Exception ex)
     {
         context.Response.StatusCode = 500;
         await context.Response.WriteAsJsonAsync(new { Error = "服务器内部错误", Detail = ex.Message });
     }
 });
 ​
 // ---------------- 2. 路由组与 Endpoint 阶段 ----------------
 ​
 var orderGroup = app.MapGroup("/orders")
                     .AddEndpointFilter(async (context, next) =>
                     {
                         // 组级过滤器:给所有订单接口加上统一耗时日志
                         var sw = System.Diagnostics.Stopwatch.StartNew();
                         var result = await next(context);
                         sw.Stop();
                         Console.WriteLine($"[Orders API] 耗时 {sw.ElapsedMilliseconds}ms");
                         return result;
                     });
 ​
 // 结合单个 Endpoint 专属过滤器
 orderGroup.MapPost("/", (CreateOrderDto order) => Results.Ok(new { OrderId = 1001, order.Amount }))
           .AddEndpointFilter(async (context, next) =>
           {
               // Endpoint 专属过滤器:校验订单金额
               var order = context.GetArgument<CreateOrderDto>(0);
               if (order.Amount <= 0)
               {
                   return Results.BadRequest(new { Error = "订单金额必须大于 0" });
               }
               return await next(context);
           });
 ​
 app.Run();
 ​
 // DTO 定义
 public record CreateOrderDto(string ProductId, decimal Amount);

七、 避坑指南与最佳实践速查

  1. 别在中间件里盲目读取 context.Request.Body
    • Request.Body 是一个不可重复读取的 Stream(默认 Position = 0)。如果你在中间件读取了它却没有将指针重置,后续 Endpoint Handler 在反序列化时就会报错。如需读取,请先调用 context.Request.EnableBuffering() 并将 Position 重置为 0
  2. 正确选择生命周期与 DI 服务解析
    • 中间件是 Singleton(单例)构造的!绝对不要在中间件的构造函数中直接注入 Scoped 服务(如 DbContext)。必须在 InvokeAsync(HttpContext context) 方法体内部通过 context.RequestServices 解析。
    • Endpoint 过滤器是在请求触发时由 DI 容器实例化的,支持直接在构造函数注入 ScopedTransient 服务。
  3. 性能考量
    • 中间件针对所有请求执行,逻辑必须保持极度轻量,否则会拉低整个应用的 TPS。
    • 能用 Endpoint 过滤器解决的参数校验与路由业务,不要写成全局中间件。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 攻克 ASP.NET Core Minimal API:中间件与过滤器的极致实战指南
    • 一、 核心概念对比:中间件 vs Endpoint 过滤器
    • 二、 核心痛点拆解:HttpContext vs InvocationContext 参数异同
      • 1. 对比表
      • 2. 直观代码对比例子:拦截并校验请求参数 id
    • 三、 彻底讲透:Minimal API 中的中间件 (Middleware)
      • 1. 代码模板与参数全解
      • 2. 每个参数的深度拆解
      • 3. 中间件提取与模块化封装
    • 四、 彻底讲透:Minimal API Endpoint 过滤器 (Endpoint Filters)
      • 1. 代码模板与参数全解
      • 2. 参数与返回值深度拆解
      • 3. 实现 IEndpointFilter 接口(强类型模块化)
    • 五、 高级进阶:路由组过滤器 (Route Group Filters)
    • 六、 终极实战:从头打造一个全功能 API 管道
    • 七、 避坑指南与最佳实践速查
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档