如何将运行时参数传递给 EF Core 谓词表达式

How to pass a runtime parameter to an EF Core predicate expression

在使用 EF Core 的应用程序中,我试图通过创建可重用的谓词表达式库来消除查询代码的重复。但是,我正在努力处理接受运行时参数的谓词。

让我们假设 parent child 关系中的 2 个简单实体 类:

public class Parent
{
    public double Salary { get; set; }
    public ICollection<Child> Children { get; set; }

}

public class Child
{
    public double Salary { get; set; }
}

我可以使用像这样的常规 EF Core 查询检索所有 parent 的 child 收入比他们多的人:

return Context.Set<Parent>()
    .Where(parent => parent.Children.Any(child =>
        child.Salary > parent.Salary));

如果我想为上述查询创建一个可重用的谓词,我想它可能看起来像这样:

private Expression<Func<Child, Parent, bool>> EarnsMoreThanParent = (Child child, Parent parent) => child.Salary > parent.Salary;

上面的谓词编译正常,但我还没有找到任何方法来使用它。问题是如何在运行时将 parent 实体传递给它。这不编译:

return _context.Set<Parent>()
    .Where(parent => parent.Children.Any(child =>
        EarnsMoreThanParent(child, parent));

我知道我将 Expression<>Func<> 语法混为一谈,但我认为这是一个相对普遍的要求,必须是可能的。

谢谢

你可以尝试像这样包裹整个表达式

private Expression<Func<Parent, bool>> EarnsMoreThanParent = 
    (Parent parent) => parent.Children.Any(child => child.Salary > parent.Salary)

然后用于整个 Where

return _context.Set<Parent>().Where(EarnsMoreThanParent);

你的代码的问题在于.Where(parent => parent.Children.Any(child => EarnsMoreThanParent(child, parent))里面的整个表达式都是一个表达式。因此,您的方法 EarnsMoreThanParent 不会被调用,因为它是 Where 表达式的一部分,并且 EF 将尝试将其解析为表达式并且会失败。

但是,是的,如果您需要将几个这样的条件与不同的 OR 结合起来,它可能无法像 Where(EanrsMoreThanParent).Where(SomeOtherCondition) 那样工作,所有这些都将被转换为 AND

这是一个有趣的话题,主要是因为这么多年 C# 没有提供用于组合表达式的语法(例如类似于字符串插值的语法)。许多 3rd 方包正试图通过提供自定义扩展方法来解决该问题,该方法最终通过注入查询提供程序所需的实际表达式来转换查询表达式树。

因为你用 [predicatebuilder] 标记了你的问题,我假设你正在使用 LINQKit 包,它提供自定义 Invoke 方法。它可以用于"invoke"你的表达:

return _context.Set<Parent>()
    .Where(parent => parent.Children.Any(child =>
        EarnsMoreThanParent.Invoke(child, parent))
    .AsExpandable();

缺点(除了 "invoked" 表达式不能来自 属性 或方法的限制之外)是您必须为每个使用此类 [=18 的查询调用 AsExpandable() =]调用,否则自定义方法不会生效,你会得到"method not supported"运行时异常。

最近我发现了一个非常有用的包DelegateDecompiler,它可以让你以更自然的方式实现代码重用。您可以使用常规的 OOP 可重用基元,例如计算(仅获取)属性和 instance/static/extension 方法,而不是可重用表达式,并简单地用 [Decompile] 属性标记它们。例如在某些 public 静态 class:

[Decompile]
public static bool EarnsMoreThanParent(this Child child, Parent parent) => child.Salary > parent.Salary;

或在 Child class:

[Decompile]
public bool EarnsMoreThanParent(Parent parent) => this.Salary > parent.Salary;

然后您只需在查询的某个点调用 Decompile() 自定义扩展方法:

return _context.Set<Parent>()
    .Where(parent => parent.Children.Any(child =>
        child.EarnsMoreThanParent(parent))
    .Decompile();

这看起来比使用表达式好多了。缺点与其他解决方案相同——需要为每个查询调用自定义方法。在我对 的回答中,我展示了一个将包插入 EF Core 3.1 查询处理管道的解决方案(我期待将来在 EF Core 中出现类似的 public 扩展点,这将消除那里需要很多样板代码),这提供了最好的 - 在表达式树中使用 OOP 功能并透明地翻译(扩展,替换为实际表达式)它们。例如按照那里的解释插入 DelegateDecompiler,查询就像为 LINQ to Objects:

编写的一样简单
return _context.Set<Parent>()
    .Where(parent => parent.Children.Any(child =>
        child.EarnsMoreThanParent(parent));