1

In the code below I want to replace x.BaseSalary with any other property whose name is stored in feildtoretrive:

var feildtoretrive  = "BaseSalary"
var groupcal = db.Employees.GroupBy(x=>x.Region)
                           .Select(group => new { 
                                    Key = group.Key, 
                                    Avg = group.Average(x => x.BaseSalary) 
                                                })
                           .ToList();
user3815413
  • 385
  • 2
  • 20

1 Answers1

3

You need to

  • create a MemberExpression that accesses that member of an instance
  • compile a lambda expression that returns that member for a passed instance parameter

(I assume that the elements in Employees are of type Employee)

// the parameter expression for the lambda (your 'x')
var parameter = Expression.Parameter(typeof(Employee));
// the property access expression (your 'x.BaseSalary' or 'x.<feildtoretrive')
var propAccess = Expression.PropertyOrField(parameter, feildtoretrive);
// the build-together and compiled lambda
 var expression = (Expression<Func<Employee, int?>>)Expression.Lambda(propAccess, parameter);

You can now use lambda for the x.Average call:

new { Key = group.Key, Avg = group.Average(lambda) }

A caveat: This only works now for members of type int?. I'm lacking a little experience on how to do this more type independent, but what types can you calculate an average over? But if there are int or double members, another cast expression maybe necessary.


EDIT: (changed the return type to int?). According to Ivan Stoev's comment on my follow up question you could try this:

new { Key = group.Key, Avg = group.AsQueryable().Average(expression) }

EF6 should recognize the call to AsQueryable() and then use the correct Average method (note that I use the expression as argument instead of the lambda). EF Core and linq2Sql won't work with that.

user3815413
  • 385
  • 2
  • 20
René Vogt
  • 43,056
  • 14
  • 77
  • 99
  • 1
    You can address the caveat by using reflection to cast the `Delegate` returned by `Expression.Compile` to a `Func` of the correct `T` based on the type of the `failedtoretrieve` Property. Then you would reflectively invoke `IGrouping.Average` passing the the result of the reflective cast in the `parameters` object array. – Aaron M. Eshbach Jan 23 '18 at 13:57
  • It Throws runtime " Internal .NET Framework Data Provider error 102" – user3815413 Jan 23 '18 at 14:15
  • @user3815413 it seems the query provider has problems to translate this to sql. Just to be sure: did it work "before" (just using `x => x.BaseSalary`)? – René Vogt Jan 23 '18 at 14:26
  • @user3815413 I edited the answer for an easier/shorter way to create the property access expression. It _may_ solve your issue, but I think it may rather have something to do with the types of the properties. Is the property you tested with of type `decimal`? – René Vogt Jan 23 '18 at 14:45
  • Yes it worked without any issue iwth (just using x => x.BaseSalary) Properties are of type int – user3815413 Jan 24 '18 at 05:52
  • Properties are of type Int32? (nullable) – user3815413 Jan 24 '18 at 06:17
  • @user3815413 if they are `int?`, then try changing it to `var lambda = (Func)Expresssion.....` instead. – René Vogt Jan 24 '18 at 08:34
  • That i changed already but it throws Internal .NET Framework Data Provider error 102, Since cox this is Linq to EF? – user3815413 Jan 24 '18 at 08:36
  • I'm sorry, I don't get it...and I currently don't have time to reproduce this in detail. I didn't find anything on goolge for that error message (maybe because it's "internal")....my last idea would be that there is a cast expression missing and that causes the query translator to fail...I'll see if I find time today to check that.... – René Vogt Jan 24 '18 at 08:39
  • @user3815413 as a workaround you could insert a `AsEnumerable()` after the `GroupBy` and before the `Select`. But that would mean that all columns are transferred from the db to your memory and the average is calculated on your cpu instead of by the db..... I don't know why it doesn't work, in Linq2Sql it complains that the overload of `Average` was not supported, but (according to the compiler) it's the same overload as with `x => x.BaseSalary`... – René Vogt Jan 24 '18 at 09:12
  • @user3815413 now I'm really curious and asked a follow-up question [here](https://stackoverflow.com/questions/48419910/notsupportedexception-when-using-compiled-lambda-expression-for-average) myself. – René Vogt Jan 24 '18 at 10:16
  • AsEnumerable(), This is already done but since data is huge i dont want to bring anything to memory, That is the reason i wanted to go for expressions – user3815413 Jan 24 '18 at 11:05
  • @user3815413 I updated again with a version that _should_ work in EF6, but won't in linq2sql or EF Core, Don't know the exact framework you're using. – René Vogt Jan 24 '18 at 11:58
  • Now also i am getting the error code "Internal .NET Framework Data Provider error 1025". I am using EF6 – user3815413 Jan 24 '18 at 13:21
  • **1025**? You said "102" before! But for 1025, `AsQueryable` should work according to [this](https://stackoverflow.com/questions/24765269/casting-linq-expression-throws-internal-net-framework-data-provider-error-1025) and [this](https://stackoverflow.com/questions/11990158/internal-net-framework-data-provider-error-1025) and the other google results. Guess I'm out now, seems I can't find a working solution for you. – René Vogt Jan 24 '18 at 13:25
  • Hey Thanks a lot René Vogt, As per advice after remvoing compile and converting to expression worked for EF now, Thanks a lot @René Vogt, – user3815413 Jan 24 '18 at 13:31
  • 1
    @user3815413 phew... you're welcoome, thank god it works :) I finally think I understand why my first version didn't.... – René Vogt Jan 24 '18 at 13:33
  • @user3815413 so if we finally solved it, you may reaccept the answer if you like :) – René Vogt Jan 24 '18 at 13:41