资讯动态

MediatR源码解读(一):程序集扫描与Handler注册背后的泛型协变魔法

发布时间:2026/9/19 21:01:29 来源:尧图企业网站定制
MediatR源码解读一程序集扫描与Handler注册背后的泛型协变魔法【免费下载链接】MediatRSimple, unambitious mediator implementation in .NET项目地址: https://gitcode.com/gh_mirrors/me/MediatRMediatR 是 .NET 生态中最流行的中介者Mediator模式库用简单、不冒进的设计把请求处理器Handler与调用方解耦。本文深入 MediatR 源码解读它启动时如何扫描程序集、把成百上千个 Handler 自动注册进依赖注入容器以及泛型逆变Covariance/Contravariance如何让一个 Handler 处理一族请求成为可能。无需逐行读懂反射也能理解这套注册魔法的完整链路。一、一行 AddMediatR 背后发生了什么大多数人在项目中只写过一行代码services.AddMediatR(cfg cfg.RegisterServicesFromAssemblyContainingPing());就是这一行MediatR 就完成了三件事入口在 MediatRServiceCollectionExtensions.cs校验至少要提供一个待扫描的程序集否则直接抛出参数异常扫描注册调用ServiceRegistrar.AddMediatRClassesWithTimeout扫描所有 Handler 并注册补全基础设施调用ServiceRegistrar.AddRequiredServices注册IMediator、ISender、IPublisher以及异常处理、前后置处理器等管道行为。 记住这条链路AddMediatR → 扫描程序集 → 找到 Handler → 注册为瞬态服务。后面三节分别拆解。二、程序集扫描三部曲容忍加载失败 递归查接口第 1 步安全地取出所有类型扫描的核心是拿到程序集中的全部类型。MediatR 没有直接调用assembly.GetTypes()而是封装了一个更健壮的方法见 ServiceRegistrar.csprivate static IEnumerableType GetLoadableDefinedTypes(this Assembly assembly) { try { return assembly.DefinedTypes; } catch (ReflectionTypeLoadException ex) { return ex.Types.OfTypeType(); } }为什么这么写大型程序集中只要有一个类型依赖缺失例如某个平台包没引用GetTypes()就会整体失败。这里捕获ReflectionTypeLoadException后只保留能加载的类型让扫描优雅降级而不是启动崩溃——这是开源库处理反射时的一个典范细节。第 2 步递归搜索闭包接口拿到类型后MediatR 用FindInterfacesThatCloseServiceRegistrar.cs判断一个类是否实现了IRequestHandler,等开放泛型模板。它的逻辑是一个沿基类链向上递归的算法检查当前类型实现的所有泛型接口其泛型定义是否等于模板如IRequestHandler,匹配到就收下来然后继续检查基类直到object为止。这意味着 Handler 可以通过继承中间基类间接实现接口依然能被扫描到。配合IsConcrete()排除抽象类和接口扫描只保留能 new 出来的实现类。第 3 步分类落袋——封闭类型 vs 开放泛型ConnectImplementationsToTypesClosingServiceRegistrar.cs把扫描结果分成两桶桶内容注册方式concretions普通封闭类如PingHandler : IRequestHandlerPing, Pong按接口精确注册TryAddTransient防重复genericConcretions开放泛型类如AuditHandlerT : IRequestHandlerT, ... where T : IAuditable延迟处理见第四节对于一个请求多个 Handler的场景通知、异常处理器参数addIfAlreadyExists true会让它们全部注册用AddTransient而非TryAddTransient最终由容器以IEnumerable的形式批量解析——这也是为什么 INotificationHandler.cs 的设计天然是一对多的。三、变体魔法为什么一个 Handler 能处理一族请求这是 MediatR 最精妙、也最容易被忽略的设计藏在接口定义的一个关键字里。看 IRequestHandler.cspublic interface IRequestHandlerin TRequest, TResponse where TRequest : IRequestTResponse注意那个in—— 它声明TRequest是**逆变contravariant**位置标题中的协变是泛型变体机制的统称。逆变带来的直接好处是IRequestHandlerOrder, string // 处理父类请求 // 在运行时可以赋值给 IRequestHandlerVIPOrder, string // 处理子类请求也就是说只要写了一个处理Order的 Handler当 MediatR 用反射查找IRequestHandlerVIPOrder, string时同一个 Handler 实例就能被容器解析出来。这个能力在扫描时由CanBeCastTo内部就是IsAssignableFrom落地var exactMatches concretions.Where(x x.CanBeCastTo(interface)).ToList();IsAssignableFrom比较类型时会自动套用泛型变体规则所以子类请求 → 父类 Handler的匹配是免费获得的MediatR 一行额外代码都没写。 实战意义做审计、权限校验这类横切逻辑时给请求定义一个公共接口如IAuditable写一个针对基接口/父类型的 Handler所有派生请求全部被覆盖。样本代码 GenericHandler.cs 正是这个思路一个INotificationHandlerINotification接住全部通知。还有一个容易踩的坑在 IRequestHandler.cs 的第二个参数TResponse没有in修饰符是**不变invariant**的。变体是双向约束——TRequest逆变保证父 Handler 能接子类请求TResponse不变则保证响应类型必须精确一致防止返回类型被意外替换。这个细节决定了注册时响应类型必须与请求声明的IRequestTResponse完全对上。四、开放泛型 Handler组合爆炸与四道安全闸最魔幻的场景是用泛型写通用 Handlerpublic class AuditHandlerT : IRequestHandlerT, Unit where T : IAuditable { ... }问题来了IAuditable可能有 10 个实现那么要注册 10 个具体化版本100 个呢MediatR 的答案是——在启动时穷举所有合法组合逐一具体化注册。流程在AddAllConcretionsThatClose→GetConcreteRequestTypesServiceRegistrar.cs读取开放泛型 Handler 每个泛型参数的约束如where T : IAuditable在程序集中筛出所有满足约束的具体类得到每个参数各自的候选列表用递归GenerateCombinations做笛卡尔积生成全部组合对每个组合调用MakeGenericType具体化逐条AddTransient注册。比如请求是ReportRequestTData, TFormatTData有 3 种、TFormat有 2 种就会自动注册 3×26 个 Handler。四道安全闸防止注册风暴组合数是乘法增长的一个手滑就能让应用启动卡死。MediatR 在 MediatrServiceConfiguration.cs 中提供了四道可配置的保险丝配置项默认值作用MaxGenericTypeParameters10限制单个泛型请求的参数个数MaxTypesClosing100限制单个参数可闭合的类型数MaxGenericTypeRegistrations125000限制总注册组合数RegistrationTimeout15000ms整个注册过程的总超时超时抛TimeoutException任何一道闸触发都会抛出带明确信息的ArgumentException/TimeoutException告诉你是哪个类型、哪个参数越界。配套的单元测试甚至用AssemblyBuilder动态生成越界的程序集来验证这些防线见 BaseGenericRequestHandlerTests.cs 和 GenericRequestHandlerTests.cs覆盖全部组合都注册、数量正确、无重复三类断言。⚠️ 新手提醒开放泛型 Handler 默认不启用RegisterGenericHandlers默认为false需要在配置中显式打开这正是防误伤的又一重保险。五、收尾AddRequiredServices 与运行时解析扫描完成后AddRequiredServicesServiceRegistrar.cs负责基础设施装配其中两处值得留意管道顺序先注册前置处理器PreProcessor再后置处理器PostProcessor最后才是普通 Behavior保证请求按前置 → 处理 → 后置顺序流经管道异常处理器的策略开关RequestExceptionActionProcessorStrategy决定异常捕获器排在异常处理器之前还是之后默认只对未被捕获的异常生效避免与常规 try/catch 语义冲突。注册是建索引运行才是查字典。看 Mediator.cs 的Send它并不每次反射而是用ConcurrentDictionaryType, ...按请求类型缓存一个 Handler 委托包装器首次解析时生成并缓存。所以注册阶段的繁重扫描只付一次成本热路径上Send几乎是一次字典查找 一次容器解析——这是 MediatR 性能友好的关键。六、源码导读路线图把本文内容对应到仓库文件建议按此顺序阅读顺序文件看点1MediatRServiceCollectionExtensions.csAddMediatR入口与整体流程2MediatrServiceConfiguration.cs所有配置项与默认值3ServiceRegistrar.cs扫描、变体匹配、组合生成的全部魔法4IRequestHandler.cs / INotificationHandler.cs接口上的in变体声明5Mediator.cs运行时解析与委托缓存6samples/MediatR.Examples/Ping/Pong 等可运行示例小结MediatR 的程序集扫描 Handler 注册看似一行AddMediatR的魔法实际是四层设计叠加的结果容错反射保证扫描不崩、接口模板递归匹配保证找得全、泛型逆变保证一个 Handler 覆盖一族请求、四道限流闸保证组合爆炸可控。理解了这条注册链路你再看 MediatR 的管道行为Pipeline Behavior与自定义IPipelineBehavior就会顺理成章。下一篇将深入管道机制请求是如何在 PreProcessor、Handler、PostProcessor 之间流动并被异常处理器拦截的。【免费下载链接】MediatRSimple, unambitious mediator implementation in .NET项目地址: https://gitcode.com/gh_mirrors/me/MediatR创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价