按表达式树的多列组

ine*_*e p 2 c# linq group-by

根据参考资产的LINQ表达式, 我已经实施了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);
    }
Run Code Online (Sandbox Code Playgroud)

Dan*_*rth 6

这个答案由两部分组成:

  1. 为您的问题提供解决方案
  2. 教育你IEnumerable<T>IQueryable<T>两者之间的差异

第1部分:解决您当前问题的解决方案

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

source.GroupBy(x => new { x.MenuText, Name = x.Role.Name })
Run Code Online (Sandbox Code Playgroud)

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

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

source.GroupBy(x => x.MenuText + ";" + x.Role.Name)
Run Code Online (Sandbox Code Playgroud)

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

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;
}
Run Code Online (Sandbox Code Playgroud)

这再次是我在前 两个答案中展示的方法的扩展.

它的工作原理如下:

  1. 对于每个提供的深属性字符串,获取相应的表达式via 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)
    
    Run Code Online (Sandbox Code Playgroud)

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

    x => x.MenuText + ";" + x.Role.Name + ";" + x.ActionName
    
    Run Code Online (Sandbox Code Playgroud)

第2部分:教育你

您在问题中显示的扩展方法是一个非常糟糕的主意.
为什么?好吧,因为它的作用IEnumerable<T>.这意味着此group by 不在数据库服务器上执行,而是在应用程序的内存中本地执行.此外,所有后面的LINQ子句,如a 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));
}
Run Code Online (Sandbox Code Playgroud)