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 Gr
This answer consists of two parts:
IEnumerable<T>
and IQueryable<T>
and the differences between the twoThe 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:
GetDeepPropertyExpression
. That is basically the code I added in my previous answer.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
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));
}