设计模式实战(六):责任链模式——ASP.NET Core 中间件的真相

写在前面

这是本系列的重头戏。因为责任链模式在 .NET 后端有一个教科书级的应用——ASP.NET Core 的中间件管道。你天天写 app.UseAuthentication()app.UseRouting(),它们的底子就是责任链。

读完这篇,你不光懂这个模式,还会"从原理上"看透你写过的那 5 篇 ASP.NET Core 笔记里的中间件机制。


一、问题:一个请求,多种处理,谁来管?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
场景:一个 HTTP 请求进来,要依次经过:
  异常处理 → 日志 → 鉴权 → 路由 → 限流 → 缓存 → 业务处理

  朴素写法(在入口方法里一堆 if/调用):
    Handle(req) {
      ExceptionFilter(req);
      Logger(req);
      Auth(req);
      Route(req);
      ...
    }
  病:
  ✗ 顺序写死在代码里,改顺序要动入口
  ✗ 每加一环改这里,违反开闭
  ✗ 处理者互相耦合,没法单独插拔
  ✗ 谁该处理、处理到哪停、怎么传递——全糊在一起

  病根:把"一串处理者"硬编进了一个方法。

二、责任链:处理者串成链,请求沿链传递

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
意图:把多个处理者串成链,请求沿链传递。
     每个处理者决定:自己处理、处理完往下传、还是直接短路。

  结构:
    IHandler                         ← 处理者接口
    ConcreteHandler(持有一个 next)  ← 处理 + 决定是否传给下一个

  关键:
    - 发送者不知道谁最终处理(解耦)
    - 每个处理者只关心自己的事 + 要不要传
    - 顺序由链的组装决定(可配置)
    - 任何一环可以"短路"(不调 next,终止链)

  适用:请求要经过一串处理步骤、顺序敏感、可能中途短路。

三、经典手写

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 处理者接口:处理 + 持有下一个
public abstract class Handler
{
    private readonly Handler? _next;
    protected Handler(Handler? next) => _next = next;
    public virtual void Handle(Request req)
    {
        _next?.Handle(req);   // 默认:只往下传
    }
}

// 具体处理者:鉴权
public class AuthHandler(Handler? next) : Handler(next)
{
    public override void Handle(Request req)
    {
        if (!req.IsAuthenticated)
        {
            req.Respond(401);
            return;   // 短路:不调 base.Handle,链终止
        }
        base.Handle(req);   // 通过 → 传给下一个
    }
}

// 具体处理者:限流
public class RateLimitHandler(Handler? next) : Handler(next)
{
    public override void Handle(Request req)
    {
        if (OverLimit(req)) { req.Respond(429); return; }
        base.Handle(req);
    }
}

// 组装链(顺序在这里定,可配置)
var chain = new AuthHandler(
            new RateLimitHandler(
            new BusinessHandler(null)));
chain.Handle(request);
1
2
3
4
重点看两种处理者的行为:
  - 通过 → base.Handle(req) → 传给 next
  - 拒绝 → return → 短路,next 不执行
  发送方只管 chain.Handle(request),不关心谁处理、在哪停。

四、重头:ASP.NET Core 中间件 = 责任链

ASP.NET Core 的请求管道,就是责任链模式的工业级实现。我写过 5 篇 ASP.NET Core 笔记,这里把它的原理彻底拆开。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 你写的中间件长这样:
app.Use(async (context, next) =>
{
    // —— 进站:请求往里走时执行 ——
    Console.WriteLine("请求进入: " + context.Request.Path);

    await next();   // ← 把控制权交给链的下一个中间件

    // —— 出站:响应往回走时执行 ——
    Console.WriteLine("响应状态: " + context.Response.StatusCode);
});
1
2
3
4
5
6
7
这段代码的每一部分,对应责任链的每一环:

  next          → 就是责任链里的 "_next","下一个处理者"
  await next()  → base.Handle(),把请求传下去
  不调 next     → 短路(如 app.Run,或鉴权失败直接 401 返回)
  进站逻辑      → 处理者"自己的事"
  出站逻辑      → next() 返回后执行(洋葱模型,后面讲)

4.1 洋葱模型(Onion / Pipeline)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
请求像一个箭头穿入、再穿出洋葱:

       ┌─ 异常处理(最外层,最后兜底)
       │  ┌─ 日志
       │  │  ┌─ 鉴权
       │  │  │  ┌─ 路由 → 控制器
       │  │  │  │
  请求 →  →  →  →  →  →  →  → 处理
       │  │  │  │
       │  │  │  └─ 出站:鉴权后置逻辑
       │  │  └──── 出站:日志记录响应
       │  └─────── 出站:异常处理收尾
       └──────────

  每个中间件 next() 之前的代码 = "进站"(按外→内顺序执行)
                next() 之后的代码 = "出站"(按内→外顺序执行)

  → 这就是为什么"鉴权放最后注册能拦住所有人""日志放最外层能记到所有响应"
     注册顺序(UseAuthentication、UseRouting 的先后)决定一切。

4.2 中间件类的写法(封装成类)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 约定俗成的中间件类写法
public class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;   // ← 这就是"下一个处理者"
    public RequestTimingMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext ctx)
    {
        var sw = Stopwatch.StartNew();
        try { await _next(ctx); }              // 传给下一个
        finally { sw.Stop(); Log($"{ctx.Request.Path} {sw.ElapsedMilliseconds}ms"); }
    }
}
// 注册
app.UseMiddleware<RequestTimingMiddleware>();
1
2
3
4
5
RequestDelegate = "下一个处理者"的类型别名:
  public delegate Task RequestDelegate(HttpContext context);
  → 它就是 Handler.Handle(Request) 的现代化身。

  整个 ASP.NET Core 管道,就是一条由 RequestDelegate 串起来的责任链。

五、为什么用责任链(而不是别的)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
中间件用责任链的好处:

  ✓ 解耦:每个中间件只管自己的事(鉴权、限流、日志互不知)
  ✓ 可插拔:加/删中间件 = 加/删一行注册,业务代码不动(开闭)
  ✓ 顺序可控:注册顺序即执行顺序,可配置
  ✓ 短路能力:鉴权失败 / 限流触发 → 不调 next,链终止
  ✓ 洋葱模型:进站、出站都能做事(前置 + 后置逻辑)

  这五条,正好是一个 Web 框架处理"请求-响应"最需要的能力。
  所以几乎所有主流 Web 框架的请求管道都是责任链变体
  (Express/Koa、Spring Filter、ASP.NET Core 中间件)。

六、责任链 vs 装饰器 vs 策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
容易混,一次分清:

  责任链:多个处理者串起来,请求沿链走,可短路
    → 重点:流程/管道,顺序敏感(鉴权→路由→业务)
    → ASP.NET Core 中间件

  装饰器:层层包裹,每层加功能,不短路(一定传到内核)
    → 重点:增强单一对象(日志/缓存/重试)
    → Stream、IRepository 装饰

  策略:从一组算法里选一个执行
    → 重点:多选一(折扣/支付方式)
    → 不串链、不叠加

  鉴别:
    "要经过一串步骤,可能中途停" → 责任链
    "要给一个对象叠加功能"       → 装饰器
    "要从几种做法里选一种"       → 策略

七、何时用 / 何时别用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
该用责任链:
  ✓ 请求要经过有序的多步骤处理(Web 管道、审批流、事件处理链)
  ✓ 步骤可插拔、顺序可配置
  ✓ 需要"任一环可决定终止"的短路能力
  ✓ 处理者之间要解耦

别用:
  ✗ 步骤固定、就两三步、顺序不变 → 直接调函数更清楚
  ✗ 是"选一种算法"而非"经过一串" → 那是策略
  ✗ 是"叠加功能"而非"流程传递"   → 那是装饰器

  信号:当你写出一个超长的 Handle() 方法,
       里面对一个请求依次做 N 件不同的事,还可能提前 return ——
       拆成责任链。

八、小结

  • 问题:一串处理步骤硬编在一个方法里,顺序死、难扩展
  • 责任链:处理者串链,请求沿链传递,每环可处理/传递/短路
  • 经典手写:Handler 抽象 + 持有 next + Handle 转发或短路
  • ASP.NET Core 中间件就是责任链next = 下一个处理者,RequestDelegate = Handler
  • 洋葱模型:next() 前是进站、后是出站,注册顺序决定执行顺序
  • 核心价值:解耦、可插拔、顺序可控、短路能力
  • vs 装饰器/策略:责任链是"流程",装饰器是"增强",策略是"多选一"
  • 这是 ASP.NET Core 管道的原理,看透了它,5 篇 ASP.NET Core 笔记里的中间件就全通了

下一篇讲观察者模式——C# 的 event 委托天生就是观察者,以及它和 DDD 领域事件的关系。