ABP框架递归实体树形数据加载的AppService实现咨询
嘿,这个问题我刚好有实战经验!在ABP框架里,并没有专门针对树形实体的现成可直接继承的AppService类,但我们可以基于框架提供的通用CRUD基类扩展,完全能实现支持完整CRUD的树形数据服务,核心就是搞清楚递归加载的逻辑。下面给你一步步拆解:
最省心的方式是继承ABP官方提供的CrudAppService<Product, ProductDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateProductDto, CreateUpdateProductDto>——这个基类帮我们封装了大部分重复的CRUD代码,省得自己从头写增删改查。
如果需要自定义树形相关的方法(比如加载树形结构、获取子节点),建议定义自己的业务接口IProductAppService,在接口里声明这些额外方法,让AppService实现它,这样更符合DDD分层思想。
1. 先搞定实体与Dto设计
实体类(Product)
注意不要直接存子实体集合到数据库(关系型数据库不这么玩),而是用ParentId关联父节点,再通过EF Core导航属性实现父子关联:
public class Product : FullAuditedAggregateRoot<Guid> { public string Description { get; set; } public decimal Price { get; set; } public Guid? ParentId { get; set; } // 父节点ID,根节点为null // EF Core导航属性,关联子节点 public virtual ICollection<Product> Children { get; set; } = new HashSet<Product>(); }
Dto类
需要两个Dto:一个用于普通CRUD的ProductDto,一个用于树形展示的ProductTreeDto(继承自普通Dto,增加子节点集合):
// 普通CRUD用的Dto public class ProductDto : FullAuditedEntityDto<Guid> { public string Description { get; set; } public decimal Price { get; set; } public Guid? ParentId { get; set; } } // 树形展示用的Dto public class ProductTreeDto : ProductDto { public List<ProductTreeDto> Children { get; set; } = new List<ProductTreeDto>(); } // 新增/更新用的Dto public class CreateUpdateProductDto { [Required] public string Description { get; set; } [Required] public decimal Price { get; set; } public Guid? ParentId { get; set; } }
2. 配置EF Core实体映射
在DbContext的OnModelCreating里配置父子关系的外键与删除行为:
protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity<Product>(b => { // 配置父子关联 b.HasMany(p => p.Children) .WithOne() .HasForeignKey(p => p.ParentId) .OnDelete(DeleteBehavior.Restrict); // 禁止级联删除,避免误删子节点,根据业务调整 }); }
3. 实现AppService的树形逻辑
继承CrudAppService后,补充树形加载的方法,分两种场景:
场景1:一次性加载全量树形(适合数据量小的情况)
一次性拉取所有数据,在内存中递归构建树形结构:
public class ProductAppService : CrudAppService<Product, ProductDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateProductDto, CreateUpdateProductDto>, IProductAppService { private readonly IRepository<Product, Guid> _productRepository; public ProductAppService(IRepository<Product, Guid> productRepository) : base(productRepository) { _productRepository = productRepository; } // 获取完整树形结构 public async Task<List<ProductTreeDto>> GetTreeAsync() { // 拉取所有产品数据 var allProducts = await _productRepository.GetListAsync(); // 筛选根节点(ParentId为null的) var rootProducts = allProducts.Where(p => p.ParentId == null).ToList(); // 转换为树形Dto并递归填充子节点 return ObjectMapper.Map<List<Product>, List<ProductTreeDto>>(rootProducts) .Select(dto => FillChildNodes(dto, allProducts)) .ToList(); } // 递归填充子节点的私有方法 private ProductTreeDto FillChildNodes(ProductTreeDto parentDto, List<Product> allProducts) { // 找到当前父节点的所有子产品 var childProducts = allProducts.Where(p => p.ParentId == parentDto.Id).ToList(); parentDto.Children = ObjectMapper.Map<List<Product>, List<ProductTreeDto>>(childProducts); // 递归填充每个子节点的子节点 foreach (var childDto in parentDto.Children) { FillChildNodes(childDto, allProducts); } return parentDto; } }
场景2:懒加载子节点(适合数据量大的情况)
前端点击父节点时,再加载对应的直接子节点,避免一次性加载过多数据:
// 在IProductAppService里添加方法声明 Task<List<ProductTreeDto>> GetChildrenAsync(Guid parentId); // 在AppService里实现 public async Task<List<ProductTreeDto>> GetChildrenAsync(Guid parentId) { // 只查询当前父节点的直接子节点 var childProducts = await _productRepository.GetListAsync(p => p.ParentId == parentId); return ObjectMapper.Map<List<Product>, List<ProductTreeDto>>(childProducts); }
4. 完善CRUD的树形校验逻辑
比如创建/更新时验证父节点是否存在,删除时禁止删除有子节点的父节点:
// 重写CreateAsync,验证父节点合法性 public override async Task<ProductDto> CreateAsync(CreateUpdateProductDto input) { if (input.ParentId.HasValue) { var parentExists = await _productRepository.AnyAsync(p => p.Id == input.ParentId.Value); if (!parentExists) { throw new UserFriendlyException("指定的父产品不存在,请检查ID!"); } } return await base.CreateAsync(input); } // 重写DeleteAsync,禁止删除有子节点的父节点 public override async Task DeleteAsync(Guid id) { var hasChildren = await _productRepository.AnyAsync(p => p.ParentId == id); if (hasChildren) { throw new UserFriendlyException("该产品下存在子节点,请先删除所有子节点再操作!"); } await base.DeleteAsync(id); }
5. 配置AutoMapper映射
在你的Profile类里配置实体与Dto的映射:
public class ProductProfile : Profile { public ProductProfile() { CreateMap<Product, ProductDto>(); CreateMap<CreateUpdateProductDto, Product>(); // 树形Dto的映射,Children属性手动填充,所以忽略自动映射 CreateMap<Product, ProductTreeDto>() .ForMember(dest => dest.Children, opt => opt.Ignore()); } }
- 实体设计用ParentId外键+导航属性是关系型数据库的标准做法,比直接存子集合更易维护。
- 树形加载根据数据量选方式:小数据量一次性构建,大数据量用懒加载。
- 基于ABP的
CrudAppService扩展能快速获得CRUD能力,只需要补充树形相关的自定义逻辑即可。
内容的提问来源于stack exchange,提问作者pinale

