Search code examples
c#.netlinqlinq-to-objectsexpression-trees

Expression.Call groupBy then Select?


I am trying to use expression trees to build a nested pair of groups, and getting totally stumped by Select and what it expects for parameters. What I ultimately want to do is build this via expression trees;

var queryNestedGroups = products.GroupBy(x => x.Category)
                .Select(p => new
                {
                    key = p.Key,
                    objects = p.ToList().GroupBy(y => y.Subcategory)
                        .Select(y => new { key = y.Key, objects = y.ToList() })
                })
                .AsQueryable();

This is my attempt so far (products is a List);

var data = Expression.Constant(products);
var arg = Expression.Parameter(typeof(Product), "arg");
var nameProperty = Expression.PropertyOrField(arg, "Category");

var groupByLambda = Expression.Lambda<Func<Product, string>>(nameProperty, arg);
var groupByExpression = Expression.Call(
    typeof(Queryable),
    "GroupBy",
    new Type[] { typeof(Product), typeof(string) },
    data,
    groupByLambda);

var parameterExp = Expression.Parameter(typeof(IGrouping<string, Product>), "p");
var keyProp = Expression.PropertyOrField(parameterExp, "Key");
ConstructorInfo constructorInfo = typeof(object)
    .GetConstructor(new[] { typeof(string), typeof(Product) });

Type anonymousResultType = new { Key = "abc", Values = new List<Product>() }.GetType();
var exp = Expression.New(
            anonymousResultType.GetConstructor(new[] { typeof(string), typeof(List<Product>) }),
            Expression.Constant("def"),
            Expression.Constant(new List<Product>()));
var selectLambda = Expression.Lambda(exp);

var selectExpression = Expression.Call(
    typeof(Queryable),
    "Select",
    new Type[] { typeof(List<Product>), selectLambda.Body.Type },
    data,
    selectLambda); 

var finalExpression = Expression.Lambda(groupByExpression);

All was going well, except I get exceptions on var selectExpression = ... telling me my type parameters and parameters are wrong. Unfortunately it doesn't tell me which parameters and why they're wrong. I've tried every permutation I can think of here.. So two questions;

How do I figure out what

  1. exactly it wants for types?
  2. What are the right types/parameters in this case?

Solution

  • Below is the code to do what you want. Each select needs to have it's own lamda projection in Expression Trees. You also have two different anonymous types one's an IEnumerable of the inner anonymous type and one is a list of products.

    Also since it's linq to objects you don't need the Queryable you can just use Enumerable and p.ToList().GroupBy(y => y.Subcategory) the ToList isn't needed so I didn't convert it.

    Also it would be simpler if you didn't use anonymous types and had concrete classes. Especially at the end. Since it can't be strongly typed you will just have to compile it and then DynamicInvoke it.

    // This could be a parameter
    var data = Expression.Constant(products);
    
    var outterGroupByarg = Expression.Parameter(typeof(Product), "x");
    var outterGroupNameProperty = Expression.PropertyOrField(outterGroupByarg, "Category");
    var outterGroupByLambda = Expression.Lambda<Func<Product, string>>(outterGroupNameProperty, outterGroupByarg);
    var outterGroupByExpression = Expression.Call(typeof(Enumerable), "GroupBy", new [] { typeof(Product), typeof(string) },
                                             data, outterGroupByLambda);
    
    var outterSelectParam = Expression.Parameter(typeof (IGrouping<string, Product>), "p");
    
    var innerGroupByarg = Expression.Parameter(typeof(Product), "y");
    var innerGroupNameProperty = Expression.PropertyOrField(innerGroupByarg, "Subcategory");
    var innerGroupByLambda = Expression.Lambda<Func<Product, string>>(innerGroupNameProperty, innerGroupByarg);
    
    var innerGroupByExpression = Expression.Call(typeof(Enumerable), "GroupBy", new[] { typeof(Product), typeof(string) },
                                             outterSelectParam, innerGroupByLambda);
    
    var innerAnonymousType = new {Key = "abc", objects = new List<Product>()};
    
    var innerSelectProjectionarg = Expression.Parameter(typeof(IGrouping<string, Product>), "y");
    var innerKeyProp = Expression.Property(innerSelectProjectionarg, "Key");
    
    var innerToList = Expression.Call(typeof (Enumerable), "ToList", new[] {typeof (Product)},
                                      innerSelectProjectionarg);
    
    var innerAnonymousResultType = innerAnonymousType.GetType();
    var innerAnonymousConstructor =
        innerAnonymousResultType.GetConstructor(new[] {typeof (string), typeof (List<Product>)});
    var innerAnonymous = Expression.New(innerAnonymousConstructor, innerKeyProp, innerToList);
    
    var innerSelectProjection =
        Expression.Lambda(typeof(Func<,>).MakeGenericType(typeof(IGrouping<string, Product>), innerAnonymousResultType), innerAnonymous,
                          innerSelectProjectionarg);
    
    var innerSelectExpression = Expression.Call(typeof(Enumerable), "Select", new [] { typeof(IGrouping<string, Product>), innerAnonymousResultType },
                                innerGroupByExpression, innerSelectProjection);
    
    
    var outterAnonymousType = new {Key = "abc", Values = new[] {innerAnonymousType}.AsEnumerable()};
    var outterAnonymousResultType = outterAnonymousType.GetType();
    var outterAnonymousConstructor =
        outterAnonymousResultType.GetConstructor(new[] { typeof(string), typeof(IEnumerable<>).MakeGenericType(innerAnonymousResultType) });
    
    var outterKeyProp = Expression.PropertyOrField(outterSelectParam, "Key");
    var outterAnonymous = Expression.New(outterAnonymousConstructor, outterKeyProp, innerSelectExpression);
    var outterSelectProjection =
        Expression.Lambda(
            typeof (Func<,>).MakeGenericType(typeof (IGrouping<string, Product>), outterAnonymousResultType),
            outterAnonymous,
            outterSelectParam);
    
    var outterSelect = Expression.Call(typeof(Enumerable), "Select", new[] { typeof(IGrouping<string, Product>), outterAnonymousResultType },
                                      outterGroupByExpression, outterSelectProjection);
    
    // Lamda is a func with no input because list of products was set as a constant and not a parameter
    var finial =
        Expression.Lambda(
            typeof (Func<>).MakeGenericType(typeof (IEnumerable<>).MakeGenericType(outterAnonymousResultType)),
            outterSelect);