【问题标题】:NotSupportedException when using compiled lambda expression for AverageNotSupportedException 使用已编译的 lambda 表达式进行平均时
【发布时间】:2018-01-24 19:38:38
【问题描述】:

我试图回答this question但失败了:

那么让我们以原始查询为例:

var result = db.Employees.GroupBy(x => x.Region)
               .Select(g => new { Region = g.Key, 
                                  Avg = g.Average(x => x.BaseSalary)});

工作正常。现在我们要动态地决定要平均什么。我尝试动态创建 Average 的 lambda:

string property = "BaseSalary";
var parameter = Expression.Parameter(typeof(Employee));
var propAccess = Expression.PropertyOrField(parameter, property);
var expression = (Expression<Func<Employee,int?>>)Expression.Lambda(propAccess, parameter);
var lambda = expression.Compile();

并使用它:

var result = db.Employees.GroupBy(x => x.Region)
               .Select(g => new { Region = g.Key, 
                                  Avg = g.Average(lambda)});

使用 Linq2Sql,这会导致 NotSupportedException:

Für den Abfrageoperator "Average" wurde eine nicht unterstützte Überladung verwendet。

(我只有德语错误信息,上面写着不支持Average 使用的重载,如果您有英文版,请随时编辑)。

原题使用了Linq2Entities,报错

内部 .NET Framework 数据提供程序错误 102

IntelliSense(或其他一些 IDE 功能)告诉我,在两个版本中,编译器都选择了Average相同的重载

double? Enumerable.Average(this IEnumerable<Employee> source, Func<Employee, int?> selector);

我用ExpressionVisitor 重新检查了我的lambdax =&gt; x.BaseSalary 的表达式完全相同

那么:为什么突然不再支持它了?


有趣:如果我不分组并简单地使用它,则没有这样的例外:

double? result = db.Employees.Average(lambda);

对于YuvalShap's answer,我还尝试了Avg = g.AsQueryable().Average(expression)(使用表达式而不是lambda),但结果相同。

【问题讨论】:

  • 通常AsQueryable() 技巧在 EF6 中有效。我们在这里谈论的是 EF Core 吗?
  • @IvanStoev 现在我遇到了麻烦:最初的问题(链接在顶部)是关于 EF 的,但我只在 linq2sql 中复制,因为它更容易/更快地设置。所以我真的很感兴趣为什么 linq2sql 会失败,特别是因为在这两个版本中,Averagex =&gt; x.BaseSalaraylambda)的参数是我可以确定的完全相同的东西。但是原来的提问者还是会对 EF 感兴趣,只是不知道是不是 EF Core(其实我完全不习惯 ef)。
  • 这取决于查询翻译器是否识别并处理表达式树中的AsQueryable 方法调用。 EF6 有,EF Core 目前没有,根据您的评论 LINQ to SQL 也没有。因此,该技术是不可靠的。因此可以使用一些类似于当前答案的自定义表达式技巧,但通常需要一些表达式组合库,如 LINQKit AsExpandable / Invoke
  • @IvanStoev 虽然这可能会解决问题(至少对于 EF6),但我实际上问是因为我不明白为什么 x =&gt; x.BaseSalary 有效并且(恕我直言)same lambda 没有。但我现在意识到整个Select 参数是一个表达式树,所以x =&gt; x.BaseSalary还没有编译,而是由提供者分析的。 lambda 是提供程序不再可解析的局部变量。因此AsQueryable 将起作用(如果提供者认可)。奇怪的是异常保持不变而不是抱怨AsQueryable()

标签: c# linq-to-sql linq-to-entities expression-trees


【解决方案1】:

您不应该编译 lambda。 EF 使用表达式树而不是编译代码,因此它可以将表达式转换为 SQL,而不是在代码中运行它。

没有编译错误,因为有一个Enumerable.Average,它确实采用了Func&lt;T, int?&gt;,因此使用了重载。但是在转换为 SQL 时,EF 不知道如何处理已编译的 lambda。

由于Average在分组中,你不能将表达式传递给它,你必须将整个表达式构建到Select

由于可以创建非常模糊的代码,您可以创建一个自定义版本的Select,将表达式的一部分替换为您的自定义表达式以获得平均值,因此至少选择的主要部分是可读的:

public static class Helper
{
    public static IQueryable<TResult> SelectWithReplace<T, TKey, TResult>(this  IQueryable<IGrouping<TKey, T>> queryable, Expression<Func<IGrouping<TKey, T>, Func<T, int?>, TResult>> select, Expression<Func<T, int?>> replaceWith)
    {
        var paramToReplace = select.Parameters[1];
        var newBody = new ReplaceVisitor(paramToReplace, replaceWith).Visit(select.Body);

        var newSelect = Expression.Lambda<Func<IGrouping<TKey, T>, TResult>>(newBody, new[] { select.Parameters.First() });
        return queryable.Select(newSelect);
    }

    public class ReplaceVisitor : ExpressionVisitor
    {
        private readonly ParameterExpression toReplace;
        private readonly Expression replaceWith;

        public ReplaceVisitor(ParameterExpression toReplace, Expression replaceWith)
        {
            this.toReplace = toReplace;
            this.replaceWith = replaceWith;
        }
        protected override Expression VisitParameter(ParameterExpression node)
        {
            if(node == toReplace)
            {
                return this.replaceWith;
            }
            return base.VisitParameter(node);
        }
    }
}

用法:

string property = "BaseSalary";
var parameter = Expression.Parameter(typeof(Employee));
var propAccess = Expression.PropertyOrField(parameter, property);
var expression = (Expression<Func<Employee, int?>>)Expression.Lambda(propAccess, parameter);
var result = db.Employees
    .GroupBy(x => x.Region)
    .SelectWithReplace((g, willReplace) => new
    {
        Region = g.Key,
        Avg = g.Average(willReplace)
    }, expression);

【讨论】:

  • 感谢您的回复。由于您低于 10k,因此您看不到我链接的(已删除)答案:我试过了,它只编译 g.AsQueryable(),因为 g 已经是 IEnumerable&lt;Employee&gt; 并且没有 @987654331 的重载@ 取一个表达式,只有 lambdas。如果我使用 g.AsQueryable().Average(expression) 它会导致相同的异常。
  • 是的,你是对的,希望它可以更简单,用工作版本更新答案。希望尽快达到 10k :)
  • 没有看到您的更改(不知何故,SO 不会通知 OP 关于答案编辑)。非常感谢。我对为什么我的方法在此期间不起作用……而您的解决方法也很有趣。对于最初的问题:它使用了 EF6,所以 AsQueryable() 技巧奏效了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-10-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-30
  • 2017-01-06
相关资源
最近更新 更多