按表达式树的多列组

时间:2013-07-16 15:31:13

标签: c# linq group-by

根据帖子LINQ Expression of the Reference Property 我已经实施了Group By Extension,感谢Daniel Hilgarth的帮助,我需要帮助来扩展这个GroupByMany,如下所示

_unitOfWork.MenuSetRepository.Get()的GroupBy( “Role.Name”, “MenuText”);

扩展方法

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);
    }

1 个答案:

答案 0 :(得分:6)

这个答案由两部分组成:

  1. 为您的问题提供解决方案
  2. IEnumerable<T>IQueryable<T>上教育您,以及两者之间的差异
  3. 第1部分:解决您当前问题的解决方案

    新要求并不像其他要求那样容易实现。主要原因是LINQ查询按复合键分组,导致在编译时创建匿名类型:

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

    这会产生一个新类,其中包含编译器生成的名称以及两个属性MenuTextName
    在运行时执行此操作是可能的,但实际上并不可行,因为它会将IL发送到新的动态程序集中。

    对于我的解决方案,我选择了另一种方法:
    因为所有涉及的属性似乎都是string类型,所以我们分组的键只是由分号分隔的属性值的串联。
    因此,我们的代码生成的表达式等同于以下内容:

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

    实现此目的的代码如下所示:

    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;
    }
    

    这再次是我在previous two个答案中展示的方法的扩展。

    它的工作原理如下:

    1. 对于每个提供的深属性字符串,通过GetDeepPropertyExpression获取相应的表达式。这基本上就是我在之前的回答中添加的代码。
    2. 如果只传递了一个属性,则直接将其用作lambda的主体。结果与我之前的答案中的表达式相同,例如x => x.Role.Name
    3. 如果已经传递了多个属性,我们将这些属性相互连接,并在其间连接一个分隔符,并将其用作lambda的主体。我选择了分号,但你可以使用你想要的任何东西。假设我们传递了三个属性("MenuText", "Role.Name", "ActionName"),那么结果将如下所示:

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

      这与C#编译器为使用加号连接字符串的表达式生成的表达式相同,因此等效于:

      x => x.MenuText + ";" + x.Role.Name + ";" + x.ActionName
      
    4. 第2部分:教育你

      您在问题中展示的扩展方法是一个非常糟糕的主意 为什么?好吧,因为它适用于IEnumerable<T>。这意味着该组by 在数据库服务器上执行,但在应用程序的内存中本地执行。此外,后面的所有LINQ子句,如Where也在内存中执行!

      如果要提供扩展方法,则需要为IEnumerable<T>(在内存中,即LINQ to Objects)和IQueryable<T>(对于要在数据库上执行的查询,像LINQ to Entity Framework) 这与微软选择的方法相同。对于大多数LINQ扩展方法,存在两种变体:一种适用于IEnumerable<T>,另一种适用于IQueryable<T>,它们位于两个不同的类EnumerableQueryable中。比较这些类中方法的第一个参数。

      所以,你想要做的就是这样:

      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));
      }