如何在 Entity Framework Core 中构建递归表达式树?

How do you build a recursive Expression tree in Entity Framework Core?

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

我的目标是 return 具有不确定长度的特定路径的对象的后代,例如给定一棵层次结构为 one->two->three->four 的树,路径 one/two/three 将 return four

知道路径的长度,我可以这样查询:

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 编译到实体 Sql.

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

不用动态表达式树生成就可以解决这个问题,至少不能直接生成,而是使用标准的 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));