责任链需手动组装与控制,handler必须返回bool以明确流转信号,di注册须显式保序,禁止动态解析,状态handler应避免单例复用,审批流需用approvalresult替代bool,横切关注点应分离至aop,链尾须有兜底且异步需统一签名。

责任链在 C# 里不是“注册完就自动跑起来”的魔法机制,它是一条你亲手组装、手动控制走向的请求流水线;用错抽象方式、漏掉终止信号、或 DI 中乱序注册,任意一项都会让链在运行时静默失效。
Handler 基类必须返回 bool,且基类统一调用 _next?.Handle()
别写 void Handle(Request request)——它无法表达“我处理完了”还是“我不管,你接着传”。bool 是最轻量、最可控的流转信号:
-
true表示已终结,后续节点不应执行 -
false表示不处理,交由_next?.Handle(request)继续流转 - 基类必须封装
_next?.Handle(request)调用逻辑,子类只专注if (CanHandle(request)) { ... return true; } - 高频错误:条件不满足时直接写
return false;,等于把请求丢进黑洞;正确写法是结尾统一return _next?.Handle(request) == true;
.NET 的 GetServices<ihandler>()</ihandler> 不保证顺序,必须显式保序
哪怕你在 ConfigureServices 里按顺序 AddSingleton<ihandler authhandler>()</ihandler> 再 AddSingleton<ihandler validationhandler>()</ihandler>,运行时 GetServices<ihandler>()</ihandler> 返回的仍是无序集合。生产环境必须绕过这个坑:
- 用
ChainBuilder类在注册阶段链式构建:services.AddSingleton<ihandler>(sp => new AuthHandler()).AddSingleton<ihandler>(sp => new ValidationHandler())</ihandler></ihandler>,再按注册顺序取 - 禁止在
Handle()内部调用sp.GetService<ihandler>()</ihandler>动态解析——会绕过生命周期管理,还可能触发循环依赖 - 若
Handler有状态(如缓存上下文),必须注册为Scoped或每次新建;复用单例 + 状态 = 并发脏读
审批流不能只靠 bool,得用 ApprovalResult 带元数据与中断语义
真实业务中,“拒绝”和“超时”不是终点,而是需要路由、补偿、升级的事件。裸 bool 会丢失关键信息:
-
ApprovalResult应含IsApproved: bool、Reason: string、NextStep?: string、Metadata: Dictionary<string object></string> - 超时必须由调度器统一检查:
ApprovalContext.StartedAt + CancellationToken注入远程调用,而非每个Handler自己计时 - 拒绝不等于终止:可返回
ApprovalResult.Reject("BUDGET_EXCEEDED")触发前端跳转补材料,而非简单false - 同一
ContextId重复进链时,应查缓存直接返回历史ApprovalResult,否则幂等性崩塌
拦截器和责任链必须分离,Attribute + AOP 才是正解
权限校验、参数验证、审计日志这些横切关注点,不是审批逻辑的一部分,但又必须在每步前后执行。混在一起写会导致:
- 职责污染:每个
IApprovalStep.Handle()里手动写CheckPermission()和LogAudit() - 难以复用:无法跨服务统一管控,比如 Web API 和 gRPC 需要两套重复逻辑
- 测试困难:单元测试要 mock 所有横切行为,而不是聚焦审批规则本身
- 正确做法是剥离为独立
AuthorizationFilterAttribute或IAuthorizationRequirement,用中间件或 AOP 框架(如 Scrutor + AspectCore)统一织入
最易被忽略的是:链尾没有兜底处理器,或 Handler 内部用了异步方法却没统一用 async Task<bool></bool> 签名——前者导致请求静默丢失,后者在 ASP.NET Core 中直接引发 InvalidOperationException 或上下文丢失。











