MediatR开发:控制器直接传Command/Query还是用ViewModel更优?
MediatR 控制器入参选择:直接传Command/Query还是用ViewModel映射?
在C# + MediatR的中介者模式实现中,常见的两种控制器写法如下:
方式一:直接将Command作为操作入参
[HttpPost] public async Task<IActionResult> Login(LoginUserCommand loginUserCommand) { var User = await _meditr.Send(loginUserCommand); return View(); }
方式二:使用ViewModel作为入参后映射
[HttpPost] public async Task<IActionResult> Login(LoginViewModel loginViewModel) { var authenticateUserCommand = _mapper.Map<LoginUserCommand>(loginViewModel); var User = await _meditr.Send(authenticateUserCommand); return View(); }
核心结论:生产级复杂项目优先选择ViewModel作为入参再映射为Command/Query的方案,小体量简单项目可以直接用Command/Query当入参提效,核心判断标准是「职责是否解耦、是否需要应对后续变更」。
为什么不推荐默认直接把Command/Query作为控制器入参
Command/Query的定位是应用层的业务用例约定,它的设计应该完全服务于业务场景本身,而不是适配HTTP接口的参数绑定规则。直接把它当控制器入参会带来几个明显的问题:
- 职责污染:你会不可避免地在Command/Query类上加一堆仅用于接口层的逻辑,比如模型绑定特性、入参格式校验规则(比如手机号正则、密码长度校验)、甚至为了适配前端传参格式修改Command的字段定义,最后业务约定和接口约定混在一起,越改越乱。举个实际场景:登录Command需要的参数是
账号、加密后的密码、登录IP,但前端只会传明文密码,IP需要从HttpContext里取,如果直接用Command当入参,你要么给Command加个明文密码字段破坏它的业务定义,要么写自定义模型绑定器绕一大圈取IP,完全没必要。 - 耦合度过高:如果后续有其他入口调用同一个Command(比如后台管理端的代登录功能、消息队列触发的登录逻辑、RPC接口调用),这些非HTTP场景根本不需要适配前端传参的字段格式,一旦你因为前端需求改了Command的字段,所有调用方都要跟着改,牵一发动全身。
- 校验逻辑混乱:接口层的入参校验(比如参数是否为空、格式是否合法)和业务层的校验(比如账号是否存在、密码是否匹配)本来就是两层逻辑,全堆在Command上后期根本没法维护。
为什么推荐ViewModel+映射的方案
ViewModel的定位是接口层的请求约定,专门用来对接HTTP请求的入参,和业务层完全隔离,刚好解决上面的问题:
- 边界清晰:ViewModel只管接收前端传的参数、做接口层的参数校验,Command/Query只管描述业务用例需要什么数据,两者互不干扰。比如前端改了传参字段名,你只需要改ViewModel和对应的映射逻辑,Command完全不用动,不会影响其他业务调用方。
- 适配灵活:像从HttpContext里取登录IP、取当前用户ID这种不属于前端传参的内容,你完全可以在映射逻辑里统一处理,不用侵入业务约定。
- 测试方便:测试应用层逻辑的时候直接构造Command就行,不用考虑HTTP参数绑定的各种规则;测试接口的时候单独构造ViewModel测参数校验就行,分层清晰。
什么时候可以直接用Command/Query当入参
不是所有项目都要硬套分层,如果你做的是内部小工具、简单CRUD项目,满足下面几个条件,直接用Command当入参完全没问题,能省掉写ViewModel和映射的重复代码:
- 项目没有复杂业务逻辑,所有接口都是简单的增删改查
- 前端传参和Command需要的字段100%对齐,没有额外的上下文参数需要从HttpContext获取
- 后续基本不会有非HTTP入口调用这些Command/Query
- 项目规模小,参与开发的人少,不会出现频繁改字段的情况
最后提个实践细节:映射不用非得用AutoMapper这类工具,字段少的话手动new Command()赋值反而更直观,不会出现隐式映射的坑,不要为了凑架构加没必要的复杂度。
内容的提问来源于stack exchange,提问作者Muhammad Ali
相关产品推荐
相关产品推荐

