【问题标题】:Pass a lambda parameter to an include statement将 lambda 参数传递给包含语句
【发布时间】:2016-03-09 10:24:43
【问题描述】:

我正在使用System.Data.Entity 命名空间,因此我可以将 lambda 表达式传递给 Linq Include 方法。

public ICollection<MyEntity> FindAll()
    {
        using (var ctx = new MyEntityContext())
        {
            return ctx.MyEntity.Include(x => x.SomeLazyLoadedValue).ToList();
        }
    }

当我在不同的方法中使用 Where 语句时,我可以像这样向它传递一个参数:

public ICollection<MyEntity> FindAllBy(Func<MyEntity, bool> criteria)
    {
        using (var ctx = new MyEntityContext())
        {
            return ctx.MyEntity.Where(criteria).ToList();
        }
    }

但是,在 Include 中尝试同样的事情是行不通的:

public ICollection<MyEntity> FindAll(Func<MyEntity, bool> criteria)
    {
        using (var ctx = new MyEntityContext())
        {
            return ctx.MyEntity.Include(criteria).ToList();
        }
    }

如果你尝试这个,Visual Studio 会抱怨它

Cannot convert from 'System.Func<MyEntity, bool>' to 'string'

如何将 lambda 传递给 Include 方法?

【问题讨论】:

  • 因为Include() extension method you intend to invoke 接受Expression&lt;Func&lt;T, TProperty&gt;&gt; path,而不是Func&lt;T, bool&gt;
  • @CodeCaster 是正确的。您看到的错误是因为 Include() 扩展方法具有接受 string 的重载。

标签: c# entity-framework linq


【解决方案1】:

您的代码存在一些问题。例如,您的 FindAllBy 不执行 sql WHERE 查询,而是加载数据库中的所有条目,然后根据您的 criteria 在内存中过滤。要了解为什么会这样,请查看以下内容:

int a = 5;
long b = 5;

现在,这里发生了什么很明显,但它仍然很重要。编译器读取以下代码并生成两个变量。一个整数和一个长整数,两者的值都设置为数字 5。然而,这两个数字的值是不同的,即使它们(在源代码中)设置为相同的值。一个是 32 位的,一个是 64 位的。

现在,我们来看看下面的代码:

Func<int, string> a = num => num.ToString();
Expr<Func<int, string>> b = num => num.ToString();

同样的事情(或多或少)正在发生。在第一种情况下,C# 编译器认为您需要一个谓词(Func&lt;int, string&gt; 谓词),而第二个值是 Expr&lt;Func&lt;int, string&gt;&gt;,即使这些值的编写方式相同。但是,与第一个示例相反,这里的最终结果大不相同。

谓词被编译为编译器生成的类的方法。它像任何其他代码一样编译,并且只允许您删除一堆样板。另一方面,表达式是实际编写代码的内存表示。例如,在这种情况下,表达式可能类似于Call(int.ToString, $1)。这可以被其他代码读取并转换为例如 SQL,然后用于查询您的数据库。

现在,回到你的问题。 EntityFramework 为您提供IQueryable&lt;T&gt; 实例,这些实例又继承IEnumerable&lt;T&gt;。每当您对可枚举对象进行枚举时,它都会查询数据库。

所有接受委托的扩展方法都在IEnumerable 上定义,因此运行谓词之前查询您的数据库。这就是为什么您需要确保选择正确的方法重载。

编辑(回答评论)]
为了更清楚一点,我将举几个例子。假设我们有一个 User 类,它包含 FirstNameLastNameAge,而 db 集合简称为 db

Expr<Func<User, bool>> olderThan10 = u => u.Age > 10;
Func<User, bool> youngerThan90 = u => u.Age < 90;
var users = db.Where(olderThan10).Where(youngerThan90);

这将导致 SQL 查找所有年龄超过 10 岁的用户,之后它会在内存中过滤掉所有年龄超过或等于 90 岁的用户。

所以传递Func 并不一定意味着它会查询整个数据库。这只是意味着它在该点停止构建查询并执行它。

至于下一个问题,Expression&lt;Func&lt;T,bool&gt;&gt; 不是一个通用的答案。它的意思是“一个接受 T 并返回布尔值的表达式”。在某些情况下,例如启动整个问题的.Include,您不想返回布尔值。你想返回任何你想包含的东西。因此,例如,如果我们回到我们的用户示例,并在引用另一个用户的用户类上修改 Father 属性,并且我们希望将其包含在常规代码中,我们会这样做

db.Include(u => u.Father);

现在。这里u是一个User,返回值u.Father也是一个用户,所以这种情况下u =&gt; u.FatherExpr&lt;Func&lt;User, User&gt;&gt;或者Expr&lt;Func&lt;User, object&gt;&gt;(不知道entity-framework.Include是否接受通用值或只是objects)。

所以你的FindAll 函数应该如下所示:

public ICollection<TData> FindAll<TInclude>(Expr<Func<TData, TInclude>> include) {
    using (var ctx = new TContext()) {
        return ctx.T.Include(include).ToList();
    }
}

不过,老实说,这是看起来很奇怪的代码,而且很可能你正在对你的模型做一些其他奇怪的事情,因为你(例如)将它们命名为 TTContext。我的猜测是,您需要阅读一下泛型在 C# 中的工作原理。

【讨论】:

  • 谢谢,读起来很有趣。为了确保我理解正确:使用 System.Data.Entity 为我的所有 Linq 方法添加了额外的重载,以接受 Expression> 。但是,这会导致我传递 Func 而不是 Expression 的所有方法在过滤返回的数据之前查询整个数据库。解决方案是更改所有方法以使用 Expression>,对吗?
  • @ohyeah - 你不能将 Func 传递给这些方法 - 你只能传递 Expression&lt;Func&lt;&gt;&gt;
  • 啊啊啊。因此,在我的源代码中,我似乎在传递一个 Func,但编译器将其处理为表达式,这会导致查询效率低下。正确的?如果是这样,我将如何解决这个问题?
  • @ohyeah 我已经附加了我的答案以澄清一点并回答您的评论。
  • @ohyeah - 编译器发挥了它的魔力并像 Expression&lt;&gt; 一样处理它,但我不知道你为什么说这会导致 inefficient 查询 - 这很相反。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-03-26
  • 1970-01-01
  • 1970-01-01
  • 2020-10-02
  • 2011-08-07
  • 2023-03-29
  • 1970-01-01
相关资源
最近更新 更多