多列组按表达式树 [英] Multi Column Group By Expression Tree
问题描述
根据 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(选择器);
这个答案包含两部分:
- 为您的问题提供解决方案
- 在
> 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这是我在以前 两个答案。
它的工作原理如下:
- 为每个提供的深属性字符串获取相应的表达式通过
GetDeepPropertyExpression
。这基本上是我在我以前的答案中添加的代码。
- 如果只传递了一个属性,则将其直接用作lambda的主体。结果与我以前的答案中的表达式相同,例如,
x => x.Role.Name
-
如果传递了多个属性,我们将这些属性相互连接起来,拉姆达的身体。我选择了分号,但你可以使用任何你想要的。假设我们传递了三个属性(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:
- Providing a solution for your problem
- 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:
- For each supplied deep property string get the corresponding expression via
GetDeepPropertyExpression
. That is basically the code I added in my previous answer.
- 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
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屋!