多列组按表达式树 [英] Multi Column Group By Expression Tree

查看:108
本文介绍了多列组按表达式树的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

根据 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);
表达式< Func< TElement,string>>拉姆达;
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();
返回elements.GroupBy(选择器);


解决方案

这个答案包含两部分:


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



第1部分:针对您的直接问题的解决方案



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

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

这将产生一个带有编译器生成名称和两个属性 MenuText 名称的新类。

在运行时这样做是可能的,但并不真正可行,因为它会涉及将IL发射到新的动态程序集中。



对于我解决方案我选择了一种不同的方法:

因为所有涉及的属性都是类型 string ,我们分组的关键仅仅是属性的串联值由分号隔开。

因此,我们的代码生成的表达式等同于以下内容:

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

代码来实现这个看起来像这样:

  private static Expression< Func< T,string>> GetGroupKey< T>(
params string [] properties)
{
if(!properties.Any())
抛出新的ArgumentException(
至少有一个属性需要指定,属性);

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

私有静态表达式GetDeepPropertyExpression(
表达式initialInstance,字符串属性)
{
表达式结果= null;
foreach(var propertyName in property.Split('。'))
{
Expression instance = result;
if(instance == null)
instance = initialInstance;
result = Expression.Property(instance,propertyName);
}
返回结果;



$ b这是我在以前 两个答案。



它的工作原理如下:


  1. 为每个提供的深属性字符串获取相应的表达式通过 GetDeepPropertyExpression 。这基本上是我在我以前的答案中添加的代码。

  2. 如果只传递了一个属性,则将其直接用作lambda的主体。结果与我以前的答案中的表达式相同,例如, x => x.Role.Name

  3. 如果传递了多个属性,我们将这些属性相互连接起来,拉姆达的身体。我选择了分号,但你可以使用任何你想要的。假设我们传递了三个属性(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< T> 。这意味着这个分组不是在数据库服务器上执行,而是在应用程序的内存中本地执行。此外,所有后续的LINQ子句,如 Where 也会在内存中执行!



如果你想提供扩展方法,您需要为 IEnumerable< T> (在内存中,即LINQ to Objects)和 IQueryable< T> code>(对于要在数据库上执行的查询,如LINQ to Entity Framework)。
这与Microsoft选择的方法相同。对于大多数LINQ扩展方法,存在两种变体:一种适用于 IEnumerable< T> ,另一种适用于 IQueryable< T> 生活在两个不同的类 Enumerable 可查询 。比较这些类中方法的第一个参数。



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

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


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

解决方案

This answer consists of two parts:

  1. Providing a solution for your problem
  2. 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:

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

这篇关于多列组按表达式树的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆