游乐游手机版
首页/编程语言/文章详情

c#如何使用MediatR_c#MediatR的正确用法与注意事项

时间:2026-05-06 07:25
C 中MediatR的正确用法与核心注意事项 在C 项目里引入MediatR,本意是让代码更清晰、更解耦。但实际操作中,不少开发者第一步就卡住了——如果没正确注册IMediator,或者注册方式有偏差,那么后续所有的Send操作都会直接抛出InvalidOperationException,让你寸步

C#中MediatR的正确用法与核心注意事项

c#如何使用MediatR_c#MediatR的正确用法与注意事项

在C#项目里引入MediatR,本意是让代码更清晰、更解耦。但实际操作中,不少开发者第一步就卡住了——如果没正确注册IMediator,或者注册方式有偏差,那么后续所有的Send操作都会直接抛出InvalidOperationException,让你寸步难行。

Program.cs 里怎么注册才不报错

问题的根源往往在服务注册这一步。必须调用AddMediatR,但仅仅写builder.Services.AddMediatR()是远远不够的(尤其是在.NET 6+的默认参数下,很容易漏掉跨程序集的Handler)。一个常见的坑是只传入了当前入口程序集,而Handler实际却定义在另一个类库中。

  • 场景一:Handler和请求类都在主项目(比如一个Web API)。这时应该用AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly)),明确指定从当前程序集加载。
  • 场景二:Handler单独放在ApplicationHandlers类库中。这就必须显式传入那个类库的程序集,例如AddMediatR(typeof(CreateUserCommandHandler).Assembly)
  • 别忘了using MediatR;——少了这个命名空间,AddMediatR扩展方法压根不会出现在智能提示里。
  • 生命周期配置是关键。通常需要加上.AsScoped()(或者确保默认就是Scoped)。因为MediatR内部依赖IServiceProvider来解析Handler,而Handler往往需要注入像DbContext这类Scoped服务。生命周期不匹配,程序运行起来很容易“炸”。

IRequest 和 IRequest 怎么选

选错接口类型,轻则编译失败,出现Cannot convert lambda expression这类错误;重则运行时逻辑混乱,比如Send一个本该返回void的请求,却试图去接收返回值。

  • 纯触发动作,不关心结果:比如发送邮件、记录日志。直接用IRequest,对应的Handler签名是Task Handle(...)
  • 需要返回数据:比如创建实体后返回ID,或者查询后返回一个DTO。那就必须用IRequestIRequest这类泛型接口,Handler签名相应是Task Handle(...)
  • 有一个绝对要避免的写法:不要用void作为泛型参数IRequest是不合法的,编译直接通不过。
  • 另外,命令类(Command)里最好别塞[Required]这类数据注解属性来做验证。验证逻辑应该交给IRequestPreProcessor或者专门的验证库(如FluentValidation)来处理,这样职责更清晰。

Handler 里为什么不能混读写逻辑

想象一下,在一个GetUserQueryHandler里,你顺手调用了_context.Sa veChangesAsync()。代码可能能跑起来,但这实际上破坏了CQRS(命令查询职责分离)的隔离前提,会引发一系列连锁问题:缓存失效、事务锁表,甚至导致本该快速的读接口变得异常缓慢。

  • Query Handler 只负责读:应该使用IUserReadRepository这类只读仓储,或者至少用DbSet.AsNoTracking()来确保不跟踪变更。严格禁止任何形式的Sa veChangesAsync调用。
  • Command Handler 只负责改状态:它的职责是改变领域状态,通常不应该返回完整的Entity对象(这容易意外触发EF Core的延迟加载或序列化时的循环引用问题)。更合适的做法是返回ID、精简的DTO,或者直接返回Unit.Value
  • 从设计上就避免混淆:不要让一个Handler类同时实现IRequestHandlerIRequestHandler。类型系统虽然允许,但这在语义上已经违背了CQRS的核心思想。
  • 当然,读写操作可以共用同一个DbContext实例(只要Scoped生命周期配置合理)。但关键在于,读写逻辑必须在代码层面严格分层,不能仅仅依靠开发者的“自觉”来隔离。

领域事件该用 IPublisher 还是 IMediator

如果你用IMediator.Publish()来发布领域事件,很可能会遇到事务一致性问题。因为IMediator并不感知EF Core的Unit of Work(工作单元),事件可能在数据库事务提交之前就被发出去了,导致数据状态不一致。

  • 正确的选择是IPublisher:这是MediatR专门提供的接口。它与EF Core的事务拦截器可以协同工作,确保事件只在Sa veChangesAsync成功执行后才真正发布出去。
  • IPublisher本身也是Scoped生命周期,直接从依赖注入容器中获取即可,千万不要自己去new实例。
  • 注意事件类的定义:它必须继承INotification,而不是IRequest。对应的Handler则需要实现INotificationHandler
  • 还有一个实践细节:不建议在Command Handler里直接await _publisher.Publish(...)之后再return。因为事件发布的失败不应该阻塞主业务流程。更稳健的做法是采用fire-and-forget(触发后不管)模式,或者用后台队列来兜底。

最后,需要时刻牢记一点:CQRS的区分,靠的不是类名,而是Handler的实际行为。哪怕你给类起名叫GetUserQuery,只要它在里面调用了Sa veChanges,那它本质上就已经不是一个Query了。这才是理解MediatR和CQRS模式的关键所在。

来源:https://www.php.cn/faq/2315573.html
上一篇Flask中怎么处理复杂的动态表单提交_Python结合WTForms实现FieldList嵌套 下一篇如何解决 Ubuntu 系统中找不到 php-fpm.conf 的问题
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
列表遍历时动态判断阈值并返回相应文本
编程语言 · 2026-07-08

列表遍历时动态判断阈值并返回相应文本

遍历数值列表时,先筛选满足阈值的元素,再根据结果输出列表或友好提示。推荐使用列表推导式结合条件判断,注意边界用`>=`,空列表自动为假。也可用`any()`提前终止遍历,提升效率。此法简洁,避免显式循环,特别适合阈值筛选。

Maven项目中如何强制使用本地构建的依赖版本
编程语言 · 2026-07-08

Maven项目中如何强制使用本地构建的依赖版本

多模块开发中强制使用本地依赖的正确做法是使用-SNAPSHOT版本并配合`-U`参数强制更新,或重构为多模块项目统一管理生命周期。避免使用版本范围语法或手动复制JAR,确保构建行为可靠可重复。

Go语言net.Conn并发写安全与原子性保障解析
编程语言 · 2026-07-08

Go语言net.Conn并发写安全与原子性保障解析

Go标准库中net Conn支持并发方法调用,但Write()不保证原子性。多goroutine同时写入时,系统可能拆分数据包,导致内容交错,破坏消息边界。必须使用互斥锁或bufio Writer等显式同步机制确保写操作完整性,不可依赖系统调用本身的原子性。

Python在Windows系统中获取指定卷标U盘驱动器字母的方法
编程语言 · 2026-07-08

Python在Windows系统中获取指定卷标U盘驱动器字母的方法

使用Pythonwmi库通过Win32_Volume接口查询Windows系统中卷标为“TOSHIBA”的U盘盘符。需安装wmi和pywin32,注意大小写区分及多设备过滤。仅适用于Windows。

TP6.0消息已读功能基于Redis Bitmap未读计数方案
编程语言 · 2026-07-08

TP6.0消息已读功能基于Redis Bitmap未读计数方案

TP6 0中使用Redis位图实现消息已读标记,每条消息仅占一个比特,内存效率高。需将消息ID映射为连续偏移量,通过SETBIT和GETBIT操作。需提前维护用户ID到偏移量的映射表,注意键过期与驱动类型(phpredis与predis)问题。此方法内存极省,适合海量消息场景,且需确保偏移量唯一。