Multi Column Group By Expression Tree

c#, group-by, linq

Solution

This answer consists of two parts:

- Providing a solution for your problem

- Educating you on `IEnumerable<T>` and `IQueryable<T>` and the differences between the two

Part 1: A solution for your immediate problem

The new requirement is not as easily fulfilled as the others were. The main reason for this is that a LINQ query, that groups by a composite key, results in an anonymous type to be created at compile time:

source.GroupBy(x => new { x.MenuText, Name = x.Role.Name })

This results in a new class with a compiler generated name and two properties `MenuText` and `Name`. Doing this at runtime would be possible, but is not really feasible, because it would involve emitting IL into a new dynamic assembly.

For my solution I chose a different approach: Because all the involved properties seem to be of type `string` the key we group by is simply a concatenation of the properties values separated by a semicolon. So, the expression our code generates is equivalent to the following:

source.GroupBy(x => x.MenuText + ";" + x.Role.Name)

The code to achieve this looks like this:

private static Expression<Func<T, string>> GetGroupKey<T>(
    params string[] properties)
{
    if(!properties.Any())
        throw new ArgumentException(
            "At least one property needs to be specified", "properties");

    var parameter = Expression.Parameter(typeof(T));
    var propertyExpressions = properties.Select(
        x => GetDeepPropertyExpression(parameter, x)).ToArray();

    Expression body = null;
    if(propertyExpressions.Length == 1)
        body = propertyExpressions[0];
    else
    {
        var concatMethod = typeof(string).GetMethod(
            "Concat",
            new[] { typeof(string), typeof(string), typeof(string) });

        var separator = Expression.Constant(";");
        body = propertyExpressions.Aggregate(
            (x , y) => Expression.Call(concatMethod, x, separator, y));
    }

    return Expression.Lambda<Func<T, string>>(body, parameter);
}

private static Expression GetDeepPropertyExpression(
    Expression initialInstance, string property)
{
    Expression result = null;
    foreach(var propertyName in property.Split('.'))
    {
        Expression instance = result;
        if(instance == null)
            instance = initialInstance;
        result = Expression.Property(instance, propertyName);
    }
    return result;
}

This again is an extension of the method I showed in my previous two answers.

It works as follows:

- For each supplied deep property string get the corresponding expression via `GetDeepPropertyExpression`. That is basically the code I added in my previous answer.

- If only one property has been passed, use it directly as the body of the lambda. The result is the same expression as in my previous answer, e.g. `x => x.Role.Name`

If multiple properties have been passed, we concatenate the properties with each other and with a separator in between and use that as the body of the lambda. I chose the semicolon, but you can use whatever you want. Assume we passed three properties (`"MenuText", "Role.Name", "ActionName"`), then the result would look something like this:

x => string.Concat(
        string.Concat(x.MenuText, ";", x.Role.Name), ";", x.ActionName)

This is the same expression the C# compiler generates for an expression that uses the plus sign to concatenate strings and thus is equivalent to this:

x => x.MenuText + ";" + x.Role.Name + ";" + x.ActionName

Part 2: Educating you

The extension method you showed in your question is a very bad idea. Why? Well, because it works on `IEnumerable<T>`. That means that this group by is not executed on the database server but locally in the memory of your application. Furthermore, all LINQ clauses that follow, like a `Where` are executed in memory, too!

If you want to provide extension methods, you need to do that for both `IEnumerable<T>` (in memory, i.e. LINQ to Objects) and `IQueryable<T>` (for queries that are to be executed on a database, like LINQ to Entity Framework). That is the same approach Microsoft has chosen. For most LINQ extension methods there exist two variants: One that works on `IEnumerable<T>` and one that works on `IQueryable<T>` which live in two different classes `Enumerable` and `Queryable`. Compare the first parameter of the methods in those classes.

So, you what you want to do is something like this:

public static IEnumerable<IGrouping<string, TElement>> GroupBy<TElement>(
    this IEnumerable<TElement> source, params string[] properties)
{
    return source.GroupBy(GetGroupKey<TElement>(properties).Compile());
}

public static IQueryable<IGrouping<string, TElement>> GroupBy<TElement>(
    this IQueryable<TElement> source, params string[] properties)
{
    return source.GroupBy(GetGroupKey<TElement>(properties));
}

Problem

As per the post LINQ Expression of the Reference Property I have implemented Group By Extension thanks to Daniel Hilgarth for the help , I need help to extend this for GroupByMany as below _unitOfWork.MenuSetRepository.Get().GroupBy("Role.Name","MenuText"); Extension Method ``` public static IEnumerable<IGrouping<string, TElement>> GroupBy<TElement>(this IEnumerable<TElement> elements,string property) { var parameter = Expression.Parameter(typeof(TElement), "groupCol"); Expression<Func<TElement, string>> lambda; if (property.Split('.').Count() > 1) { Expression body = null; foreach (var propertyName in property.Split('.')) { Expression instance = body; if (body == null) instance = parameter; body = Expression.Property(instance, propertyName); } lambda = Expression.Lambda<Func<TElement, string>>(body, parameter); } else { var menuProperty = Expression.PropertyOrField(parameter, property); lambda = Expression.Lambda<Func<TElement, string>>(menuProperty, parameter); } var selector= lambda.Compile(); return elements.GroupBy(selector); } ```

Original source

Related problems