Multi Column Group By Expression Tree

前端 未结 1 372
春和景丽
春和景丽 2021-01-03 12:30

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 Gr

1条回答
  •  栀梦
    栀梦 (楼主)
    2021-01-03 13:01

    This answer consists of two parts:

    1. Providing a solution for your problem
    2. Educating you on IEnumerable and IQueryable 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> GetGroupKey(
        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>(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:

    1. For each supplied deep property string get the corresponding expression via GetDeepPropertyExpression. That is basically the code I added in my previous answer.
    2. 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
    3. 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. 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 (in memory, i.e. LINQ to Objects) and IQueryable (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 and one that works on IQueryable 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> GroupBy(
        this IEnumerable source, params string[] properties)
    {
        return source.GroupBy(GetGroupKey(properties).Compile());
    }
    
    public static IQueryable> GroupBy(
        this IQueryable source, params string[] properties)
    {
        return source.GroupBy(GetGroupKey(properties));
    }
    

    0 讨论(0)
提交回复
热议问题