C# .NET Entity Framework 递归Include未知层级关联实体方案
EF Core 无限层级自关联实体加载方案
EF Core 原生的Include/ThenInclude链式写法不支持未知深度的递归预加载,针对自关联Thing1实体的全层级加载场景,有两种可直接落地的实现方式:
方案1:动态拼接Include加载路径
Include方法支持传入字符串格式的导航属性路径,不需要硬编码Lambda链式调用,你可以提前计算当前实体下的最大嵌套深度,再逐段拼接加载路径,同时覆盖每一层级下关联的Thing2实体。
步骤1:计算当前MainThing下Thing1的最大嵌套深度
// 先取出当前MainThing关联的所有Thing1的Id和自关联外键 var thing1Refs = await _db.Thing1 .Where(t => t.MainThingId == memainthingId) .Select(t => new { t.Id, t.ParentId }) .ToListAsync(); int maxDepth = 0; // 递归计算最大嵌套深度 void CalcDepth(int? parentId, int currentDepth) { if (currentDepth > maxDepth) maxDepth = currentDepth; var childIds = thing1Refs.Where(t => t.ParentId == parentId).Select(t => t.Id); foreach (var cid in childIds) { CalcDepth(cid, currentDepth + 1); } } // 顶级Thing1的ParentId为空,从0层开始计算 CalcDepth(null, 0);
步骤2:动态拼接Include路径执行查询
var query = _db.Mainthing.AsQueryable(); // 加载第一级Thing1及关联的Thing2 query = query.Include("thing1.thing2"); string currentNavPath = "thing1"; // 从第二级开始逐段追加自关联路径,每一级都加载对应的Thing2 for (int i = 1; i <= maxDepth; i++) { currentNavPath += ".thing1"; query = query.Include($"{currentNavPath}.thing2"); } var copy = await query .AsSplitQuery() .AsNoTrackingWithIdentityResolution() .FirstOrDefaultAsync(x => x.Id == memainthingId);
如果你的业务有明确的嵌套上限(比如最多允许嵌套10层),可以跳过计算最大深度的步骤,直接按上限拼接路径即可。由于你开启了身份解析,多拼接的超出实际深度的路径不会引发报错,仅会生成少量冗余查询,对性能影响极小。
方案2:分层递归显式加载
如果你不想动态拼接查询路径,可以分层递归查询每一级的子实体,依托EF Core的身份解析能力,查出来的实体会自动挂载到对应父实体的导航属性上,不需要手动做映射赋值。
// 先加载主实体和第一级Thing1、关联Thing2 var copy = await _db.Mainthing .Include(x => x.thing1).ThenInclude(x => x.thing2) .AsSplitQuery() .AsNoTrackingWithIdentityResolution() .FirstOrDefaultAsync(x => x.Id == memainthingId); if (copy == null) { // 处理主实体不存在的逻辑 return; } var currentLevel = copy.thing1.ToList(); // 循环加载下一级子项,直到没有更深层级为止 while (currentLevel.Any()) { var parentIds = currentLevel.Select(t => t.Id).ToList(); // 查询下一级的所有Thing1和关联的Thing2 var nextLevel = await _db.Thing1 .Include(t => t.thing2) .Where(t => t.ParentId.HasValue && parentIds.Contains(t.ParentId.Value)) .AsSplitQuery() .ToListAsync(); currentLevel = nextLevel; }
这个方案不需要提前计算深度,每一次查询只拉取对应层级的必要数据,没有冗余查询,适合嵌套深度差异大、数据量较高的场景。注意必须开启身份解析(默认跟踪查询或AsNoTrackingWithIdentityResolution),才能实现导航属性的自动挂载。
注意:EF Core目前不支持在Include中自动生成递归CTE实现单SQL递归加载,硬写原生SQL递归的话后续实体映射、维护成本很高,上述两种方案已经能覆盖绝大多数业务场景,性能表现稳定。
内容的提问来源于stack exchange,提问作者Hanna
相关产品推荐
相关产品推荐

