c#原生event+delegate可实现发布订阅,需用private字段封装event、判空调用、及时反订阅防泄漏;mediatr适用于需管道中间件的场景,channel适合后台批量消费,事件总线非万能解耦方案。

用 event + delegate 实现最简发布订阅
不用引入任何第三方库,C# 原生就能跑通核心逻辑。关键不是“怎么写”,而是“谁该负责订阅、谁该触发、委托签名要不要泛型”。
常见错误是把 event 声明成 public 字段——这会让外部直接赋值覆盖所有监听器,彻底破坏订阅机制。必须用 private 字段 + public 事件包装器,或直接用自动实现的 event。
-
delegate类型推荐用Action<t></t>或自定义泛型委托,避免为每种消息类型写一堆非泛型委托 - 触发事件前务必判空:
MyEvent?.Invoke(data),否则空引用异常在生产环境很难定位 - 如果发布者生命周期短于订阅者(比如窗体关闭后后台任务还在发消息),记得在释放时调用
-=反订阅,否则引发内存泄漏
public class NewsPublisher
{
public event Action<string> NewsPublished;
public void Publish(string news) => NewsPublished?.Invoke(news);
}</string>
用 MediatR 替代手写 EventBus 的真实理由
MediatR 不是“更高级的 EventBus”,它是把请求/响应、通知、管道中间件这几层职责拆清楚了。如果你只想要“发个消息让别人收到”,用它反而绕路;但如果你需要“发完消息后统一加日志、验证、事务包装”,它就省掉大量胶水代码。
容易踩的坑是混淆 IRequestHandler 和 INotificationHandler:前者是一对一同步处理(如保存订单),后者才是一对多广播(即传统意义上的发布订阅)。
- 注册必须用
AddMediatR(Assembly),漏掉程序集会导致Handler not found错误,且无明确提示 -
INotification默认异步执行,但不会自动 await 所有 handler —— 如果某个 handler 抛异常,其他 handler 仍会继续执行,这点和原生event不同 - 不要在 handler 里直接 new DbContext,MediatR 的 scope 生命周期和 ASP.NET Core 的 request scope 对齐,应通过构造函数注入
System.Threading.Channels 适合什么场景下的“发布订阅”
当你的“订阅者”其实是消费者线程(比如后台工作队列、批量处理任务),而不是 UI 更新或业务逻辑响应时,Channel<t></t> 比事件模型更合适。它天然支持背压、限流、取消,且不依赖对象引用关系。
典型误用是拿它替代 UI 层的消息通知——因为 Channel.Reader.ReadAsync() 是 awaitable 的,但你没法保证 UI 线程去 await 它,容易卡主线程或引发跨线程访问异常。
- 创建 channel 时优先选
Channel.CreateBounded<t>(new BoundedChannelOptions(capacity))</t>,无界 channel 在突发流量下可能吃光内存 - 消费者端必须用
await foreach (var msg in channel.Reader.ReadAllAsync(ct)),别手动循环调TryRead,否则 CPU 空转 - 如果多个消费者共用一个
Reader,它们会竞争消费同一条消息;要广播得每个消费者持有一个独立Reader
为什么不该把 EventBus 当成万能解耦银弹
松耦合是有代价的:调试时看不到调用栈,单元测试得 mock 大量 handler,消息顺序和重试逻辑得自己补全。很多团队最后发现,“模块 A 直接调用模块 B 的接口”比“A 发消息、B 订阅、B 再调 C”的链路更可控、更易测。
真正值得上 EventBus 的场景其实很窄:跨边界通信(如领域事件出界)、异步解耦(如发邮件不阻塞主流程)、或者事件溯源架构中作为事实来源。
- 别在同一个类库里混用多种 EventBus(比如既有 MediatR 又有自研事件总线),路由规则和生命周期管理会打架
- 事件命名别带动词过去式(如
UserCreated),而要用现在完成态(UserCreatedEvent),否则和命令(CreateUserCommand)语义混淆 - 所有事件类必须是不可变的 record 或只读属性 class,避免某个 handler 改了数据影响后续 handler
消息总线不是用来消除依赖的,是用来把隐式依赖变成显式契约的。契约越模糊,后期维护成本越高。










