You need to enable JavaScript to run this app. Entity Framework Core如何构建递归Expression树查询任意长度HierarchyId路径
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.15 15:24:03