Search code examples
c#lambdaiqueryablelinq-expressions

Convert Expressions C# with existing Queryable methods


I need to append methods to an existion experssion and combine them into a new resultExpression.

Expression<TSource, IQueryable<TResult>> sourceExpression;
Expression<TSource, int, int, IQueryable<TResult>> resultExpression;

I need to append Queryable.Skip() and Queryable.Take() methods to sourceExpression and convert altogether to a resultExpression. How can I do it using c# Expression methods?

I tried to use Expression.Lambda<Func<TSource, int, int, IQueryable>> with Expression.Call, but it throws InvalidOperationException when I pass Queryable methods to Expression.Call parameters

var skipCall = Expression.Call(
                typeof(Queryable),
                nameof(Queryable.Skip),
                new[] {typeof(TResult)},
                sourceExpression.Body,
                Expression.Parameter(typeof(int))
            );
 var takeCall = Expression.Call(
                typeof(Queryable),
                nameof(Queryable.Take),
                new[] {typeof(TResult)},
                skipCall,
                Expression.Parameter(typeof(int))
            );
 var resultExpression = Expression.Lambda<Func<TSource, int, int, IQueryable<TResult>>>(
                takeCall, sourceExpression.Parameters
            );

No generic method 'Take' on type 'System.Linq.Queryable' is compatible with the supplied type arguments and arguments. No type arguments should be provided if the method is non-generic

...

So I'm able to build final resultExpression using Expression.Call with Func needed from Queryable

var skipParameter = Expression.Parameter(typeof(int), "skip");
var skipFunc = new Func<IQueryable<TResult>, int, IQueryable<TResult>>(Queryable.Skip).Method;
var takeParameter = Expression.Parameter(typeof(int), "take");
var takeFunc = new Func<IQueryable<TResult>, int, IQueryable<TResult>>(Queryable.Take).Method;
var initialCall = Expression.Invoke(sourceExpression,sourceExpression.Parameters[0]);
var skipCall = Expression.Call(
                skipFunc,
                initialCall,
                skipParameter);
var takeCall = Expression.Call(
                takeFunc,
                skipCall, 
                takeParameter);
resultExpression = Expression.Lambda<Func<TSource, int, int, IQueryable<TResult>>>(
                takeCall,
                sourceExpression.Parameters[0],
                skipParameter,
                takeParameter);

however I get Invoke() method in resulted expression string which can't be translated further. (source, skip, take) => Invoke(source => // sourceExpression...), source).Skip(skip).Take(take) How can I get rid of wrapping Invoke()?


Solution

  • So it turns out that I need to build lambda from original expression with Body as a parameter

    // Skip() function from Queryable with 'skip' parameter
    var skipParameter = Expression.Parameter(typeof(int), "skip");
    var skipFunc = new Func<IQueryable<TResult>, int, IQueryable<TResult>>(Queryable.Skip).Method;
    
    // Take() function from Queryable with 'take' parameter
    var takeParameter = Expression.Parameter(typeof(int), "take");
    var takeFunc = new Func<IQueryable<TResult>, int, IQueryable<TResult>>(Queryable.Take).Method;
    
    // Create lambda expresion from source
    var initialCall = Expression.Lambda<Func<TSource, IQueryable<TResult>>>(
                        sourceExpression.Body,
                        sourceExpression.Parameter[0]);
    
    // Append Skip function
    var skipCall = Expression.Call(
                        skipFunc,
                        initialCall.Body,
                        skipParameter);
    
    // Append Take function
    var takeCall = Expression.Call(
                        takeFunc,
                        skipCall,
                        takeParameter);
    
    // Wrap calls in the final expression
    resultExpression = Expression.Lambda<Func<TSource, int, int, IQueryable<TResult>>>(
                        takeCall,
                        contextParameter,
                        skipParameter,
                        takeParameter);
    

    That finally gives me the result I need.