
深入理解 CommandMyMeetings 模块化单体仓库中的命令模式与 CQRS 实战【免费下载链接】modular-monolith-with-dddFull Modular Monolith application with Domain-Driven Design approach.项目地址: https://gitcode.com/GitHub_Trending/mo/modular-monolith-with-dddCommand命令是 CQRS 架构中写半边的基本载体它表达系统用户的意图驱动领域对象改变状态并最终以领域事件的形式宣告结果。本文以 modular-monolith-with-ddd 仓库MyMeetings 模块化单体应用中的取消会议Cancel Meeting用例为骨架完整拆解命令的定义、两种形态、命名规范、处理链路与拒绝回滚机制并结合源码证明每一处结论。读完你将能在这套代码库中快速定位、读懂甚至仿写任意一条命令。一、什么是 Command定义与特征Command 术语条目给出的定义是A command is a request made to do something. A command represents the intention of a systems user regarding what the system will do to change its state.即命令是一次要做某事的请求它代表系统用户关于系统将如何改变自身状态的意图。它与查询Query相对查询只读不改命令只改必返回结果成功或失败。该定义还给出了命令的三条关键特征命令的结果只能是成功或失败结果以事件Event的形式呈现——也就是说命令本身不返回数据它要么产生一个或多个领域事件要么抛异常被拒绝参见 Event 术语条目成功时系统中必然发生了状态改变——如果什么都没变说明这条命令什么都没做属于异常情况命令命名应使用动词现在时或不定式 来自领域的名词组聚合或实体——例如CancelMeetingCommand、CreateMeetingCommand、BuySubscriptionCommand一眼即可看出意图与作用对象。这三条特征在本仓库的代码中都有严格体现下面逐条验证。二、命令的两种形态应用层对象与聚合上的方法原文档特别强调在 MyMeetings 中命令存在两种互补的形态作为应用层的对象即实现 Parameter Object参数对象模式的可序列化请求对象携带完成操作所需的全部输入参数作为领域对象上的方法在 DDD 中命令最终落在聚合Aggregate上的一个公开方法由该方法完成业务规则校验与状态变更参见 Aggregate 术语条目。以取消会议为例两种形态的代码分别位于应用层与领域层。应用层形态是CancelMeetingCommand源码与文档示例完全一致public class CancelMeetingCommand : CommandBase { public CancelMeetingCommand(Guid meetingId) { MeetingId meetingId; } public Guid MeetingId { get; } }领域层形态是Meeting聚合上的Cancel方法源码public void Cancel(MemberId cancelMemberId) { this.CheckRule(new MeetingCannotBeChangedAfterStartRule(_term)); if (!_isCanceled) { _isCanceled true; _cancelDate SystemClock.Now; _cancelMemberId cancelMemberId; this.AddDomainEvent(new MeetingCanceledDomainEvent(this.Id, _cancelMemberId, _cancelDate.Value)); } }注意命名规范Cancel是不定式动词 领域名词MeetingCancelMeetingCommand同样遵循动词 领域名词 Command的约定。整个仓库内CreateMeeting、JoinToGroup、BuySubscription等命令均沿用同一套命名便于在源码中按名称直接定位。三、命令处理链路从 Command 到 Handler 再到聚合CancelMeetingCommand本身只是一个参数对象真正驱动领域模型的是它的处理器Handler。文档给出了CancelMeetingCommandHandler该类的完整实现位于 CancelMeetingCommandHandler.csinternal class CancelMeetingCommandHandler : ICommandHandlerCancelMeetingCommand { private readonly IMeetingRepository _meetingRepository; private readonly IMemberContext _memberContext; internal CancelMeetingCommandHandler(IMeetingRepository meetingRepository, IMemberContext memberContext) { _meetingRepository meetingRepository; _memberContext memberContext; } public async Task Handle(CancelMeetingCommand request, CancellationToken cancellationToken) { var meeting await _meetingRepository.GetByIdAsync(new MeetingId(request.MeetingId)); meeting.Cancel(_memberContext.MemberId); } }这条链路揭示了命令处理的标准三步曲在 MyMeetings 的每个业务模块中反复出现解析输入把命令对象中的原始Guid包装为强类型 IDnew MeetingId(request.MeetingId)这是 DDD 中防止ID 混用的典型做法加载聚合通过仓储接口IMeetingRepository.GetByIdAsync把聚合从持久化中取出调用领域方法把命令语义交给聚合上的方法meeting.Cancel(...)由聚合自行完成规则校验与状态变更。注意 Handler 中并没有任何Save调用——持久化与事务由基础设施层的装饰器统一处理见下文第五节。同时可以观察到Handler 依赖的IMeetingRepository与IMemberContext均通过构造函数注入这类处理器的组装由各模块独立的 Autofac 容器完成模块间通过 ADR-0016 每模块独立 IoC 容器 实现解耦。四、命令对象的底层契约CommandBase 与 ICommand所有命令对象并非孤立存在它们都继承自模块内的CommandBase。以 Meetings 模块为例CommandBase.cs 的实现如下public abstract class CommandBase : ICommand { public Guid Id { get; } protected CommandBase() { Id Guid.NewGuid(); } protected CommandBase(Guid id) { Id id; } }这里有两个值得注意的细节每个命令实例自动获得全局唯一的Id该 ID 用于在日志、Outbox、内部命令调度等场景中追踪同一条命令的完整生命周期CommandBase同时提供无参与带参两种构造函数带参版本允许在重放、定时任务等场景中显式指定命令 ID例如 IRecurringCommand 相关实现。CommandBase继承自模块契约层定义的ICommandICommand.cspublic interface ICommandout TResult : IRequestTResult { Guid Id { get; } } public interface ICommand : IRequest { Guid Id { get; } }该接口直接继承 MediatR 的IRequest因此命令天然具备请求-响应语义ICommandHandlerCancelMeetingCommand实际就是 MediatR 的IRequestHandler。此外还有返回结果的泛型变体ICommandTResult用于需要命令处理结果如新实体 ID的场景——这正是 ADR-0008 允许命令处理返回结果 的落地体现。从项目层面看命令/查询分离是 MyMeetings 架构决策的基石ADR-0007 采用 CQRS 架构风格 明确要求每个模块的 Facade 方法只接受 Command 或 Query 对象因为命令作为普通对象可以方便地被序列化、保存与记录日志这也是 Outbox/内部命令机制能够工作的前提。五、命令的拒绝与回滚为什么失败不会留下脏状态原文档强调的核心要点是命令在状态改变之前可以被随时拒绝。这体现在两层防护上Handler 层如果meetingId无效GetByIdAsync找不到聚合仓储会抛出异常命令在此被拒绝领域层Cancel方法开头的this.CheckRule(new MeetingCannotBeChangedAfterStartRule(_term))会校验业务规则——如果会议已经开始就抛出BusinessRuleValidationException定义于 src/BuildingBlocks/Domain/BusinessRuleValidationException.cs命令同样被拒绝。拒绝之后的回滚机制来自基础设施层的统一处理Meetings 模块的命令在执行时会经过 UnitOfWorkCommandHandlerDecorator.cs 这类装饰器的包装src/Modules/Meetings/Infrastructure/Configuration/Processing目录下的 22 个处理类即为完整的命令处理管线。该装饰器在命令成功时才提交工作单元一旦命令或领域规则抛异常所有未提交的更改全部回滚数据库不产生任何部分状态——这正对应原文档中all uncommitted changes are rolled back (state does not change)的说明。需要强调的是Cancel方法还做了一个幂等保护if (!_isCanceled)保证重复调用不会重复改变状态、不会重复发布领域事件。这与命令特征第二条成功时状态必须改变否则等于什么都没做形成呼应——已取消的会议再次调用Cancel属于无效果调用方法直接跳过。六、命令的产物领域事件Domain Event命令成功的结果不是返回值而是领域事件。Cancel在状态变更后调用this.AddDomainEvent(new MeetingCanceledDomainEvent(this.Id, _cancelMemberId, _cancelDate.Value));MeetingCanceledDomainEvent源码携带三个事实被取消的会议 ID、执行取消的成员 ID、取消时间。它的构造函数public MeetingCanceledDomainEvent(MeetingId meetingId, MemberId cancelMemberId, DateTime cancelDate)领域事件会经由 DomainEventsDispatcher 在工作单元提交时被发布进而触发模块内通知Notification或跨模块集成事件。这正是命令的结果是事件这一特征的具体实现命令驱动状态改变改变被固化为事件记录事件再驱动下游反应如发送邮件、更新其他模块的数据。感兴趣可进一步阅读 Domain-Event 术语条目 与 Event 术语条目。七、实战小结如何在 MyMeetings 中读懂或新增一条命令把以上内容浓缩成可操作的清单无论阅读还是仿写命令都可参照命名动词现在时/不定式 领域名词 Command后缀如CancelMeetingCommand载体继承模块的CommandBase在构造函数中接收并固化所有输入参数属性只读处理实现ICommandHandlerTCommand注入所需仓储与上下文加载聚合后调用其领域方法不写任何业务逻辑到 Handler规则把校验放进聚合方法开头的CheckRule中失败即抛异常由管线回滚结果通过AddDomainEvent发布领域事件而不是返回成功提示定位入口模块的 Facade如 IMeetingsModule接收命令对象通过 MediatR 分发到对应 Handler。以 CancelMeeting 目录 为例它只包含CancelMeetingCommand.cs与CancelMeetingCommandHandler.cs两个文件是理解命令 参数对象 处理器最小闭环的最佳起点而Meeting聚合上的Cancel方法src/Modules/Meetings/Domain/Meetings/Meeting.cs#L278-L290则是领域模型承载命令语义的标准示范。整套设计贯穿于 Administration、Meetings、Payments、Registrations、UserAccess 五个业务模块构成了 MyMeetings 写操作的一致骨架。【免费下载链接】modular-monolith-with-dddFull Modular Monolith application with Domain-Driven Design approach.项目地址: https://gitcode.com/GitHub_Trending/mo/modular-monolith-with-ddd创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考