为什么我不能在LINQ查询中调用方法时使用范围值的属性作为参数?

时间:2015-07-22 22:21:20

标签: c# sql-server linq linq-to-sql

这是我的测试功能:

sizeof p:8
sizeof arr:40
sizeof arg:8
sizeof arg:8

为什么这样做:

public bool Test(string a, string b)
{
     return a.Contains(b);
}

这项工作:

var airplanes = _dataContext.Airplanes.Where(p => Test("abc", "a"));

有效:

string s = "abc";
var airplanes = _dataContext.Airplanes.Where(p => Test(s, "a"));

这项工作:

var airplanes = _dataContext.Airplanes.Where(p => Test(new Random().Next(1, 10).ToString(), "1"));

但这不起作用:

var airplanes = _dataContext.Airplanes.Where(p => p.Status.Contains("a"));

而是抛出错误:

  

“System.NotSupportedException”类型的第一次机会异常   发生在System.Data.Linq.dll

中      

附加信息:方法'布尔测试(System.String,   System.String)'没有支持的SQL转换。

最初我将整个var airplanes = _dataContext.Airplanes.Where(p => Test(p.Status.ToString(), "a"); 变量传递给函数,并认为问题可能是参数是一个自定义的,非SQL识别的类,所以我创建的参数只是类的字符串属性但是它没有解决任何问题。

为什么我不能将范围变量的属性用作参数?有办法解决这个问题吗?因为这意味着我可以将它分解成漂亮的小方法而不是令人难以置信的丑陋的linq查询。

编辑另外,为什么this示例在执行相同操作时起作用,将迭代变量的属性作为参数传递:

p

3 个答案:

答案 0 :(得分:0)

在前两种情况下,对Test的调用与lambda中的参数无关,因此它们都减少到p => true

在第三种类似的情况下,虽然它有时会减少到p => true,有时会减少到p => false,但无论哪种方式,在创建表达式时,都会调用{{{找到1}},然后将其作为常量输入表达式。

在第四个表达式中,表达式包括访问实体属性并调用Test的子表达式,这两个表达式都是EF理解并可以转换为SQL。

第五个表达式包含访问属性并调用Contains的子表达式。 EF并不了解如何将调用转换为Test,因此您需要将其与SQL函数关联,或重写Test以便创建表达式而不是直接计算结果。

更多关于承诺的表达方式:

让我们从你可能已经知道的两件事开始,但如果你不这样做,那么剩下的就更难理解了。

第一个是Test的实际含义。

这就是它自己,绝对没有。与C#中的大多数表达式不同,lambda不具有没有上下文的类型。

p => p.Status.Contains("a")的类型为1 + 3,因此在int中,编译器会为var x = 1 + 3提供类型x。即使int也会以long x = 1 + 3表达式int开头,然后将其转换为1 + 3

long没有类型。即使p => p.Status.Contains("a")没有类型,也不允许(Airplane p) => p.Status.Contains("a")

相反,lambda表达式的类型可以是委托类型,也可以是委托人强类型的var λ = (Airplane p) => p.Status.Contains("a");。因此所有这些都是允许的(并且意味着什么):

Expression

好。也许你知道,如果不是你现在就做。

第二件事是Func<Airplane, bool> funcλ = p => p.Status.Contains("a"); Expression<Func<Airplane, bool>> expFuncλ = p => p.Status.Contains("a"); delegate bool AirplanePredicate(Airplane plane); AirplanePredicate delλ = p => p.Status.Contains("a"); Expression<AirplanePredicate> expDelλ = p => p.Status.Contains("a"); 在Linq中实际做了什么。

Where Queryable形式的Where(我们现在忽略Enumerable表单,然后回到它)定义如下:

public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)

IQueryable<T>表示可以获得0个或更多项目的内容。它可以通过四种方法完成四件事:

  1. 为您提供一个枚举器来枚举这些项目(继承自IEnumerable<T>)。
  2. 告诉您它具有哪种类型的项目(typeof(T),但它继承自IQueryable,而不是那么明显。)
  3. 告诉您它的查询提供程序是什么。
  4. 告诉你它的表达方式。
  5. 现在,最后两个是重要的部分。

    如果从new List<Airplane>().AsQueryable开始,则查询提供程序将是EnumerableQuery<Airplane>,它是一个处理有关Airplane的内存中枚举的查询的类,其表达式将表示返回该列表。

    如果从_dataContext.Airplanes开始,提供程序将是System.Data.Entity.Internal.Linq.DbQueryProvider,它是一个处理有关数据库的EF查询的类,其表达式将表示在数据库上运行SELECT * FROM Airplanes并且然后为返回的每一行创建一个对象。

    现在,Where的工作是让提供者创建一个新的IQueryable,它代表根据传递给它的Expression<Func<Airplane, bool>>过滤我们开始的表达式的结果。

    有趣的是,这是非常自我参照:Where使用IQueryable<Airplane>Expression<Func<Airplane, bool>>的参数调用时返回的表达式实际上代表调用Where } IQueryable<Airplane>Expression<Func<Airplane, bool>>的参数!这就像调用Where会导致Where说&#34;嘿,你应该在这里拨打Where&#34;。

    那么,接下来会发生什么?

    好吧,我们迟早会做一些操作导致IQueryable没有被用来返回另一个IQueryable,而是一些其他对象代表查询的结果。为了简单起见,我们假设我们只是开始列举单Where的结果。

    如果它是Linq-to-objects,那么我们所拥有的是一个带有表达式的可查询表示:

      

    获取Expression<Func<Airplane, bool>>并进行编译,以便拥有Func<Airplane, bool>代表。循环遍历列表中的每个元素,使用它调用该委托。如果代理人返回true,那么yield该元素,否则不会。

    (顺便提一下,Enumerable WhereFunc<Airplane, bool>版直接使用Expression<Func<Airplane, bool>>代替Where。请记住,当我说{{1}的结果时这是一个表达的说法&#34;嘿,你应该在这里打电话给Where&#34;?这几乎就是它的作用,但是因为提供商现在选择了Enumerable形式的Where使用Func<Airplane, bool>而不是Expression<Func<Airplane, bool>>,我们得到了我们想要的结果。这也意味着只要IQueryable<T>上提供的操作具有相同的效果IEnumerable<T> linq-to-objects可以满足linq通常需要的所有内容。

    但这不是linq-to-objects,它是EF所以我们所拥有的是一个表达意味着:

      

    Expression<Func<Airplane, bool>>并将其转换为SQL布尔表达式,例如可以在SQL WHERE子句中使用。然后将其作为WHERE子句添加到早期表达式(转换为SELECT * FROM Airplanes)。

    这里的棘手问题是&#34;并将其转换为SQL布尔表达式&#34;。

    当您的lambda为p => p.Status.Contains("a")时,可以为不同类型的数据库生成SQL(取决于SQL版本)CONTAINS (Status, 'a')Status LIKE '%a%'或其他内容。因此最终结果为SELECT * FROM Airplanes WHERE Status LIKE '%a%'左右。 EF知道如何将该表达式分解为组件表达式,以及如何将.Status转换为列访问权以及如何将string的{​​{1}}转换为SQL where子句。< / p>

    当你的lambda为Contains(string value)时,结果为p => Test(p.Status.ToString(), "a"),因为EF不知道如何将NotSupportedException方法转换为SQL。

    好。这是肉,让我们去布丁。

      

    你能详细说明你的意思&#34;重写测试所以它创建一个表达而不是直接计算结果&#34;。

    这里的一个问题是,我不太了解你的最终目标,就像你想要灵活的地方一样。因此,我将以三种方式做一些相当于Test的事情;一种简单的方法,一种非常简单的方法,一种艰难的方式,可以通过各种方式使它们变得更加灵活。

    首先是最简单的方法:

    .Where(p => Test(p.Status.ToString(), someStringParameter))

    在这里,您可以使用public static class AirplaneQueryExtensions { public static IQueryable<Airplane> FilterByStatus(this IQueryable<Airplane> source, string statusMatch) { return source.Where(p => p.Status.Contains(statusMatch)); } } ,就像您使用了_dataContext.Airplanes.FilterByStatus("a")一样。因为这正是它正在做的事情。我们在这里做的并不多,尽管在更复杂的Where()电话上确实存在DRY的余地。

    大致相同:

    Where()

    在这里,您可以使用public static Expression<Func<Airplane, bool>> StatusFilter(string sought) { return p => p.Status.Contains(sought); } 并使用它,就像您使用工作_dataContext.Airplanes.Where(StatusFilter("a"))一样。同样,我们在这里做了很多工作,但如果过滤器更复杂,那么DRY的范围也很广。

    现在有趣的版本:

    Where()

    除了使用.NET元数据标记识别类型,方法和属性以及我们使用public static Expression<Func<Airplane, bool>> StatusFilter(string sought) { var param = Expression.Parameter(typeof(Airplane), "p"); // p var property = typeof(Airplane).GetProperty("Status"); // .Status var propExp = Expression.Property(param, property); // p.Status var soughtExp = Expression.Constant(sought); // sought var contains = typeof(string).GetMethod("Contains", new[]{ typeof(string) }); // .Contains(string) var callExp = Expression.Call(propExp, contains, soughtExp); // p.Status.Contains(sought) var lambda = Expression.Lambda<Func<Airplane, bool>>(callExp, param); // p => p.Status.Contains(sought); return lambda; } 和名称之外,它与幕后的StatusFilter的前一版本完全相同。 / p>

    当每行中的注释显示时,第一行获得表示属性的表达式。我们并不需要给它起一个名字,因为我们不会直接在源代码中使用它,但我们无论如何都称它为typeof()

    下一行获取"p"的{​​{1}},后续版本会创建一个表达式,表示PropertyInfoStatus

    下一行创建一个表达p常量值的表达式。虽然p.Status通常不会保持不变,但它与我们正在创建的整体表达方式有关(这就是为什么EF能够将sought视为常量sought而不是翻译它。)

    下一行获取Test("abc", "a")的{​​{1}},在下一行中我们创建一个表达式,表示在true的结果上调用MethodInfo作为参数。

    最后我们创建一个表达式,将它们全部绑定到等效的Contains,然后返回它。

    这显然比做p.Status更多的工作。好吧,这就是在C#中使用lambda表达式的重点,所以我们通常不必做这项工作。

    的确,要有一个真实的基于表达式的等价物,我们发现自己在做sought

    p => p.Status.Contains(sought)

    但是要使用它我们需要做更多基于表达式的工作,因为我们不能p => p.Status.Contains(sought)因为Test不是该上下文中的表达式。我们必须这样做:

    public static MethodCallExpression Test(Expression a, string b)
    {
      return Expression.Call(a, typeof(string).GetMethod("Contains", new[]{ typeof(string) }), Expression.Constant(b));
    }
    

    现在终于可以使用p => Test(p.Status, "a")了。呼!

    基于表达式的方法有两个优点。

    1. 如果我们想要使用除了lambdas允许方向之外的表达式进行一些操作,我们可以使用它,例如在https://stackoverflow.com/a/30795217/400547中,它们与反射一起使用,以便能够指定要访问的属性一个字符串。
    2. 您希望足够了解linq如何在幕后工作,了解您需要了解的所有内容,以便充分了解为什么您的问题中的某些查询有效,有些则无效。

答案 1 :(得分:0)

在示例中我看到Where方法没有在数据库本身上运行,但是已经从数据库中提取了一个列表。

即。这不起作用:

var airplanes = _dataContext.Airplanes.Where(p => Test(p.Status.ToString(), "a");

但这样做:

var airplanes = _dataContext.Airplanes.ToList().Where(p => Test(p.Status.ToString(), "a");

虽然在我的情况下,这种做法违背了做LINQ而不是迭代的目的。

答案 2 :(得分:-1)

LINQ to Entities必须知道如何将代码转换为可以针对数据库运行的SQL查询。

当您使用时(我从您发布的代码中删除了ToString来电,因为ToString无法使用LINQ to Entities)

var airplanes = _dataContext.Airplanes.Where(p => p.Status.Contains("a"));

编译器将您的lambda转换为LINQ提供程序可以遍历并生成正确SQL的表达式树:

  

您可以让C#或Visual Basic编译器根据匿名lambda表达式为您创建表达式树,或者您可以使用System.Linq.Expressions命名空间手动创建表达式树。 / p>      

引自Expression Trees (C# and Visual Basic)

它知道如何将来电转换为IEnumerable.Contains,因为它是已添加到其中的方法之一,因为已知&#39;东西 - 它知道它必须生成IN语句,就像它知道!=应该在SQL中被转换为<>一样。

使用时

var airplanes = _dataContext.Airplanes.Where(p => Test(p.Active, "a");

所有表达式树都有Test方法调用,LINQ提供程序对此一无所知。它也无法检查它的内容,发现它实际上只是绕过Contains调用,因为这些方法被编译为IL,而不是表达式树。

<强> 附录

至于为什么Where(p => Test("abc", "a"))单词和Where(p => Test(s, "a"))并不是100%肯定,但我的猜测是LINQ提供商足够聪明,可以看到你的使用两个常量值调用它,因此它只是尝试执行它并查看它是否可以返回值,这可以在SQL查询中视为常量。