【问题标题】:Multi Column Group By Expression Tree按表达式树的多列分组
【发布时间】:2013-07-17 15:49:39
【问题描述】:

根据帖子LINQ Expression of the Reference Property 感谢 Daniel Hilgarth 的帮助,我已经实现了 Group By Extension,我需要帮助来为 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);
    }

【问题讨论】:

    标签: c# linq group-by


    【解决方案1】:

    这个答案由两部分组成:

    1. 为您的问题提供解决方案
    2. 向您介绍 IEnumerable&lt;T&gt;IQueryable&lt;T&gt; 以及两者之间的区别

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

    这又是我在previoustwo 答案中展示的方法的扩展。

    它的工作原理如下:

    1. 对于每个提供的深层属性字符串,通过GetDeepPropertyExpression 获取相应的表达式。这基本上是我在上一个答案中添加的代码。
    2. 如果只传递了一个属性,则直接将其用作 lambda 的主体。结果与我之前的答案中的表达式相同,例如x =&gt; 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
      

    第 2 部分:教育你

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

    如果您想提供扩展方法,您需要为IEnumerable&lt;T&gt;(在内存中,即 LINQ to Objects)和IQueryable&lt;T&gt;(对于要在数据库上执行的查询,例如 LINQ to Entity)执行此操作框架)。
    这与 Microsoft 选择的方法相同。对于大多数 LINQ 扩展方法,存在两种变体:一种适用于 IEnumerable&lt;T&gt;,另一种适用于 IQueryable&lt;T&gt;,它们位于两个不同的类 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));
    }
    

    【讨论】:

    • 我更喜欢使用 Tuple.Create 而不是 String.Concat,但 +1 仍然是一个非常好的、完整的答案。
    • @p.s.w.g:我也想过这个问题,但我不确定 所有 LINQ 提供商是否支持它。
    • 可能不是,但它仍然比字符串连接更可靠。您还可以生成像Tuple 这样的虚拟'anonymous' 类型,但是这些暴露的属性设置器因此Expression.Bind 可以工作,而不必像真正的匿名类型那样动态生成程序集。当然,您可以根据最多属性进行分组。
    • @p.s.w.g:您将如何生成虚拟匿名类型以使其受到 LINQ 提供程序的支持?字符串连接是否不太可靠完全取决于数据。
    • 是的,我会尽量简化它们,但提供者仍需要支持通用 DTO。
    猜你喜欢
    • 2023-04-08
    • 1970-01-01
    • 2019-10-29
    • 2014-09-10
    • 1970-01-01
    • 2016-08-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多