根据帖子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);
}
答案 0 :(得分:6)
这个答案由两部分组成:
IEnumerable<T>
和IQueryable<T>
上教育您,以及两者之间的差异新要求并不像其他要求那样容易实现。主要原因是LINQ查询按复合键分组,导致在编译时创建匿名类型:
source.GroupBy(x => new { x.MenuText, Name = x.Role.Name })
这会产生一个新类,其中包含编译器生成的名称以及两个属性MenuText
和Name
。
在运行时执行此操作是可能的,但实际上并不可行,因为它会将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个答案中展示的方法的扩展。
它的工作原理如下:
GetDeepPropertyExpression
获取相应的表达式。这基本上就是我在之前的回答中添加的代码。x => x.Role.Name
如果已经传递了多个属性,我们将这些属性相互连接,并在其间连接一个分隔符,并将其用作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
您在问题中展示的扩展方法是一个非常糟糕的主意
为什么?好吧,因为它适用于IEnumerable<T>
。这意味着该组by 不在数据库服务器上执行,但在应用程序的内存中本地执行。此外,后面的所有LINQ子句,如Where
也在内存中执行!
如果要提供扩展方法,则需要为IEnumerable<T>
(在内存中,即LINQ to Objects)和IQueryable<T>
(对于要在数据库上执行的查询,像LINQ to Entity Framework)
这与微软选择的方法相同。对于大多数LINQ扩展方法,存在两种变体:一种适用于IEnumerable<T>
,另一种适用于IQueryable<T>
,它们位于两个不同的类Enumerable
和Queryable
中。比较这些类中方法的第一个参数。
所以,你想要做的就是这样:
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));
}