写在前面
这是本系列的重头戏。因为责任链模式在 .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 领域事件的关系。