NotSupportedException 对 Average 使用已编译的 lambda 表达式时

NotSupportedException when using compiled lambda expression for Average

我试图回答 但失败了:

所以让我们采用原始查询:

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,报错

Internal .NET Framework Data Provider error 102

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

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

然后我用 ExpressionVisitor 重新检查了我的 lambdax => x.BaseSalary.

完全相同的 表达式

所以:为什么它突然不再受支持了?


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

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

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

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

没有编译错误,因为 Enumerable.Average 确实需要 Func<T, int?>,因此使用了重载。但是当转换为 SQL 时,EF 不知道如何处理已编译的 lambda。

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

由于这会创建非常混乱的代码,您可以创建一个自定义版本的 Select,用您的自定义表达式替换表达式的一部分以获得平均值,因此至少 [=27] 的主要部分=] 可读:

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