这是我的测试功能:
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"));
但这不起作用:
var airplanes = _dataContext.Airplanes.Where(p => Test(p.Status.ToString(), "a");
而是抛出错误:
类型为“ System.NotSupportedException”的第一次机会异常
发生在System.Data.Linq.dll中
附加信息:方法'Boolean Test(System.String,
System.String)'不支持SQL转换。
最初,我将整个
p
变量传递给该函数,并认为问题可能在于该参数是一个自定义的,非SQL识别的类,因此我使参数成为该类的字符串属性,但它无法解决任何问题。为什么不能将范围变量的属性用作参数?有办法解决这个问题吗?因为这意味着我可以将其分解为漂亮的小方法,而不是令人讨厌的linq查询。
编辑:此外,为什么this示例在看起来好像在做相同的事情时又起作用,为什么将迭代变量的属性作为参数传递:
private bool IsInRange(DateTime dateTime, decimal max, decimal min)
{
decimal totalMinutes = Math.Round((dateTime - DateTime.Now).TotalMinutes, 0);
return totalMinutes <= max && totalMinutes > min;
}
// elsewhere
.Where(m => IsInRange(m.DateAndTime, 30, 0));
最佳答案
在前两种情况下,对Test的调用与lambda中的参数无关,因此它们都减小为p => true
。
在第三种中,类似的情况发生,尽管有时会减少为p => true
,有时会减少为p => false
,但是无论哪种方式,创建表达式时,都会找到调用Test
的结果,然后将其输入表达式作为一个常数。
在第四个表达式中,子表达式包括访问实体的属性并调用Contains
,EF都可以理解这两个子表达式,并且可以将它们转换为SQL。
在第五个表达式中,子表达式访问属性并调用Test
。 EF不了解如何将调用转换为Test
,因此您需要将其与SQL函数相关联,或者需要重写Test
以使其创建表达式而不是直接计算结果。
有关承诺的更多表达式:
让我们从您可能已经知道的两件事开始,但是如果您不了解,那么其余的事情将更难以理解。
首先是p => p.Status.Contains("a")
的实际含义。
就其本身而言,绝对没有。与C#中的大多数表达式不同,lambdas不能具有没有上下文的类型。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")
没有类型,因此也不允许var λ = (Airplane p) => p.Status.Contains("a");
。
相反,lambda表达式的类型可以是委托类型,也可以是强类型的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");
好的。也许您知道,否则请立即执行。
第二件事是
Where
在Linq中实际执行的操作。这样定义了
Queryable
的Where
形式(我们现在将忽略Enumerable
形式,然后再回到它):public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)
IQueryable<T>
表示可以获取0个或更多项目的东西。它可以通过四种方法完成四件事:给您一个枚举器,以枚举这些项(从
IEnumerable<T>
继承)。告诉您项目的类型(将是
typeof(T)
,但它是从IQueryable
继承的,但并不明显)。告诉你它的查询提供者是什么。
告诉你它的表达是什么。
现在,最后两个是这里的重要部分。
如果以
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
!就像在Where
中调用IQueryable
结果说:“嘿,您应该在此处调用IQueryable
”。那么,接下来会发生什么呢?
好吧,迟早我们要进行一些操作,导致
Where
不用于返回另一个Expression<Func<Airplane, bool>>
,而是某个其他代表查询结果的对象。为了简单起见,假设我们只是开始枚举单个Func<Airplane, bool>
的结果。如果是Linq-to-objects,那么我们将拥有一个可查询的表达式,该表达式表示:
使用
true
并对其进行编译,以便您具有yield
委托。遍历列表中的每个元素,并使用它调用该委托。如果委托返回Enumerable
,则Where
该元素,否则不返回。(顺便说一下,这是
Func<Airplane, bool>
的Expression<Func<Airplane, bool>>
版本直接对Where
而不是Where
进行的操作。请记住,当我说Enumerable
的结果是一个表达式时,表示“嘿,您应该调用< cc>在这里”?这几乎就是它的作用,但是因为提供者现在选择了Where
的Func<Airplane, bool>
形式并使用Expression<Func<Airplane, bool>>
而不是IQueryable<T>
,所以我们得到了想要的结果。这也意味着只要IEnumerable<T>
提供的操作具有与Expression<Func<Airplane, bool>>
linq-to-objects提供的等效项,就可以满足linq通常满足的所有要求。但这不是linq-to-objects,它是EF,所以我们拥有的表达式表示:
将
WHERE
转换为SQL布尔表达式,例如可以在SQL WHERE
子句中使用的表达式。然后将其作为SELECT * FROM Airplanes
子句添加到较早的表达式(转换为p => p.Status.Contains("a")
)中。棘手的地方是“并将其转换为SQL布尔表达式”。
当您的lambda是
CONTAINS (Status, 'a')
时,则可以生成SQL(取决于SQL版本)Status LIKE '%a%'
或SELECT * FROM Airplanes WHERE Status LIKE '%a%'
或其他类型的数据库。因此,最终结果为.Status
等。 EF知道如何将表达式分解为组件表达式,如何将string
转换为列访问,以及如何将Contains(string value)
的p => Test(p.Status.ToString(), "a")
转换为SQL where子句。当您的lambda是
NotSupportedException
时,结果是Test
,因为EF不知道如何将.Where(p => Test(p.Status.ToString(), someStringParameter))
方法转换为SQL。好的。那就是肉,让我们吃布丁。
您能否详细说明“重写测试,以便它创建一个表达式而不是直接计算结果”的含义。
这里的问题是,我不十分了解您的最终目标是什么,就像您想变得灵活一样。因此,我将通过三种方式执行与
_dataContext.Airplanes.FilterByStatus("a")
等效的操作:一个简单的方法,一个非常简单的方法和一个困难的方法,可以通过各种方法使它们变得更加灵活。首先最简单的方法:
public static class AirplaneQueryExtensions
{
public static IQueryable<Airplane> FilterByStatus(this IQueryable<Airplane> source, string statusMatch)
{
return source.Where(p => p.Status.Contains(statusMatch));
}
}
在这里您可以使用
Where()
,就好像您在使用工作的Where()
一样。因为那正是它在做什么。尽管在更复杂的_dataContext.Airplanes.Where(StatusFilter("a"))
调用上当然存在DRY的范围,但我们在这里实际上并没有做很多事情。大致同样容易:
public static Expression<Func<Airplane, bool>> StatusFilter(string sought)
{
return p => p.Status.Contains(sought);
}
在这里,您可以使用
Where()
,再次与使用工作的StatusFilter
几乎相同。同样,我们在这里没有做很多事情,但是如果过滤器更复杂,则存在DRY的范围。现在提供有趣的版本:
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;
}
这与
typeof()
的先前版本几乎完全相同,只是使用.NET元数据标记来标识类型,方法和属性,而我们使用"p"
和名称。如每一行中的注释所示,第一行获得一个表示属性的表达式。我们真的不需要给它起一个名字,因为我们不会直接在源代码中使用它,但是无论如何我们都将其称为
PropertyInfo
。下一行为
Status
获取p
,随后的一行创建一个表达式,表示为p.Status
得到,因此为sought
。下一行创建一个表示
sought
常量值的表达式。虽然Test("abc", "a")
通常不是常量,但它是根据我们正在创建的整体表达式(这就是EF能够将true
视为常量MethodInfo
而不用翻译的原因)。下一行获取
Contains
的p.Status
,并在下一行中创建一个表达式,该表达式表示使用sought
作为参数在p => p.Status.Contains(sought)
结果上调用该表达式。最后,我们创建一个表达式,将它们全部绑定为等价的
p => p.Status.Contains(sought)
,然后将其返回。显然,这比仅执行
Test
还要多得多。很好,这就是在C#中使用lambda表示表达式的要点,因此我们通常不必这样做。确实,要拥有与您的
p => Test(p.Status, "a")
相同的基于表达式的表达式,我们发现自己在做:public static MethodCallExpression Test(Expression a, string b)
{
return Expression.Call(a, typeof(string).GetMethod("Contains", new[]{ typeof(string) }), Expression.Constant(b));
}
但是,要使用它,我们需要做更多基于表达式的工作,因为我们不能只是
p.Status
,因为_dataContext.Airplanes.UseTest("a")
在那种情况下不是表达式。我们必须做:public static Expression<Func<Airplane, bool>> UseTest(string b)
{
var param = Expression.Parameter(typeof(Airplane));
return Expression.Lambda<Func<Airplane, bool>>(Test(Expression.Property(param, typeof(Airplane).GetProperty("Status")), b), param);
}
现在终于可以使用
但是,基于表达式的方法有两个优点。
如果我们要对超出lambda允许方向的表达式进行一些操作,则可以使用它,例如在https://stackoverflow.com/a/30795217/400547中,它们与反射一起使用以能够指定要作为字符串访问的属性。
您希望您对linq在幕后如何工作有足够的了解,以了解您需要了解的所有信息,以全面了解问题中某些查询为何有效,而在某些地方无效。
关于c# - 为什么在LINQ查询中调用方法时不能使用范围值的属性作为参数?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31575057/