I am trying to emulate this lambda call with expression trees:
myList.AsQueryable().GroupBy(g=>g.Name).Select(s => s.FirstOrDefault());
So far i got myself here:
public Expression Distinct (IQueryable queryable, string propertyName)
{
var propInfo = queryable.ElementType.GetProperty(propertyName);
var collectionType = queryable.ElementType;
var groupParameterExpression = Expression.Parameter(collectionType, "g");
var propertyAccess = Expression.MakeMemberAccess(groupParameterExpression, propInfo);
var groupLambda = Expression.Lambda(propertyAccess, groupParameterExpression);
var groupExpression = Expression.Call(typeof(Queryable), "GroupBy", new Type[] { collectionType, propInfo.PropertyType }, queryable.Expression, groupLambda);
var selectParameterExpression = Expression.Parameter(groupExpression.Type, "s");
var selectFirstOrDefaultMethodExpression = Expression.Call(typeof(Enumerable), "FirstOrDefault", new Type[] { collectionType }, selectParameterExpression);
var selectLambda = Expression.Lambda(selectFirstOrDefaultMethodExpression, selectParameterExpression);
return Expression.Call(typeof(Queryable), "Select", new Type[] { groupExpression.Type, selectParameterExpression.Type }, groupExpression, selectLambda);
}
This part passess okay:
var groupParameterExpression = Expression.Parameter(collectionType, "g");
var propertyAccess = Expression.MakeMemberAccess(groupParameterExpression, propInfo);
var groupLambda = Expression.Lambda(propertyAccess, groupParameterExpression);
var groupExpression = Expression.Call(typeof(Queryable), "GroupBy", new Type[] { collectionType, propInfo.PropertyType }, queryable.Expression, groupLambda);
I get a valid expression in groupExpression
and the type
received in this case when calling a Name
property from a model is:
IGrouping<string, Product>
where a Product is a model that looks like this:
public class Product
{
public string Name { get; set; }
}
selectParameterExpression
is a parameter expression with a type of IGrouping<string,Product>
.
I get an exception when i try to call FirstOrDefault
here:
var selectFirstOrDefaultMethodExpression = Expression.Call(typeof(Enumerable), "FirstOrDefault", new Type[] { collectionType }, selectParameterExpression);
No generic method 'FirstOrDefault' on type 'System.Linq.Enumerable' is compatible with the supplied type arguments and arguments. No type arguments should be provided if the method is non-generic.
There are several issues with that code.
First, when using Expression.Call
to call Queryable
methods, you have to wrap the lambdas with Expression.Quote
in order to let them be treated as Expression<Func<...>>
rather than just Func<...>
.
Second, the Select
parameter type in your example should be IGrouping<TKey, TElement>
while you are passing groupExpression.Type
which is IQueryable<IGrouping<TElement, TKey>>
, i.e. you need to extract the IGrouping
part, for instance like this:
groupExpression.Type.GetGenericArguments().Single()
Finally, the Select
call generic arguments should be IGrouping<TKey, TElement>
, TElement
, which can be obtained for instance from selectParameterExpression.Type
, selectLambda.Body.Type
.
So the working method could be like this:
public Expression Distinct(IQueryable queryable, string propertyName)
{
var propInfo = queryable.ElementType.GetProperty(propertyName);
var collectionType = queryable.ElementType;
var groupParameterExpression = Expression.Parameter(collectionType, "g");
var propertyAccess = Expression.MakeMemberAccess(groupParameterExpression, propInfo);
var groupLambda = Expression.Lambda(propertyAccess, groupParameterExpression);
var groupExpression = Expression.Call(typeof(Queryable), "GroupBy", new Type[] { collectionType, propInfo.PropertyType }, queryable.Expression, Expression.Quote(groupLambda));
var selectParameterExpression = Expression.Parameter(groupExpression.Type.GetGenericArguments().Single(), "s");
var selectFirstOrDefaultMethodExpression = Expression.Call(typeof(Enumerable), "FirstOrDefault", new Type[] { collectionType }, selectParameterExpression);
var selectLambda = Expression.Lambda(selectFirstOrDefaultMethodExpression, selectParameterExpression);
return Expression.Call(typeof(Queryable), "Select", new Type[] { selectParameterExpression.Type, selectLambda.Body.Type }, groupExpression, Expression.Quote(selectLambda));
}