【问题标题】:How to pass a runtime parameter to an EF Core predicate expression如何将运行时参数传递给 EF Core 谓词表达式
【发布时间】:2023-03-16 05:05:01
【问题描述】:

在使用 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&lt;&gt;Func&lt;&gt; 语法混为一谈,但我认为这是一个相对常见的要求,必须是可能的。

谢谢

【问题讨论】:

    标签: entity-framework-core expression-trees predicatebuilder


    【解决方案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 =&gt; parent.Children.Any(child =&gt; EarnsMoreThanParent(child, parent)) 中的整个表达式都是一个表达式。因此,您的方法 EarnsMoreThanParent 不会被调用,因为它是 Where 表达式的一部分,EF 会尝试将其解析为表达式并且会失败。

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

    【讨论】:

    • 谢谢 - 这很有帮助。我希望能够将我的表达式分成小单元(其中一些被其他人调用),但我开始明白这比看起来更困难,因为你解释的原因。
    • 是的,在这种情况下,它仍然需要从顶部对象(Parent)开始。您也可以使用 PredicateBuilder 或类似的组合并编写 _context.Set&lt;Parent&gt;().Where(EarnsMoreThanParent.And(SomeOtherCondition).Not(AnotherCondition)); 但所有这些条件仍应基于 Parent 对象
    【解决方案2】:

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

    由于您使用[predicatebuilder] 标记了您的问题,我假设您使用的是LINQKit 包,它提供了自定义Invoke 方法。它可以用来“调用”你的表达式:

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

    缺点(除了“调用”表达式不能来自属性或方法的限制)是您必须使用此类 Invoke(...) 调用为每个查询调用 AsExpandable(),否则自定义方法将不会使用效果,你会得到“方法不支持”运行时异常。

    最近发现了一个非常有用的包DelegateDecompiler,它可以让你以更自然的方式实现代码重用。您可以使用常规的 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();
    

    这看起来比使用表达式要好得多。缺点与其他解决方案相同 - 需要为每个查询调用自定义方法。在我对EF Core queries all columns in SQL when mapping to object in Select 的回答中,我展示了一种将包插入 EF Core 3.1 查询处理管道的解决方案(我希望将来 EF Core 中会出现类似的公共扩展点,这将消除大量所需的样板代码那里),它提供了最好的 - 在表达式树中使用 OOP 功能并透明地翻译(扩展,替换为实际表达式)它们。例如插入DelegateDecompiler,正如那里解释的那样,查询就像为 LINQ to Objects 编写的一样:

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

    【讨论】:

    • 感谢发帖!这看起来是一个非常强大的概念,我很高兴尝试一下。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-11
    • 1970-01-01
    • 1970-01-01
    • 2018-08-19
    • 1970-01-01
    • 2017-03-19
    相关资源
    最近更新 更多