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

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

这是我的测试功能:

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 + 3int表达式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中实际执行的操作。

这样定义了QueryableWhere形式(我们现在将忽略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>在这里”?这几乎就是它的作用,但是因为提供者现在选择了WhereFunc<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而不用翻译的原因)。

下一行获取Containsp.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/

相关文章:

mysql - 我可以在一张表的复合主键中有太多列吗

sql-server - SQL Server DDL 代码的版本控制

c# - LINQ:确定两个序列是否包含完全相同的元素

c# - 使用可选参数创建 Type 实例的 Linq 表达式?

c# - 我可以使用什么构造来代替 Contains?

c# - 如何在 Asp.Net Core 2.x 中将 root 重定向到 swagger?

c# - TCP 会分解小于 1kb 的数据吗?

sql - 事务 SQL : selecting a boolean expression

c# - 将数组的一部分复制到另一个数组中

c# - MVC3 & StructureMap,基于 Controller 注入(inject)具体类