You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DDD单聚合根多持久化数据源实现问询——基于.NET微服务指南

单个聚合根对接多持久化数据源的DDD落地方案(结合eShopOnContainers规范)

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 07:16:55