DDD单聚合根多持久化数据源实现问询——基于.NET微服务指南
Great question—this is a super common scenario when scaling DDD systems while sticking to core principles like Persistence Ignorance (PI) and Infrastructure Ignorance (II), especially when aligning with the patterns laid out in * .NET微服务:容器化.NET应用架构指南* (eShopOnContainers) on page 213. Let's break this down step by step, with code examples that fit the eShopOnContainers style.
首先,对齐核心原则与eShopOnContainers规范
First, let's lock in the ground rules we need to follow:
eShopOnContainers聚合根CUD规范(第213页):聚合根是业务修改的唯一入口,所有创建(Create)、更新(Update)、删除(Delete)操作必须由聚合根自身封装实现,仓储仅负责将聚合根的状态持久化到数据源,不包含任何业务逻辑。
Plus, we need to strictly adhere to:
- Persistence Ignorance (PI):The domain layer has zero knowledge of how data is stored (no references to EF Core, MongoDB drivers, etc.)
- Infrastructure Ignorance (II):The domain layer doesn't even know multiple data sources exist—all persistence details are hidden behind abstractions.
架构设计思路:分层隔离 + 抽象适配
The core idea is to keep the domain layer pure, let the infrastructure layer handle data source-specific logic, and use the application layer to coordinate everything:
- Domain Layer: Defines aggregates, business rules, and generic repository interfaces (no data source specifics)
- Infrastructure Layer: Implements repository interfaces with adapters for each data source, or a composite repository that wraps multiple adapters
- Application Layer: Calls domain logic and repository interfaces, with zero awareness of underlying data sources
具体落地步骤(eShopOnContainers风格)
Let's use an Order aggregate as an example: suppose core order data (header, status, total) lives in a relational database, while non-core data (operation logs, attachment metadata) lives in a document database.
1. Domain Layer: Pure Aggregate + Abstract Repository
Keep the domain layer completely free of persistence concerns:
// 领域层:Order聚合根(无任何持久化依赖) public class Order : AggregateRoot { public Guid OrderId { get; private set; } public OrderStatus Status { get; private set; } public decimal TotalAmount { get; private set; } // 非核心日志数据:领域层只关心业务逻辑,不关心存储位置 private readonly List<OrderOperationLog> _operationLogs = new(); // 业务方法:封装CUD逻辑(严格遵循eShopOnContainers规范) public void MarkAsShipped() { if (Status != OrderStatus.Paid) throw new InvalidOperationException("Only paid orders can be marked as shipped."); Status = OrderStatus.Shipped; _operationLogs.Add(new OrderOperationLog(OrderId, DateTime.UtcNow, "Order marked as shipped")); AddDomainEvent(new OrderShippedEvent(OrderId)); // 用于最终一致性的领域事件 } // 暴露只读日志给仓储(防止外部修改聚合根状态) public IReadOnlyCollection<OrderOperationLog> OperationLogs => _operationLogs.AsReadOnly(); // 内部方法:重建聚合根状态(从多数据源加载后组装) internal void ReconstructLog(OrderOperationLog log) => _operationLogs.Add(log); } // 领域层:抽象仓储接口(只定义业务契约,不涉及数据源) public interface IOrderRepository : IRepository<Order> { Task<Order> GetByIdAsync(Guid orderId); Task AddAsync(Order order); Task UpdateAsync(Order order); }
2. Infrastructure Layer: Multi-DataSource Repository Adapters
Create a composite repository that wraps data-source-specific implementations, exposing the single IOrderRepository interface to the rest of the system:
// 基础设施层:SQL数据源适配(处理核心订单数据) public class SqlOrderCoreRepository : IOrderCoreRepository { private readonly AppDbContext _dbContext; public SqlOrderCoreRepository(AppDbContext dbContext) => _dbContext = dbContext; public async Task<Order> GetCoreDataByIdAsync(Guid orderId) => await _dbContext.Orders.FindAsync(orderId); public async Task AddCoreDataAsync(Order order) { await _dbContext.Orders.AddAsync(order); await _dbContext.SaveChangesAsync(); } public async Task UpdateCoreDataAsync(Order order) { _dbContext.Orders.Update(order); await _dbContext.SaveChangesAsync(); } } // 基础设施层:MongoDB数据源适配(处理订单日志) public class MongoOrderLogRepository : IOrderLogRepository { private readonly IMongoCollection<OrderOperationLog> _logCollection; public MongoOrderLogRepository(IMongoDatabase database) => _logCollection = database.GetCollection<OrderOperationLog>("OrderLogs"); public async Task AddLogsAsync(IEnumerable<OrderOperationLog> logs) => await _logCollection.InsertManyAsync(logs); public async Task<List<OrderOperationLog>> GetLogsByOrderIdAsync(Guid orderId) => await _logCollection.Find(log => log.OrderId == orderId).ToListAsync(); } // 基础设施层:组合仓储(对外统一实现IOrderRepository) public class CompositeOrderRepository : IOrderRepository { private readonly IOrderCoreRepository _sqlRepo; private readonly IOrderLogRepository _mongoRepo; public CompositeOrderRepository(IOrderCoreRepository sqlRepo, IOrderLogRepository mongoRepo) { _sqlRepo = sqlRepo; _mongoRepo = mongoRepo; } public async Task<Order> GetByIdAsync(Guid orderId) { // 从SQL加载核心数据,从Mongo加载日志,组装完整聚合根 var order = await _sqlRepo.GetCoreDataByIdAsync(orderId); if (order == null) return null; var logs = await _mongoRepo.GetLogsByOrderIdAsync(orderId); logs.ForEach(order.ReconstructLog); return order; } public async Task AddAsync(Order order) { // 先保存核心数据,再保存日志(最终一致性) await _sqlRepo.AddCoreDataAsync(order); await _mongoRepo.AddLogsAsync(order.OperationLogs); } public async Task UpdateAsync(Order order) { // 更新核心数据,同步新增的日志(避免重复插入) await _sqlRepo.UpdateCoreDataAsync(order); var newLogs = order.OperationLogs.Where(log => !log.IsPersisted).ToList(); await _mongoRepo.AddLogsAsync(newLogs); newLogs.ForEach(log => log.MarkAsPersisted()); } }
3. Handling Multi-DataSource Consistency
Since local transactions don't span multiple data sources, we use event-driven eventual consistency—a pattern heavily used in eShopOnContainers:
- The aggregate publishes domain events when state changes
- Infrastructure event handlers sync data to other data sources
- Add retry/dead-letter queues for failure recovery
Example event handler for syncing logs to MongoDB:
public class OrderShippedEventHandler : INotificationHandler<OrderShippedEvent> { private readonly IOrderLogRepository _mongoRepo; public OrderShippedEventHandler(IOrderLogRepository mongoRepo) => _mongoRepo = mongoRepo; public async Task Handle(OrderShippedEvent notification, CancellationToken cancellationToken) { var log = new OrderOperationLog(notification.OrderId, DateTime.UtcNow, "Order marked as shipped"); await _mongoRepo.AddLogsAsync(new[] { log }); } }
4. Dependency Injection & Application Layer Usage
Register the repository implementations in your DI container (Program.cs/Startup.cs):
builder.Services.AddScoped<IOrderCoreRepository, SqlOrderCoreRepository>(); builder.Services.AddScoped<IOrderLogRepository, MongoOrderLogRepository>(); builder.Services.AddScoped<IOrderRepository, CompositeOrderRepository>();
The application layer calls the repository without any knowledge of multiple data sources:
public class ShipOrderCommandHandler : IRequestHandler<ShipOrderCommand, bool> { private readonly IOrderRepository _orderRepo; public ShipOrderCommandHandler(IOrderRepository orderRepo) => _orderRepo = orderRepo; public async Task<bool> Handle(ShipOrderCommand request, CancellationToken cancellationToken) { var order = await _orderRepo.GetByIdAsync(request.OrderId); if (order == null) return false; order.MarkAsShipped(); await _orderRepo.UpdateAsync(order); return true; } }
Key Best Practices
- Enforce PI/II: The domain layer must never reference infrastructure libraries. All dependencies are injected via interfaces.
- Keep Aggregates Whole: Even if data is split across sources, the in-memory aggregate must be complete to enforce business rules.
- Avoid Distributed Transactions: Use eventual consistency instead—this is the recommended approach in eShopOnContainers for microservices.
- Repository Responsibility: The composite repository only coordinates persistence; all business logic lives in the aggregate root.
内容的提问来源于stack exchange,提问作者Coffe Cold

