基于.NET Entity Framework的DDD电商产品更新实现困惑
解决方案:DDD 产品模块与仓储模式的正确交互方式
核心原则先明确
- 领域对象绝对不能依赖仓储:领域层只负责封装业务规则与状态变更,仓储属于基础设施层,是持久化细节,两者边界必须清晰。
- 聚合根是一致性边界:Product 作为聚合根,所有涉及 Variants、Options、Instances 的变更必须通过 Product 的公共方法发起,确保内部状态始终符合业务规则。
具体实现步骤
1. 领域层:封装私有集合的变更逻辑
让 Product 和 Variant 仅通过公共方法暴露操作入口,内部处理私有集合的变更与业务规则(比如修改 Option 价格后自动重新生成 Instance)。
// 聚合根:Product public class Product : AggregateRoot { private readonly List<Variant> _variants = new(); private readonly List<Instance> _instances = new(); // 公共方法:修改Option价格 public void UpdateOptionPrice(Guid variantId, Guid optionId, decimal newPrice) { var variant = _variants.First(v => v.Id == variantId); variant.UpdateOptionPrice(optionId, newPrice); // 触发Instance重新生成(业务规则内聚) RegenerateInstances(); } // 公共方法:添加Variant public void AddVariant(Variant newVariant) { // 业务校验:禁止重复特征组 if (_variants.Any(v => v.Name.Equals(newVariant.Name, StringComparison.OrdinalIgnoreCase))) throw new DomainException("相同名称的特征组已存在"); _variants.Add(newVariant); RegenerateInstances(); } // 私有方法:生成/更新所有Instance private void RegenerateInstances() { _instances.Clear(); // 实现组合所有Variant的Option、计算最终价格、生成SKU的逻辑 // 完全由领域层控制业务规则,无需依赖外部组件 var optionCombinations = GetAllOptionCombinations(); foreach (var combo in optionCombinations) { var totalPrice = BasePrice + combo.Sum(o => o.PriceAdjustment); var sku = GenerateSku(combo); _instances.Add(new Instance(Guid.NewGuid(), sku, totalPrice, combo)); } } // 辅助方法:生成所有Option组合(示例逻辑) private IEnumerable<IEnumerable<Option>> GetAllOptionCombinations() { // 实现笛卡尔积组合逻辑 return _variants.Select(v => v.Options).Aggregate( new List<IEnumerable<Option>> { Enumerable.Empty<Option>() }, (acc, options) => acc.SelectMany(a => options.Select(o => a.Append(o))).ToList() ); } } // 实体:Variant public class Variant : Entity { private readonly List<Option> _options = new(); public void UpdateOptionPrice(Guid optionId, decimal newPrice) { var option = _options.First(o => o.Id == optionId); // 业务校验:价格不能为负 if (newPrice < 0) throw new DomainException("价格不能为负数"); option.SetPriceAdjustment(newPrice); } }
2. 基础设施层:仓储仅处理聚合根的持久化
仓储只负责加载、保存整个 Product 聚合根,无需关心内部子实体的变更——EF 的变更追踪会自动处理所有关联实体的更新。
public class ProductRepository : IProductRepository { private readonly AppDbContext _dbContext; public ProductRepository(AppDbContext dbContext) { _dbContext = dbContext; } public async Task<Product> GetByIdAsync(Guid productId) { return await _dbContext.Products .Include(p => p.Variants) .ThenInclude(v => v.Options) .Include(p => p.Instances) .FirstOrDefaultAsync(p => p.Id == productId); } public async Task SaveAsync(Product product) { if (_dbContext.Entry(product).State == EntityState.Detached) _dbContext.Products.Attach(product); await _dbContext.SaveChangesAsync(); } // 暴露事务入口,由应用层控制事务范围 public async Task<IDbContextTransaction> BeginTransactionAsync() { return await _dbContext.Database.BeginTransactionAsync(); } }
3. 应用层:协调流程与事务
应用层负责接收前端请求、转换为领域方法参数、调用仓储获取聚合根、执行领域操作,最后统一提交事务——确保所有变更(比如修改 Option 价格 + 重新生成 Instance)在同一个事务内完成。
public class ProductApplicationService { private readonly IProductRepository _productRepository; public ProductApplicationService(IProductRepository productRepository) { _productRepository = productRepository; } public async Task UpdateOptionPrice(Guid productId, Guid variantId, Guid optionId, decimal newPrice) { using var transaction = await _productRepository.BeginTransactionAsync(); try { var product = await _productRepository.GetByIdAsync(productId); if (product == null) throw new NotFoundException("产品不存在"); // 通过聚合根的公共方法触发变更 product.UpdateOptionPrice(variantId, optionId, newPrice); // 仅需保存聚合根,EF自动处理所有子实体的变更 await _productRepository.SaveAsync(product); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; } } }
关键问题解答
- 能不能让领域对象访问仓储?:绝对不行。这会打破领域层与基础设施层的边界,导致领域逻辑依赖持久化细节,失去DDD的封装性。
- 要不要跳过领域类直接操作仓储?:绝对不要。直接修改数据库会绕过领域层的业务规则(比如价格校验、Instance自动生成),导致数据不一致。所有变更必须通过聚合根的公共方法发起。
- 多表更新的事务怎么处理?:由应用层在业务流程的最外层开启事务,包裹“获取聚合-执行领域操作-保存聚合”的全流程,确保所有变更要么全部成功,要么全部回滚。
内容的提问来源于stack exchange,提问作者Codrin pp
相关产品推荐
相关产品推荐

