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

问题描述 投票:0回答:1

在一个使用EF Core的应用程序中,我试图通过创建一个可重用的谓词表达式库来消除查询代码的重复。然而,我正在为接受运行时参数的谓词而苦恼。

让我们假设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查询 检索所有的父类和一个比他们收入更高的子类,比如这样。

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;

上面的谓词编译得很好, 但我还没有找到任何方法来消耗它。问题是如何在运行时将父实体传入其中。这样就不能编译了。

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

我知道我把 "父实体 "和 "母实体 "混为一谈了。Expression<>Func<> 语法,但我认为这是一个比较普遍的要求,一定是可以的。

谢谢你

entity-framework-core expression-trees predicatebuilder
1个回答
1
投票

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

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


1
投票

这是一个有趣的话题,主要是因为这么多年来,C#并没有提供一个合成表达式的语法(例如类似于字符串插值的东西)。许多第三方软件包正试图通过提供自定义扩展方法来解决这个问题,这些方法通过注入查询提供者所需的实际表达式来转换查询表达式树。

由于你把你的问题标记为 [predicatebuilder],我想你使用的是 LINQKit 包,它提供了自定义的 Invoke 方法。它可以用来 "调用 "你的表达式。

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

缺点是(除了 "调用 "表达式不能来自属性或方法的限制外) 你必须调用 AsExpandable() 对每个查询使用这样的 Invoke(...) 调用,否则自定义的方法不会生效,你会得到 "方法不支持 "的运行时异常。

最近我发现了一个非常有用的包,叫做 语音识别器(DeproateDecompiler,它允许你以更自然的方式实现代码重用。您可以使用常规的 OOP 可重用基元(如计算(仅获取)属性和实例扩展方法)来代替可重用表达式,并简单地将它们标记为 [Decompile] 属性,例如在一些公共静态类中。

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

或在 Child 类。

[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();

这看起来比使用表达式要好得多。缺点和其他解决方案一样--需要为每个查询调用自定义方法。在我对 在Select中映射到对象时,EF Core会查询SQL中的所有列。 我已经展示了一个将包插入EF Core 3.1查询处理流水线的解决方案(我期待未来EF Core中会有类似的公共扩展点,它将消除那里所需的大量模板代码),它给出了最好的--在表达式树中使用OOP特性,并透明地翻译(扩展、替换为实际的表达式)它们。例如用 DelegateDecompiler 插上那里的解释,查询就像为LINQ to Objects写的那样简单。

return _context.Set<Parent>()
    .Where(parent => parent.Children.Any(child =>
        child.EarnsMoreThanParent(parent));
© www.soinside.com 2019 - 2024. All rights reserved.