1

I tried to answer this question but failed:

So let's take the original query:

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

Works fine. Now we want to dynamically decide what to average. I try to create the lambda for Average dynamically:

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

and use it:

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

With Linq2Sql this results in a NotSupportedException:

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

(I only have the German error message, it says that the used overload of Average is not supported, feel free to edit if you have the English version).

The original question used Linq2Entities and got the error

Internal .NET Framework Data Provider error 102

IntelliSense (or some other IDE feature) tells me that in both versions the compiler chooses the same overload of Average:

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

And I rechecked with an ExpressionVisitor that my lambda is exactly the same expression as x => x.BaseSalary.

So: Why it suddenly isn't supported anymore?


Interesting: there is no such exception if I don't group and use it simply like:

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

With YuvalShap's answer I also tried Avg = g.AsQueryable().Average(expression) (using an expression instead of the lambda), but with the same result.

René Vogt
  • 43,056
  • 14
  • 77
  • 99
  • Usually the `AsQueryable()` trick works in EF6. Are we talking about EF Core here? – Ivan Stoev Jan 24 '18 at 11:40
  • @IvanStoev now I'm getting into trouble: the original question (linked at the top) was about EF, but I only reproduced in linq2sql, because it was easier/faster to setup. So I'm actually interested why linq2sql fails, especially since in both versions the parameters to `Average` ( `x => x.BaseSalaray` and `lambda`) are as far as I can determine exactly the same things. But the original asker will still be interested in EF, but I don't know if it's EF Core or not (I'm actually not really used to ef at all). – René Vogt Jan 24 '18 at 11:46
  • 1
    It depends if the query translator recognizes and handles `AsQueryable` method call in the expression tree or not. EF6 does, EF Core currently does not, according to your comment LINQ to SQL does not as well. Hence the technique is unreliable. So some custom expression tricks similar to the current answer can be used, but in general one needs some expression composing library like LINQKit `AsExpandable` / `Invoke`. – Ivan Stoev Jan 24 '18 at 11:52
  • 1
    @IvanStoev though that may solve the issue (for EF6 at least), I actually asked because I don't understand why `x => x.BaseSalary` works and the (imho) _same_ `lambda` does not. But I now realized that the whole `Select` argument is an _expression tree_, so `x => x.BaseSalary` is **not yet compiled**, but analyzed by the provider. `lambda` instead is a local variable no longer parseable by the provider. Hence `AsQueryable` would work (if recognized by the provider). Strange though that the exception stays the same instead of complaining about `AsQueryable()`. – René Vogt Jan 24 '18 at 13:27

1 Answers1

4

You should not compile the lambda. EF works with expression trees not with the compiled code, so that it can transform the Expression to SQL rather then running it in code.

There is no compilation error because there is an Enumerable.Average which does take a Func<T, int?> so that overload is used. But when converting to SQL EF does not know what to do with the compiled lambda.

Since Average is on the grouping, you can't pass the expression to it, you have to build up the entire expression to Select.

Since that can create very obfuscated code, you can create a custom version of Select that replaces a portion of the expression with your custom expression for average, so at least the main part of the select is readable:

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

Usage:

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);
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • Thanks for the response. Since you're below 10k, you can't see the (deleted) answer I linked: I tried this, it only compiles with an `g.AsQueryable()` as `g` already is an `IEnumerable` and there is no overload of `Enumerable.Average` taking an expression, only lambdas. If I use `g.AsQueryable().Average(expression)` it results in the same exception. – René Vogt Jan 24 '18 at 11:19
  • Yup, you are right, hoped it could be simpler, updated the answer with a working version. Hoping to get to 10k soon :) – Titian Cernicova-Dragomir Jan 24 '18 at 11:39
  • 1
    Didn't see your changes (somehow SO doesn't notify OP about answer edits). Thanks a lot. I developed an understanding about _why_ my approach didn't work in the meantime...and your workaround is interesting, too. For the original question: it used EF6, so the `AsQueryable()` trick worked. – René Vogt Jan 24 '18 at 13:40