6

To dynamically generate a GroupBy expression, I am trying to build a Linq expression tree. The fields to group by are dynamic and can differ in number.

I use this code:

string[] fields = {"Name", "Test_Result"};
Type studentType = typeof(Student);

var itemParam = Expression.Parameter(studentType, "x");

var addMethod = typeof(Dictionary<string, object>).GetMethod(
    "Add", new[] { typeof(string), typeof(object) });
var selector = Expression.ListInit(
        Expression.New(typeof(Dictionary<string,object>)),
        fields.Select(field => Expression.ElementInit(addMethod,
            Expression.Constant(field),
            Expression.Convert(
                Expression.PropertyOrField(itemParam, field),
                typeof(object)
            )
        )));
var lambda = Expression.Lambda<Func<Student, Dictionary<string,object>>>(
    selector, itemParam);

The code is copied from this post (Thanks Mark Gravel!).

It finalizes with ...

var currentItemFields = students.Select(lambda.Compile());

... of which I expected that I could change it to ...

var currentItemFields = students.GroupBy(lambda.Compile());

I assumed that the lambda expression is nothing more than ...

var currentItemFields = students.GroupBy(o => new { o.Name, o.Test_Result });

... but unfortunally that seems not to be the case. The GroupBy with a dynamic lambda does not give any exceptions, it just doesn't group anything and returns all elements.

What am I doing wrong here? Any help would be appreciated. Thanks in advance.

Community
  • 1
  • 1
Jorr.it
  • 1,222
  • 1
  • 14
  • 25
  • When you print out the generated expression, before compiling it, what does it look like? – Servy Jan 15 '14 at 16:56
  • @Servy Like this: {x => new Dictionary`2() {Void Add(System.String, System.Object)("Shift", Convert(x.Shift)), Void Add(System.String, System.Object)("Section", Convert(x.Section))}} – Jorr.it Jan 15 '14 at 19:12
  • So shouldn't it be obvious from what you see there that the end result is a dictionary, and you know that grouping on a dictionary will do comparisons based on the dictionary's reference, not its contents, making the results clear. – Servy Jan 15 '14 at 19:13
  • If you compile those lambda's, you are doomed to rely on client side evaluation. – Jeremy Lakeman Dec 05 '19 at 05:51

2 Answers2

7

That lambda expression builds a dictionary of grouping fields.
Dictionary<TKey, TValue> does not implement Equals() and GetHashCode(), so it groups them by reference equality.
Since you always return a new dictionary, each item gets its own group.

You need to change it to create a type that correctly implements Equals() and GetHashCode() for value equality.
Ordinarily, you would have the compiler generate an anonymous type. However, you can't do that here since you don't know the type signature at compile-time.
Instead, you can construct a Tuple<...>:

Expression.New(
    Type.GetType("System.Tuple`" + fields.Length)
        .MakeGenericType(fields.Select(studentType.GetProperty), 
    fields.Select(f => Expression.PropertyOrField(itemParam, f))
)
SLaks
  • 868,454
  • 176
  • 1,908
  • 1,964
  • 1
    Thanks for your useful answer, it helped me to find a solution, as I posted in a separate answer. Maybe you like to know that the sample code you wrote misses a parenthesis and gives a conversion error. – Jorr.it Jan 17 '14 at 05:19
7

This post shows a expression function which can be used for both Select and GroupBy. Hope it helps others!

public Expression<Func<TItem, object>> GroupByExpression<TItem>(string[] propertyNames)
{
    var properties = propertyNames.Select(name => typeof(TItem).GetProperty(name)).ToArray();
    var propertyTypes = properties.Select(p => p.PropertyType).ToArray();
    var tupleTypeDefinition = typeof(Tuple).Assembly.GetType("System.Tuple`" + properties.Length);
    var tupleType = tupleTypeDefinition.MakeGenericType(propertyTypes);
    var constructor = tupleType.GetConstructor(propertyTypes);
    var param = Expression.Parameter(typeof(TItem), "item");
    var body = Expression.New(constructor, properties.Select(p => Expression.Property(param, p)));
    var expr = Expression.Lambda<Func<TItem, object>>(body, param);
    return expr;
}  

To be called like this:

var lambda = GroupByExpression<Student>(fields);
var currentItemFields = students.GroupBy(lambda.Compile());
Community
  • 1
  • 1
Jorr.it
  • 1,222
  • 1
  • 14
  • 25
  • How can I identify the keys for further selection after group by? Thanks. – Jitendra Gupta Mar 17 '15 at 15:54
  • I haven't tried this, but why should the fluent way not work? students.GroupBy(lambda1.Compile()).Select(lambda2.Compile()); The expression function can work for both GroupBy and [Select](http://stackoverflow.com/questions/9000753/how-can-i-create-a-dynamic-multi-property-select-on-an-ienumerablet-at-runtime/9001249#9001249). – Jorr.it Mar 21 '15 at 10:45
  • 1
    This is a nice solution that I've used. I think the drawback is that it fails on line 5 (tupleTypeDefinition) if you have more than 8 propertyNames, although that was not needed in my case. I think for more than 8 elements in a tuple, the last element is a new tuple definition of the remaining elements. I assume nested like that to inf. so one could probably amend the code slightly to support this definition. https://learn.microsoft.com/en-us/dotnet/api/system.tuple-8?view=netcore-3.0 – RoleyBaxter Apr 15 '20 at 09:21
  • Cannot be translated by entity framework... – ed22 Dec 14 '21 at 14:04