Search code examples
c#linqlambdaexpression-trees

Call a method in an expression tree lambda


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.


Solution

  • 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));
    }