最小可用事件接口需满足:IEvent为空标记接口;事件类为public class而非record;避免含IDisposable对象。同步发布须用ConcurrentDictionary和ToArray快照,异步需单独await并捕获异常,EventBus必须注册为Singleton。

直接用 IEventBus.Publish 就能发事件,但不加约束、不处理线程安全、不隔离异常,上线后大概率会遇到订阅者丢失、主线程被卡死、内存泄漏三连击。
怎么定义一个最小可用的事件接口
别一上来就抄完整版 IAsyncEventHandler,先保证事件能被识别、能被路由。核心就两条:
-
IEvent必须是空标记接口,不能带属性或方法,否则泛型推导会失败 - 事件类必须是
public class,不能是record(.NET 6+ 虽支持,但部分总线实现对record的类型反射不稳定) - 避免在事件里放
IDisposable对象(比如Stream、DbContext),发布后生命周期不可控
示例:
public interface IEvent { }
public class OrderCreatedEvent : IEvent
{
public Guid OrderId { get; set; }
public string CustomerId { get; set; }
// 不要写 public FileStream LogFile { get; set; }
}
同步发布时为什么订阅者偶尔收不到事件
常见于多线程环境下的字典读写竞争——比如 Dictionary<type list>></type> 在 Publish 时遍历,同时另一个线程在 Subscribe,可能触发 InvalidOperationException: Collection was modified。
- 必须用
ConcurrentDictionary<type concurrentbag>></type>替代普通字典 -
Publish里不能直接 foreach,要先.ToArray()快照当前处理器列表 - 如果用
lock,锁粒度必须小到只包住“取处理器列表”这一步,别把整个 handler 执行也锁进去
异步事件处理器里异常没被捕获怎么办
await handler.HandleAsync(event) 抛异常后,若没显式 try/catch,会导致 Task 进入 Faulted 状态,且不会向上传播——发布线程完全感知不到,就像事件静默消失了。
- 必须在总线内部对每个
Task单独await并catch,不能用Task.WhenAll一类批量 await - 异常不能吞掉:至少记录
ILogger.LogError,最好提供IAsyncEventHandler<t>.HandleException</t>回调供业务自定义降级逻辑 - 不要让异常阻塞后续订阅者:一个 handler 失败,其他 handler 仍需继续执行
依赖注入注册 EventBus 时最常漏的一件事
很多人把 EventBus 注册成 Scoped 或 Transient,结果发现跨 HTTP 请求的事件收不到,或者每次请求都新建一个总线实例,订阅关系完全不共享。
-
EventBus实例必须是Singleton,否则订阅者和发布者根本不在同一个对象上操作 - 但订阅者自身可以是
Scoped(比如InventoryService),只要它们通过构造函数注入的IEventBus是同一个单例实例即可 - 如果要用 MediatR 这类第三方库,注意它默认也是 Singleton,但它的
INotificationHandler<t></t>是 Scoped 生命周期,别混淆概念
真正难的不是写通第一版总线,而是当几十个服务都开始往里扔事件时,谁在什么时候订阅、谁处理慢了、哪个事件被重复消费——这些得靠日志埋点和事件 ID 追踪,光靠接口定义解决不了。


















