首页 > 解决方案 > 如何在 Entity Framework Core 中构建递归表达式树?

问题描述

我们使用EFCore.SqlServer.HierarchyId来表示数据中的层次结构。

我的目标是返回具有不确定长度的特定路径的对象的后代,例如,给定一棵具有层次结构 1->2->3->4 的树,路径 1/2/3 将返回 4

知道路径的长度后,我可以进行如下查询:

var collections = await context.Collections.Where(c => c.CollectionHierarchyid.IsDescendantOf(
    context.Collections.FirstOrDefault(c1 => c1.FriendlyId == "three" &&
        context.Collections.Any(c2 => c2.CollectionHierarchyid == c1.CollectionHierarchyid.GetAncestor(1) && c2.FriendlyId == "two" &&
            context.Collections.Any(c3 => c3.CollectionHierarchyid == c2.CollectionHierarchyid.GetAncestor(1) && c3.FriendlyId == "one")
        )
    ).CollectionHierarchyid
)).ToListAsync();

但是如果路径的长度未知,你会怎么做呢?我不能从表达式中调用递归函数,因为它不会从 Linq 编译到 Entity Sql。

我知道答案在于使用 System.Linq.Expressions 构建表达式,但我不确定从哪里开始。

标签: entity-framework-corelinq-expressionshierarchyid

解决方案


该问题可以在不生成动态表达式树的情况下解决,至少不是直接生成,而是使用标准 LINQ 查询运算符。

假设您有一个像这样的分层实体

public class Entity
{
    public HierarchyId Id { get; set; }
   // other properties...
}

给定一个返回完整集的子查询

IQueryable<Entity> fullSet = context.Set<Entity>();

和子查询定义一些包含所需祖先的过滤子集

IQueryable<Entity> ancestors = ...;

现在可以很容易地获得所有直接和间接后代

IQueryable<Entity> descendants = fullSet
    .Where(d => ancestors.Any(a => d.Id.IsDescendantOf(a.Id));

所以问题是如何ancestors动态构建子查询。

可以通过使用简单的连接运算符来对整个集合应用一些过滤器并检索由另一个条件过滤的直接祖先

from p in fullSet.Where(condition1)
join c in fullSet.Where(condition2)
on p.Id equals c.Id.GetAncestor(1)
select c

因此,您只需要递归地应用它,例如

IEnumerable<TArg> args = ...;

表示按级别排序的过滤条件参数,则可以按如下方式构建查询

var ancestors = args
    .Select(arg => fullSet.Where(e => Predicate(e, arg)))
    .Aggregate((prevSet, nextSet) =>
        from p in prevSet join c in nextSet on p.Id equals c.Id.GetAncestor(1) select c);

话虽如此,将其应用于您的示例:

IEnumerable<string> friendlyIds = new [] { "one", "two", "three" };

var fullSet = context.Collections.AsQueryable();

var ancestors = friendlyIds
    .Select(friendlyId => fullSet.Where(e => e.FriendlyId == friendlyId))
    .Aggregate((prevSet, nextSet) =>
        from p in prevSet join c in nextSet on p.CollectionHierarchyid equals c.CollectionHierarchyid.GetAncestor(1) select c);

var descendants = fullSet
    .Where(d => ancestors.Any(a => d.CollectionHierarchyid.IsDescendantOf(a.CollectionHierarchyid));


推荐阅读