Entity Framework Core如何构建递归Expression树查询任意长度HierarchyId路径
实现方案
思路说明
我们可以先把输入的路径按/拆分得到从根到目标父节点的FriendlyId序列,再根据序列长度动态生成嵌套的查询条件,最后执行查询即可。整个过程不需要写递归,完全可以拼接出EF可以正常解析的SQL。
表达式树实现方案
步骤1:路径预处理
先把输入的路径拆分为有序的节点标识数组:
// 示例输入路径 string inputPath = "one/two/three"; string[] pathSegments = inputPath.Split('/', StringSplitOptions.RemoveEmptyEntries); if (!pathSegments.Any()) { // 空路径可根据业务需求处理,比如返回根节点所有后代 return; }
步骤2:动态拼接查询条件
根据路径长度动态生成嵌套的匹配条件:
using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; // 定义最内层查询参数,对应路径最后一段的节点 var parameter = Expression.Parameter(typeof(Collection), "c"); // 初始条件:匹配路径最后一段的FriendlyId Expression condition = Expression.Equal( Expression.Property(parameter, nameof(Collection.FriendlyId)), Expression.Constant(pathSegments.Last()) ); var currentParam = parameter; // 从倒数第二段开始向上逐层拼接祖先匹配规则 for (int i = pathSegments.Length - 2; i >= 0; i--) { // 生成GetAncestor(1)方法调用,取当前节点的直接父级层级ID var ancestorMethod = typeof(HierarchyId).GetMethod(nameof(HierarchyId.GetAncestor), new[] { typeof(int) }); var ancestorHierarchyId = Expression.Call( Expression.Property(currentParam, nameof(Collection.CollectionHierarchyid)), ancestorMethod, Expression.Constant(1) ); // 构建内层Any查询的条件:父级节点层级ID匹配 + FriendlyId匹配当前段 var innerParam = Expression.Parameter(typeof(Collection), $"c{i}"); var innerCondition = Expression.AndAlso( Expression.Equal( Expression.Property(innerParam, nameof(Collection.CollectionHierarchyid)), ancestorHierarchyId ), Expression.Equal( Expression.Property(innerParam, nameof(Collection.FriendlyId)), Expression.Constant(pathSegments[i]) ) ); var innerLambda = Expression.Lambda<Func<Collection, bool>>(innerCondition, innerParam); // 生成Queryable.Any方法的调用表达式 var anyMethod = typeof(Queryable).GetMethods() .First(m => m.Name == nameof(Queryable.Any) && m.GetParameters().Length == 2) .MakeGenericMethod(typeof(Collection)); var anyCall = Expression.Call( anyMethod, Expression.Constant(context.Collections), innerLambda ); // 拼接当前条件和上层匹配规则 condition = Expression.AndAlso(condition, anyCall); currentParam = innerParam; } // 生成最终查询目标父节点的Lambda表达式 var parentQueryLambda = Expression.Lambda<Func<Collection, bool>>(condition, parameter);
步骤3:执行查询获取结果
// 查询匹配路径的父节点 var parentNode = await context.Collections.FirstOrDefaultAsync(parentQueryLambda); if (parentNode == null) { // 路径不存在的业务处理 return new List<Collection>(); } // 查询父节点的所有后代节点 var descendants = await context.Collections .Where(c => c.CollectionHierarchyid.IsDescendantOf(parentNode.CollectionHierarchyid)) .ToListAsync();
简化实现方案(无需手动拼接表达式树)
如果不想处理复杂的表达式树语法,也可以用逐级过滤的方式实现,EF会自动把整个逻辑翻译为单条SQL,性能没有差异:
string inputPath = "one/two/three"; string[] pathSegments = inputPath.Split('/', StringSplitOptions.RemoveEmptyEntries); // 从根节点第一层开始过滤 IQueryable<Collection> currentLevelNodes = context.Collections .Where(c => c.FriendlyId == pathSegments[0]); // 逐层向下过滤直到匹配路径最后一段 for (int i = 1; i < pathSegments.Length; i++) { var currentSegment = pathSegments[i]; currentLevelNodes = from child in context.Collections join parent in currentLevelNodes on child.CollectionHierarchyid.GetAncestor(1) equals parent.CollectionHierarchyid where child.FriendlyId == currentSegment select child; } // 查询最终匹配节点的所有后代 var result = await context.Collections .Where(c => currentLevelNodes.Any(p => c.CollectionHierarchyid.IsDescendantOf(p.CollectionHierarchyid))) .ToListAsync();
内容的提问来源于stack exchange,提问作者dtryan
相关产品推荐
相关产品推荐

