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

Why can't I use a range value's property as a parameter when calling a method in a LINQ query?

这是我的测试函数:

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");

而是抛出错误:

A first chance exception of type 'System.NotSupportedException' occurred in System.Data.Linq.dll

Additional information: Method 'Boolean Test(System.String, System.String)' has no supported translation to SQL.

最初我将整个 p 变量传递给函数并且认为问题可能是参数是自定义的,非 SQL 识别 class,所以我做了参数只是 class 的字符串属性,但它没有解决任何问题。

为什么我不能使用范围变量的 属性 作为参数?有办法解决这个问题吗?因为这意味着我可以将它分解成漂亮的小方法,而不是非常丑陋的 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));  

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

当您使用 (我从您发布的代码中删除了 ToString 调用,因为 ToString 不适用于 LINQ to Entities):

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

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

You can have the C# or Visual Basic compiler create an expression tree for you based on an anonymous lambda expression, or you can create expression trees manually by using the System.Linq.Expressions namespace.

quote from Expression Trees (C# and Visual Basic)

它知道如何将调用转换为 IEnumerable.Contains 因为它是作为 'known' 东西添加到它的方法之一 - 它知道它必须生成相同的 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 查询中被视为常量。

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

在第三个中,这种情况类似,虽然它有时会减少到 p => true,有时会减少到 p => false,但无论哪种方式,在创建表达式时,调用 [= 的结果20=] 被找到,然后作为常量输入表达式。

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

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

更多关于表达的承诺:

让我们从您可能已经了解的两件事开始,但如果您不了解,那么其余的事情将更难理解。

第一个是p => p.Status.Contains("a")实际的意思。

这本身就什么都不是。与 C# 中的大多数表达式不同,lambda 不能有没有上下文的类型。

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 中实际做的事情。

WhereQueryable 形式(我们暂时忽略 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. 告诉你它的表达式是什么

现在,最后两个是这里的重要部分。

如果您从 new List<Airplane>().AsQueryable 开始,那么查询提供程序将是一个 EnumerableQuery<Airplane>,它是一个 class,用于处理有关 Airplane 的内存中枚举的查询,它的表达式将表示 returning 该列表。

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

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

有趣的是,这是非常棒的自我引用:表达式 return 由 Where 编辑,当你用 IQueryable<Airplane>Expression<Func<Airplane, bool>> 的参数调用它时fact 表示使用 IQueryable<Airplane>Expression<Func<Airplane, bool>> 的参数调用 Where!这就像调用 Where 结果 Where 说 "hey, you should call Where here".

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

好吧,迟早我们会执行一些操作,导致 IQueryable 不被用于 return 另一个 IQueryable 而是代表查询结果的其他对象。为了简单起见,假设我们只是开始枚举我们的单个 Where.

的结果

如果它是 Linq-to-objects 那么我们将拥有一个可查询的表达式,这意味着:

Take the Expression<Func<Airplane, bool>> and compile it so you have a Func<Airplane, bool> delegate. Cycle through every element in the list, calling that delegate with it. If the delegate returns true then yield that element, otherwise don't.

(顺便说一下,WhereEnumerable 版本直接用 Func<Airplane, bool> 而不是 Expression<Func<Airplane, bool>> 做的。记得我说过 Where 是一个表示 "hey, you should call Where here" 的表达式吗?这就是它的作用,但是因为提供者现在选择 WhereEnumerable 形式并使用 Func<Airplane, bool> 而不是Expression<Func<Airplane, bool>>,我们得到了我们想要的结果。这也意味着只要在 IQueryable<T> 上提供的操作具有在 IEnumerable<T> 上提供的等效操作,linq-to-objects 就可以满足 linq 的所有需求一般迎合)。

但这不是 linq-to-objects,它是 EF,所以我们有一个表达式,意思是:

Take the Expression<Func<Airplane, bool>> and turn it into a SQL boolean expression such as can be used in a SQL WHERE clause. Then add that as a WHERE clause to the earlier expression (which translates to SELECT * FROM Airplanes).

这里的棘手之处是 "and turn it into a SQL boolean expression"。

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

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

好的。那是肉,让我们开始吃布丁吧。

Can you elaborate on what you mean by "rewrite Test so it creates an expression rather than calculating the result directly".

这里的一个问题是我不太清楚您的最终目标是什么,因为您只是想在哪些方面保持灵活性。因此,我将做一些与 .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()。因为这正是它正在做的。我们在这里并没有真正做很多事情,尽管在更复杂的 Where() 调用中肯定有 DRY 的余地。

大致同样容易:

public static Expression<Func<Airplane, bool>> StatusFilter(string sought)
{
  return p => p.Status.Contains(sought);
}

在这里您可以使用 _dataContext.Airplanes.Where(StatusFilter("a")) 并且它与您使用工作 Where() 几乎相同。同样,我们在这里没有做太多,但如果过滤器更复杂,则有 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;
}

这与以前版本的 StatusFilter 在幕后所做的几乎完全相同,只是使用 .NET 元数据标记标识类型、方法和属性,我们使用 typeof() 和名称.

正如每行中的注释所示,第一行获得一个表示属性的表达式。我们真的不需要给它起个名字,因为我们不会在源代码中直接使用它,但无论如何我们都称它为 "p"

下一行为 Status 获取 PropertyInfo,随后创建一个表示为 p 获取的表达式,因此 p.Status.

下一行创建一个表达式,表示 sought 的常量值。虽然 sought 通常不是常量,但就我们正在创建的整体表达式而言(这就是为什么 EF 能够将 Test("abc", "a") 视为常量 true 而不是必须翻译一下)。

下一行获取 ContainsMethodInfo,在下一行中,我们创建一个表达式,表示在 p.Status 的结果上调用它,其中 sought 作为参数。

最后,我们创建一个表达式,将它们全部联系在一起,形成 p => p.Status.Contains(sought) 和 return 的等价物。

这显然比 p => p.Status.Contains(sought) 要多得多。好吧,这就是在 C# 中为表达式使用 lambda 的意义所在,因此我们通常不必做这项工作。

确实,要获得与您的 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"),因为 p.Status 不是那个上下文中的表达式。我们要做的:

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);
}

现在终于可以使用_dataContext.Airplanes.UseTest("a")了。呸!

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

  1. 如果我们想对表达式进行一些超出 want lambdas 允许方向的操作,我们可以使用它,例如在 中,它们与反射一起使用以能够指定 属性 作为字符串访问。
  2. 希望您对 linq 在幕后的工作方式有足够的了解,从而了解您需要了解的所有内容,以充分理解为什么您的问题中的某些查询有效而某些无效。

事实证明,在我查看的示例中,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 而不是迭代的目的。