使用已编译的lambda表达式进行平均

时间:2018-01-24 10:16:11

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

我试图回答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ürdenAbfrageoperator“Average”wurde einenichtunterstützteÜberladungverwendet。

(我只有德语错误消息,它表示 Average的使用过载不受支持,如果你有英文版,请随时编辑。)

原始问题使用Linq2Entities并收到错误

  

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

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

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

我使用ExpressionVisitor重新检查我的lambda x => x.BaseSalary完全相同的表达式。

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

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

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

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

1 个答案:

答案 0 :(得分:4)

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

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

由于Average在分组上,您无法将表达式传递给它,您必须将整个表达式构建为Select

由于这可以创建非常混淆的代码,您可以创建Select的自定义版本,用自定义表达式替换表达式的一部分以进行平均,因此至少select的主要部分是可读的:< / p>

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