在Domain-Driven Design与CQRS项目中如何在领域层遍历并修改自引用聚合根分类树
问题描述
我在项目中采用Domain-Driven Design(DDD)与CQRS架构,使用自引用聚合根
Category来建模层级结构,Category的字段如下:Category - CategoryId $id - CategoryName $name - ?CategoryId $parentId - CategoryPosition $position现在需要实现分类树排序功能,API控制器会接收如下结构的树形数据:
[ { "id": "ffb03f1d-dade-4f45-80d5-6e28fea17e70", "children": [ { "id": "bdb82eb6-59ea-4645-ae70-d03b5e01c4b6", "children": [ { "id": "0ad9c916-eb64-4b0a-b2d0-6864b81b304f", "children": [] } ] }, { "id": "afa58caf-a0b4-4787-b784-ed3344af2b2b", "children": [ { "id": "6f334ab9-47b3-4f7b-8f87-f9c36db8afe5", "children": [] } ] } ] }, { "id": "7d3b6110-c282-42b3-b649-efc9394fa5f0", "children": [] } ]目前我在基础设施层通过递归遍历新树,使用
MoveCategory命令来修改每个分类的parentId和position。但每个命令只能修改单个Category,无法通过单一命令完成操作。同时我需要检查树深度不超过4层,这是一条领域规则。
我的问题是:如何在领域层中遍历该新树并进行修改操作?
解决方案
1. 先把树深度校验逻辑放到领域层
领域规则必须由领域层守护,不能放到基础设施层。你可以在Category所在的领域服务里实现递归校验的方法,确保传入的树形结构符合深度要求:
public class CategoryTreeDomainService { public void ValidateTreeDepth(List<CategoryTreeDto> treeNodes, int currentDepth = 1) { if (currentDepth > 4) { throw new DomainException("分类树深度不能超过4层"); } foreach (var node in treeNodes) { if (node.Children.Any()) { ValidateTreeDepth(node.Children, currentDepth + 1); } } } }
这里的CategoryTreeDto是你从API接收的树形数据映射到领域层的传输模型(不是聚合根,只是用来传递结构)。
2. 定义批量更新命令,统一处理整个树形结构
既然单个MoveCategory命令无法覆盖整个树的修改,建议你定义一个批量更新分类树的命令,比如UpdateCategoryTreeCommand,把整个树形结构作为命令参数。然后在命令处理程序里完成校验和递归更新:
// 批量更新命令定义 public class UpdateCategoryTreeCommand : ICommand { public List<CategoryTreeDto> TreeNodes { get; set; } } // 命令处理程序 public class UpdateCategoryTreeCommandHandler : ICommandHandler<UpdateCategoryTreeCommand> { private readonly ICategoryRepository _categoryRepo; private readonly CategoryTreeDomainService _treeService; public UpdateCategoryTreeCommandHandler(ICategoryRepository categoryRepo, CategoryTreeDomainService treeService) { _categoryRepo = categoryRepo; _treeService = treeService; } public async Task Handle(UpdateCategoryTreeCommand command, CancellationToken cancellationToken) { // 第一步:先校验树深度,不符合规则直接抛出领域异常 _treeService.ValidateTreeDepth(command.TreeNodes); // 第二步:递归遍历树形结构,更新每个分类的父节点和排序位置 await ProcessTreeNodes(command.TreeNodes, parentId: null); // 第三步:提交所有变更到数据库 await _categoryRepo.UnitOfWork.SaveChangesAsync(cancellationToken); } private async Task ProcessTreeNodes(List<CategoryTreeDto> nodes, Guid? parentId) { for (int i = 0; i < nodes.Count; i++) { var node = nodes[i]; // 从仓储获取对应的聚合根 var category = await _categoryRepo.GetByIdAsync(node.Id); if (category == null) { throw new DomainException($"分类ID {node.Id} 不存在"); } // 调用聚合根的方法修改父节点和位置,把领域行为封装在聚合根内 category.UpdateParentAndPosition(parentId, i + 1); // 排序位置通常从1开始计数 // 递归处理子节点 if (node.Children.Any()) { await ProcessTreeNodes(node.Children, node.Id); } } } }
3. 在聚合根内封装修改行为,保证状态一致性
上面代码中的category.UpdateParentAndPosition方法,应该是Category聚合根内部的方法,用来封装修改parentId和position的逻辑,确保聚合根的状态始终合法:
public class Category : AggregateRoot<CategoryId> { public CategoryName Name { get; private set; } public CategoryId? ParentId { get; private set; } public CategoryPosition Position { get; private set; } // 构造函数等其他代码省略... // 封装更新父节点和排序位置的行为 public void UpdateParentAndPosition(CategoryId? newParentId, int newPosition) { // 可以在这里添加额外的领域规则校验,比如不能把自己设为父节点 if (newParentId == Id) { throw new DomainException("分类不能设置自己作为父节点"); } ParentId = newParentId; Position = new CategoryPosition(newPosition); } }
4. 避免基础设施层直接操作领域对象
之前你在基础设施层直接递归修改的做法,会导致领域规则被绕过。正确的做法是:基础设施层只负责实现仓储接口,处理数据持久化逻辑;所有的业务规则校验、状态修改都由领域层(聚合根、领域服务、命令处理程序)来完成。
内容的提问来源于stack exchange,提问作者David

