.NET模块化单体应用:模块间集成事件传递对象的问题
解决方案
针对你在模块化单体DDD架构中遇到的跨模块事件通信问题,这里提供几个符合DDD原则的可行方案:
方案一:事件携带订单创建所需的最小化数据
不要在事件中传递整个Basket领域对象,而是提取创建订单必须的原始类型或值对象(这些类型可定义在Shared模块,或直接用基础类型)构建事件。既避免模块间的领域对象依赖,也无需额外DTO层。
修改事件定义:
public record BasketCheckedOutEvent( Guid CustomerId, Guid BasketId, List<OrderItemData> Items, // 自定义值对象,仅包含订单所需的商品信息 decimal TotalAmount, string CouponCode, int ShippingMethod, int PaymentMethod) : IEvent; // 订单项数据值对象(可放在Shared模块或事件所在模块) public record OrderItemData(Guid ProductId, int Quantity, decimal UnitPrice);
在CheckoutBasketCommand的Handle方法中提取数据构建事件:
public async Task<Unit> Handle(CheckoutBasketCommand command, CancellationToken cancellationToken) { var customerId = _currentUserService.UserId; var basket = await _basketRepository.GetBasketByCustomerId(customerId) ?? throw new NotFoundException("Basket not found."); await _unitOfWork.CommitAndDispatchDomainEventsAsync(basket); // 从Basket对象中提取订单所需的最小数据 var orderItems = basket.Items.Select(item => new OrderItemData( item.ProductId, item.Quantity, item.UnitPrice)).ToList(); await _eventDispatcher.PublishAsync(new BasketCheckedOutEvent( customerId, basket.Id, orderItems, basket.TotalAmount, command.CouponCode, command.ShippingMethod, command.PaymentMethod)); return Unit.Value; }
这个方案的核心是事件仅传递消费方(Order模块)需要的最少数据,完全隔离模块间的领域对象依赖,是DDD跨模块事件通信的首选方式。
方案二:Basket模块对外暴露只读查询服务
如果不想在事件中传递大量数据,可只传递BasketId,同时在Basket模块提供对外公开的只读查询服务,Order模块通过该服务获取创建订单所需的数据。
- 在Basket模块定义查询服务接口与实现:
// 接口可放在Shared模块,实现留在Basket模块内部 public interface IBasketQueryService { Task<BasketOrderData> GetBasketOrderDataAsync(Guid basketId, CancellationToken cancellationToken); } // 仅包含订单所需字段的查询模型 public record BasketOrderData( Guid CustomerId, List<OrderItemData> Items, decimal TotalAmount); public class BasketQueryService : IBasketQueryService { private readonly IBasketRepository _basketRepository; public BasketQueryService(IBasketRepository basketRepository) { _basketRepository = basketRepository; } public async Task<BasketOrderData> GetBasketOrderDataAsync(Guid basketId, CancellationToken cancellationToken) { var basket = await _basketRepository.GetByIdAsync(basketId, cancellationToken) ?? throw new NotFoundException("Basket not found."); return new BasketOrderData( basket.CustomerId, basket.Items.Select(item => new OrderItemData(item.ProductId, item.Quantity, item.UnitPrice)).ToList(), basket.TotalAmount); } }
- Order模块监听事件时,注入查询服务获取数据:
public class BasketCheckedOutEventHandler : IEventHandler<BasketCheckedOutEvent> { private readonly IOrderRepository _orderRepository; private readonly IBasketQueryService _basketQueryService; public BasketCheckedOutEventHandler(IOrderRepository orderRepository, IBasketQueryService basketQueryService) { _orderRepository = orderRepository; _basketQueryService = basketQueryService; } public async Task Handle(BasketCheckedOutEvent @event, CancellationToken cancellationToken) { var basketData = await _basketQueryService.GetBasketOrderDataAsync(@event.BasketId, cancellationToken); // 使用获取到的basketData创建订单 var order = Order.Create( basketData.CustomerId, basketData.Items, basketData.TotalAmount, @event.CouponCode, @event.ShippingMethod, @event.PaymentMethod); await _orderRepository.AddAsync(order, cancellationToken); await _unitOfWork.CommitAsync(cancellationToken); } }
此方案既保持了Basket模块的封装性(仓储仅内部访问),又让Order模块能合法获取所需数据,适合数据量较大且不想在事件中传递过多数据的场景。
方案三:基于事件投影的共享只读存储
在Basket模块处理领域事件时,提前将订单需要的数据投影到共享的只读存储(如单独数据库表、视图或缓存),Order模块监听BasketCheckedOutEvent后,直接从该存储读取数据,无需依赖Basket模块的任何服务或仓储。
例如,在Basket模块添加投影处理器:
public class BasketToOrderProjectionHandler : IEventHandler<BasketCheckedOutEvent> { private readonly ISharedReadOnlyRepository _sharedReadOnlyRepository; public BasketToOrderProjectionHandler(ISharedReadOnlyRepository sharedReadOnlyRepository) { _sharedReadOnlyRepository = sharedReadOnlyRepository; } public async Task Handle(BasketCheckedOutEvent @event, CancellationToken cancellationToken) { var basketProjection = new BasketOrderProjection( @event.BasketId, @event.CustomerId, @event.Items, @event.TotalAmount); await _sharedReadOnlyRepository.AddOrUpdateBasketProjectionAsync(basketProjection, cancellationToken); } }
Order模块的事件处理器直接从共享存储读取投影数据:
public class BasketCheckedOutEventHandler : IEventHandler<BasketCheckedOutEvent> { private readonly IOrderRepository _orderRepository; private readonly ISharedReadOnlyRepository _sharedReadOnlyRepository; public BasketCheckedOutEventHandler(IOrderRepository orderRepository, ISharedReadOnlyRepository sharedReadOnlyRepository) { _orderRepository = orderRepository; _sharedReadOnlyRepository = sharedReadOnlyRepository; } public async Task Handle(BasketCheckedOutEvent @event, CancellationToken cancellationToken) { var basketProjection = await _sharedReadOnlyRepository.GetBasketProjectionAsync(@event.BasketId, cancellationToken); // 创建订单逻辑 var order = Order.Create( basketProjection.CustomerId, basketProjection.Items, basketProjection.TotalAmount, @event.CouponCode, @event.ShippingMethod, @event.PaymentMethod); await _orderRepository.AddAsync(order, cancellationToken); await _unitOfWork.CommitAsync(cancellationToken); } }
这个方案适合高并发场景,能降低Basket模块的查询压力,但需要维护额外的投影存储,增加了系统复杂度。
内容的提问来源于stack exchange,提问作者Mateusz Babski
相关产品推荐
相关产品推荐

