Using Expression trees, I would need to build a GroupBy in a generic way.
The static method I'm going to use is the following:
public static IQueryable<Result> GroupBySelector<TSource>(this IQueryable<TSource> source, String coloumn)
{
//Code here
}
The Result class has two property :
public string Value { get; set; }
public int Count { get; set; }
Basically I'd like to build the following Linq query via Expression trees:
query.GroupBy(s => s.Country).Select(p => new
{
Value = p.Key,
Count = p.Count()
}
)
How would you implement it?
Looking at:
query.GroupBy(s => s.Country).Select(p => new
{
Value = p.Key,
Count = p.Count()
}
);
To match the signature of IQueryable<Result>
what you actually need here is:
query.GroupBy(s => s.Country).Select(p => new
Result{
Value = p.Key,
Count = p.Count()
}
);
Now, the Select
can work with any IQueryable<IGrouping<string, TSource>>
as is. It's only the GroupBy
that needs us to use expression trees.
Our task here is to start with a type and a string that represents a property (that itself returns string) and create a Expression<Func<TSource, string>>
that represents obtaining the value of that property.
So, let's produce the simple bit of the method first:
public static IQueryable<Result> GroupBySelector<TSource>(this IQueryable<TSource> source, string column)
{
Expression<Func<TSource, string>> keySelector = //Build tree here.
return source.GroupBy(keySelector).Select(p => new Result{Value = p.Key, Count = p.Count()});
}
Okay. How to build the tree.
We're going to need a lambda that has a paramter of type TSource
:
var param = Expression.Parameter(typeof(TSource));
We're going to need to obtain the property whose name matches column
:
Expression.Property(param, column);
And the only logic needed in the lambda is simply to access that property:
Expression<Func<TSource, string>> keySelector = Expression.Lambda<Func<TSource, string>>
(
Expression.Property(param, column),
param
);
Putting it all together:
public static IQueryable<Result> GroupBySelector<TSource>(this IQueryable<TSource> source, String column)
{
var param = Expression.Parameter(typeof(TSource));
Expression<Func<TSource, string>> keySelector = Expression.Lambda<Func<TSource, string>>
(
Expression.Property(param, column),
param
);
return source.GroupBy(keySelector).Select(p => new Result{Value = p.Key, Count = p.Count()});
}
About the only thing left is the exception-handling, which I normally don't include in an answer, but one part of this is worth paying attention to.
First the obvious null and empty checks:
public static IQueryable<Result> GroupBySelector<TSource>(this IQueryable<TSource> source, String column)
{
if (source == null) throw new ArgumentNullException("source");
if (column == null) throw new ArgumentNullException("column");
if (column.Length == 0) throw new ArgumentException("column");
var param = Expression.Parameter(typeof(TSource));
Expression<Func<TSource, string>> keySelector = Expression.Lambda<Func<TSource, string>>
(
Expression.Property(param, column),
param
);
return source.GroupBy(keySelector).Select(p => new Result{Value = p.Key, Count = p.Count()});
}
Now, let's consider what happens if we pass a string for column
that doesn't match a property of TSource
. We get an ArgumentException
with the message Instance property '[Whatever you asked for]' is not defined for type '[Whatever the type is]'.
That's pretty much what we want in this case, so no issue.
If however we passed a string that did identify a property but where that property wasn't of type string
we'd get something like "Expression of type 'System.Int32' cannot be used for return type 'System.String'"
. That's not dreadful, but it's not great either. Let's be more explicit:
public static IQueryable<Result> GroupBySelector<TSource>(this IQueryable<TSource> source, String column)
{
if (source == null) throw new ArgumentNullException("source");
if (column == null) throw new ArgumentNullException("column");
if (column.Length == 0) throw new ArgumentException("column");
var param = Expression.Parameter(typeof(TSource));
var prop = Expression.Property(param, column);
if (prop.Type != typeof(string)) throw new ArgumentException("'" + column + "' identifies a property of type '" + prop.Type + "', not a string property.", "column");
Expression<Func<TSource, string>> keySelector = Expression.Lambda<Func<TSource, string>>
(
prop,
param
);
return source.GroupBy(keySelector).Select(p => new Result{Value = p.Key, Count = p.Count()});
}
If this method was internal the above would perhaps be over-kill, but if it was public the extra info would be well worth it if you came to debug it.