实战解析:以 Modular Monolith with DDD 的 CancelMeeting 命令为例)
依赖注入Dependency Injection实战解析以 Modular Monolith with DDD 的 CancelMeeting 命令为例【免费下载链接】modular-monolith-with-dddFull Modular Monolith application with Domain-Driven Design approach.项目地址: https://gitcode.com/GitHub_Trending/mo/modular-monolith-with-ddd依赖注入Dependency InjectionDI是构建解耦、可测试的面向对象系统的核心技巧也是本项目 Modular Monolith模块化单体架构中每个业务模块得以自治运转的基石。本文以CancelMeetingCommandHandler这一真实命令处理器为主线完整讲解 DI 的定义、构造器注入的写法、依赖接口的真实定义并结合本仓库的 Autofac 容器配置每模块一个 IoC 容器、Composition Root、装饰器模式剖析依赖从注册到解析的完整链路。读完本文你将掌握 DI 在 CQRS DDD 模块化架构中的标准落地姿势并能直接在本仓库源码中一一对应验证。术语定义什么是依赖注入Dependency Injection is a technique in which an object receives other objects that it depends on. These other objects are called dependencies.依赖注入是一种让对象接收其所需的其他对象的技术这些被接收的对象称为依赖。这句话定义了 DI 的三大参与角色客户对象Client需要依赖才能完成工作的对象如命令处理器依赖Dependency客户对象调用其方法的其他对象通常以接口形式出现注入器Injector负责创建依赖实例并将其注入给客户对象的机制在本仓库中即每个模块内部的 Autofac 容器。关键点在于客户对象不负责创建依赖也不负责查找依赖它只声明我需要什么由容器在运行时按注册规则把正确的实现交给它。这与控制反转Inversion of Control理念一脉相承——对象不再控制自己依赖的生命周期与装配方式控制权被反转给了容器相关术语可对照术语表中的 Dependency Injection、Composition Root 与 Decorator Pattern 词条。一个最小示例CancelMeetingCommandHandler 的构造器注入本仓库 术语表 - Dependency Injection 词条 用一个极其典型的案例演示了 DI取消会议命令的处理。该示例与仓库真实源码一致位于 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); } }该处理器要完成取消会议这一职责需要两个协作者依赖依赖作用使用位置IMeetingRepository会议仓储按 Id 加载会议聚合加载Meeting聚合根IMemberContext当前登录成员上下文提供MemberId记录由谁取消可以看到CancelMeetingCommandHandler自身没有new出任何实现类而是通过构造函数把两个接口收进来存入readonly字段——这就是文档所强调的Constructor Injection构造器注入。这样的写法带来三个直接收益依赖一目了然只要看构造器签名就知道该处理器依赖什么符合单一职责与显式依赖原则易于替换接口背后可以是真实仓储、也可以是测试替身Mock/Stub不可变约束依赖一旦注入即readonly杜绝运行期被悄悄替换。深入源码两个依赖接口的真实定义为了理解注入进来的到底是什么我们看一下这两个接口在 Domain 层中的真实定义。会议仓储接口IMeetingRepository.cspublic interface IMeetingRepository { Task AddAsync(Meeting meeting); TaskMeeting GetByIdAsync(MeetingId id); }成员上下文接口IMemberContext.cspublic interface IMemberContext { MemberId MemberId { get; } }处理器调用meeting.Cancel(_memberContext.MemberId)后真正的领域行为发生在 Meeting.cs 中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)); } }值得注意IMeetingRepository返回的是富领域模型聚合根Meeting继承Entity, IAggregateRoot业务规则如MeetingCannotBeChangedAfterStartRule在领域层内部通过CheckRule强制执行处理器本身不掺杂任何业务规则判断。这正是 DI 与 DDD 分层架构配合的典范应用层处理器只做编排依赖接口注入领域服务与基础设施抽象。注入过程谁把依赖交给 HandlerComposition Root 与每模块一个 IoC 容器构造函数只是声明需要依赖真正把实现注入进来的是容器装配代码。本仓库对此有一个明确的架构决策记录在 ADR 0016Create an IoC Container per module 中。该 ADR 对比了两种方案方案优点缺点1. 整个应用共用一个 IoC 容器位于宿主项目标准做法、依赖集中在一处配置宿主与所有项目/库强耦合、模块自治性下降2. 每个模块一个 IoC 容器模块自治、松散耦合、宿主只依赖应用服务层存在少量重复代码、非标准做法决策结果采用方案 2——每个模块一个 IoC 容器。ADR 明确写道模块自治与松散耦合比少量重复代码更重要。由此带来的后果是需要为每个模块创建并维护独立的容器实现方式虽非标准但足够简单可以独立为某个模块增加依赖而不影响其他模块。这一决策在源码中清晰可见。以 Meetings 模块为例容器装配集中在 MeetingsStartup.cs 中——它就是该模块的Composition Root整个模块依赖图的唯一组装点private static void ConfigureCompositionRoot( string connectionString, IExecutionContextAccessor executionContextAccessor, ILogger logger, EmailsConfiguration emailsConfiguration, IEventsBus eventsBus) { var containerBuilder new ContainerBuilder(); containerBuilder.RegisterModule(new LoggingModule(logger.ForContext(Module, Meetings))); var loggerFactory new SerilogLoggerFactory(logger); containerBuilder.RegisterModule(new DataAccessModule(connectionString, loggerFactory)); containerBuilder.RegisterModule(new ProcessingModule()); containerBuilder.RegisterModule(new EventsBusModule(eventsBus)); containerBuilder.RegisterModule(new MediatorModule()); containerBuilder.RegisterModule(new AuthenticationModule()); // ... domain notifications map 装配 ... containerBuilder.RegisterModule(new OutboxModule(domainNotificationsMap)); containerBuilder.RegisterModule(new EmailModule(emailsConfiguration)); containerBuilder.RegisterModule(new QuartzModule()); containerBuilder.RegisterInstance(executionContextAccessor); _container containerBuilder.Build(); MeetingsCompositionRoot.SetContainer(_container); }而宿主API在 Startup.cs 的InitializeModules中为每个模块分别调用各自的XxxStartup.Initialize(...)Meetings、Administration、UserAccess、Payments、Registrations各模块容器互不干涉且都从同一个MeetingsConnectionString读取配置、共享ExecutionContextAccessor与EmailsConfiguration等宿主级基础设施。Autofac 模块化注册依赖如何被解析容器通过一组Autofac.Module完成细粒度注册这正是依赖在何处被声明的答案。以与本例直接相关的三个模块为例MediatorModuleHandler 的自动注册MediatorModule.cs 将应用层装配中的 Handler 类型按约定批量注册builder.RegisterAssemblyTypes(Assemblies.Application, ThisAssembly) .AsClosedTypesOf(mediatorOpenType) .AsImplementedInterfaces() .FindConstructorsWith(new AllConstructorFinder());其中mediatorOpenTypes覆盖了ICommandHandler、ICommandHandler,、IRequestHandler,、INotificationHandler、IValidator等一系列开放泛型。CancelMeetingCommandHandler正是因为实现了ICommandHandlerCancelMeetingCommand才被容器自动发现并注册——你不需要为每个 Handler 手写一行注册代码。DataAccessModule仓储的按约定注册DataAccessModule.cs 以类名以 Repository 结尾为约定自动注册全部仓储实现builder.RegisterAssemblyTypes(infrastructureAssembly) .Where(type type.Name.EndsWith(Repository)) .AsImplementedInterfaces() .InstancePerLifetimeScope() .FindConstructorsWith(new AllConstructorFinder());同时它还注册了SqlConnectionFactoryISqlConnectionFactory与MeetingsContextEF Core DbContext。这样当容器解析IMeetingRepository时会实例化对应的 EF 仓储实现而该实现自身的依赖如MeetingsContext、ISqlConnectionFactory又由容器递归注入——整个对象图由容器构建。ProcessingModule装饰器与横切关注点ProcessingModule.cs 展示了 DI 与装饰器模式的经典配合——通过RegisterGenericDecorator为所有命令处理器叠加横切行为builder.RegisterGenericDecorator( typeof(UnitOfWorkCommandHandlerDecorator), typeof(ICommandHandler)); builder.RegisterGenericDecorator( typeof(ValidationCommandHandlerDecorator), typeof(ICommandHandler)); builder.RegisterGenericDecorator( typeof(LoggingCommandHandlerDecorator), typeof(IRequestHandler)); builder.RegisterGenericDecorator( typeof(DomainEventsDispatcherNotificationHandlerDecorator), typeof(INotificationHandler));这意味着取消会议命令在执行时实际调用链是层层包裹的装饰器日志 → 校验 → 工作单元事务 领域事件分发→ 真正的CancelMeetingCommandHandler。Handler 对这些横切逻辑一无所知依赖注入让业务核心与基础设施关注点彻底分离。命令执行链路从模块门面到 Handler 的依赖解析依赖最终在什么时机、以什么作用域被解析看 MeetingsModule.cs 与 CommandsExecutor.cspublic async TaskTResult ExecuteCommandAsyncTResult(ICommandTResult command) { return await CommandsExecutor.Execute(command); } public async Task ExecuteCommandAsync(ICommand command) { await CommandsExecutor.Execute(command); }internal static async Task Execute(ICommand command) { using (var scope MeetingsCompositionRoot.BeginLifetimeScope()) { var mediator scope.ResolveIMediator(); await mediator.Send(command); } }执行一次命令的完整链路是API 控制器调用模块门面IMeetingsModule.ExecuteCommandAsync门面注册见 MeetingsAutofacModule.csCommandsExecutor通过MeetingsCompositionRoot.BeginLifetimeScope()为每次调用开启一个独立的 Lifetime Scope在作用域内ResolveIMediator由 MediatR 定位到ICommandHandlerCancelMeetingCommand的实现容器在解析 Handler 构造参数时递归注入IMeetingRepository与IMemberContext的实现InstancePerLifetimeScope生命周期保证同一请求内共享同一 DbContext/UnitOfWork命令执行完毕using作用域释放本次请求的所有依赖一并销毁。MeetingsCompositionRootMeetingsCompositionRoot.cs持有静态容器引用仅对外暴露BeginLifetimeScope()把容器本身藏在了模块内部——这也是 Composition Root 模式的要点容器不泄漏到领域层与门面之外。DI 与测试可替换性是可测试性的前提依赖注入带来的最大工程红利之一是测试性。因为处理器只依赖接口测试中可以无缝替换实现领域单元测试直接构造聚合根触发行为见 src/Modules/Meetings/Tests/UnitTests/Meetings/如MeetingTests.cs、MeetingRolesTests.cs**集成测试与系统级测试SUT**通过替身替换宿主级依赖例如 ExecutionContextMock.cs 用 Mock 实现了IExecutionContextAccessor让测试在不依赖真实 HTTP 上下文的情况下驱动整条命令链路。从源码结构可以推断正是面向接口 构造器注入的架构约束才使得这些测试能够以如此低的成本替换依赖、隔离外部系统。小结DI 在本仓库中的最佳实践清单以 Dependency Injection 词条 为起点结合源码验证本仓库的 DI 实践可总结为五条可复用的准则只依赖接口不依赖实现Handler 声明IMeetingRepository、IMemberContext从不new具体类构造器注入为唯一注入方式依赖以readonly字段固化显式、不可变、易测试Composition Root 集中在模块 Startup依赖装配只在 MeetingsStartup.cs 一处完成领域层与门面层不感知容器每模块一个容器以 ADR 0016 为决策依据模块自治、松散耦合宿主只依赖各模块门面约定优于配置Handler 与 Repository 通过AsClosedTypesOf/ 名称约定批量注册新增一个命令处理器或仓储实现时零注册代码。依赖注入本身只是对象如何获得依赖的机制但当它与 DDD 分层、CQRS 命令管道、装饰器模式、模块化单体架构组合使用时就成了支撑整个系统可扩展性、可测试性与模块自治性的基础设施——这正是本仓库值得反复研读的价值所在。【免费下载链接】modular-monolith-with-dddFull Modular Monolith application with Domain-Driven Design approach.项目地址: https://gitcode.com/GitHub_Trending/mo/modular-monolith-with-ddd创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考