C#怎么实现发布订阅模式 C#如何用事件总线EventBus实现模块间的松耦合消息通信【架构】

冰火之心

冰火之心

2026-03-31

285人浏览

原创

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

c#怎么实现发布订阅模式 c#如何用事件总线eventbus实现模块间的松耦合消息通信【架构】

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”,它是把请求/响应、通知、管道中间件这几层职责拆清楚了。如果你只想要“发个消息让别人收到”,用它反而绕路;但如果你需要“发完消息后统一加日志、验证、事务包装”,它就省掉大量胶水代码。

容易踩的坑是混淆 IRequestHandlerINotificationHandler:前者是一对一同步处理(如保存订单),后者才是一对多广播(即传统意义上的发布订阅)。

  • 注册必须用 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> 比事件模型更合适。它天然支持背压、限流、取消,且不依赖对象引用关系。

C函数速查手册(CHM版)
C函数速查手册(CHM版)

C函数速查手册(CHM版)

下载

典型误用是拿它替代 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

消息总线不是用来消除依赖的,是用来把隐式依赖变成显式契约的。契约越模糊,后期维护成本越高。

相关专题

更多
c#是什么意思
c#是什么意思

C#是一种通用的面向对象编程语言,它在.NET框架上运行,可用于开发各种类型的应用程序。本专题为大家提供c#相关的各种文章、以及下载和课程。

2023.08.08

528

5

什么是c#
什么是c#

C#(读作C Sharp)是一种现代的、通用的、面向对象的编程语言。它由微软公司开发,于2000年首次发布,主要用于开发Windows桌面应用程序、Web应用程序、移动应用程序以及游戏开发等领域。C#是一种类型安全和托管的编程语言,它结合了C++的强大性能和C语言的简单易学特点,并引入了许多新的特性和概念,如委托、属性、泛型和异步编程等,以提高开发效率和代码可维护性。

2023.08.31

1137

3

C#中base关键字的用法
C#中base关键字的用法

在C#中,base关键字用于访问当前类的基类成员,这包括基类的字段、属性、方法、构造函数、索引器以及事件。想了解更多C#的相关内容,可以阅读本专题下面的文章。

2024.03.13

556

8

C#Task用法
C#Task用法

Task在C#中是用于表示异步操作的对象,它位于System.Threading.Tasks命名空间中。想了解更多C#中各种用法的相关内容,可以阅读本专题下面的文章。

2024.03.14

1049

11

C# 并发与异步编程
C# 并发与异步编程

本专题系统讲解 C# 异步编程与并发控制,重点介绍 async 和 await 关键字、Task 类、线程池管理、并发数据结构、死锁与线程安全问题。通过多个实战项目,帮助学习者掌握 如何在 C# 中编写高效的异步代码,提升应用的并发性能与响应速度。

2026.02.03

148

18

C# 多线程与异步编程
C# 多线程与异步编程

本专题深入讲解 C# 中多线程与异步编程的核心概念与实战技巧,包括线程池管理、Task 类的使用、async/await 异步编程模式、并发控制与线程同步、死锁与竞态条件的解决方案。通过实际项目,帮助开发者掌握 如何在 C# 中构建高并发、低延迟的异步系统,提升应用性能和响应速度。

2026.02.06

202

20

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

2026.03.11

526

24

C# Unity游戏开发与ECS架构实践
C# Unity游戏开发与ECS架构实践

本专题聚焦 C# 在 Unity 游戏开发中的应用,系统讲解 ECS(Entity-Component-System)架构设计、游戏对象管理、性能优化与内存控制策略。通过实际游戏案例,帮助开发者构建高性能、结构清晰的游戏系统,提高游戏开发效率与运行性能。

2026.04.13

137

20

c#是什么意思
c#是什么意思

C#是一种通用的面向对象编程语言,它在.NET框架上运行,可用于开发各种类型的应用程序。本专题为大家提供c#相关的各种文章、以及下载和课程。

2023.08.08

528

5

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
RabbitMQ 教程手册
RabbitMQ 教程手册

共0课时 | 0人学习

Linux man-pages 项目
Linux man-pages 项目

共0课时 | 0人学习

C# 教程
C# 教程

共94课时 | 20.2万人学习