Trouble combining Linq Expressions into a Func

1.1k views Asked by At

I've been trying to factor some common lambda subexpressions out into reusable components and have hit a wall. I'll show what I've done so far with a simplified example and hope one of you can shed some light.

My subexpressions are ultimately used in an NHibernate Query (IQueryable interface). Here's an example:

var depts = session.Query<Department>().Where(d => d.employees.Any(ex1.AndAlso(ex2).Compile()));

AndAlso is an Expression extension that's defined like this (taken from an answer to a related SO question, with some minor adjustments):

public class ParameterRebinder : ExpressionVisitor
{
    private readonly Dictionary<ParameterExpression, ParameterExpression> _map;

    public ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
    {
        _map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
    }
    public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
    {
        return new ParameterRebinder(map).Visit(exp);
    }
    protected override Expression VisitParameter(ParameterExpression p)
    {
        ParameterExpression replacement;
        if (_map.TryGetValue(p, out replacement))
        {
            p = replacement;
        }
        return base.VisitParameter(p);
    }
}

public static class ExpressionExtensions
{
    public static Expression<T> Compose<T>(this Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge)
    {
        // build parameter map (from parameters of second to parameters of first)
        var map = first.Parameters.Select((f, i) => new { f, s = second.Parameters[i] }).ToDictionary(p => p.s, p => p.f);
        // replace parameters in the second lambda expression with parameters from the first
        var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body);
        // apply composition of lambda expression bodies to parameters from the first expression 
        return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters);
    }
    public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
    {
        return first.Compose(second, Expression.AndAlso);
    }
    public static Expression<Func<T, bool>> OrElse<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
    {
        return first.Compose(second, Expression.OrElse);
    }
}

All would be well except that the Any call is an IEnumerable call, not an IQueryable call, so it expects a Func argument rather than an Expression. To that end I call Compile() on the combined Expression, but then I get the following run-time error:

Remotion.Linq.Parsing.ParserException: Could not parse expression 'c.employees.Any(value(System.Func2[Entities.Domain.Department,System.Boolean]))': Object of type 'System.Linq.Expressions.ConstantExpression' cannot be converted to type 'System.Linq.Expressions.LambdaExpression'. If you tried to pass a delegate instead of a LambdaExpression, this is not supported because delegates are not parsable expressions. at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.CreateExpressionNode(Type nodeType, MethodCallExpressionParseInfo parseInfo, Object[] additionalConstructorParameters) at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.Parse(String associatedIdentifier, IExpressionNode source, IEnumerable1 arguments, MethodCallExpression expressionToParse) at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseMethodCallExpression(MethodCallExpression methodCallExpression, String associatedIdentifier) at Remotion.Linq.Parsing.Structure.QueryParser.GetParsedQuery(Expression expressionTreeRoot) at Remotion.Linq.Parsing.ExpressionTreeVisitors.SubQueryFindingExpressionTreeVisitor.VisitExpression(Expression expression) at Remotion.Linq.Parsing.ExpressionTreeVisitor.VisitLambdaExpression(LambdaExpression expression) at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.ProcessArgumentExpression(Expression argumentExpression) at System.Linq.Enumerable.WhereSelectEnumerableIterator2.MoveNext()
at System.Linq.Buffer
1..ctor(IEnumerable1 source) at System.Linq.Enumerable.ToArray(IEnumerable1 source) at Remotion.Linq.Parsing.Structure.MethodCallExpressionParser.Parse(String associatedIdentifier, IExpressionNode source, IEnumerable`1 arguments, MethodCallExpression expressionToParse) at Remotion.Linq.Parsing.Structure.ExpressionTreeParser.ParseMethodCallExpression(MethodCallExpression methodCallExpression, String associatedIdentifier) at Remotion.Linq.Parsing.Structure.QueryParser.GetParsedQuery(Expression expressionTreeRoot) at NHibernate.Linq.NhRelinqQueryParser.Parse(Expression expression) in NhRelinqQueryParser.cs: line 39

...and so on for the rest of the stack.

My conundrum seems to be that one can't combine Funcs, only Expressions -- which produces another Expression. But one can't hand an Expression to IEnumerable.Any() -- only a Func. But then the Func produced by Expression.Compile() seems to be the wrong kind...

Any ideas?

Michael

2

There are 2 answers

1
Oskar Berggren On BEST ANSWER

Consider this code:

Func<T1, TResult> func = t => t == 5;
Expression<Func<T1, TResult>> expression = t => t == 5;

With this code, func will be a reference to compiled code, while expression will be a reference to an abstract syntax tree representing the lambda expression. Calling Compile() on the expression will return compiled code functionally equivalent to func.

What type of expression is contained in the compiled code is irrelevant - the LINQ providers simply cannot decode compiled code. They rely on walking the abstract syntax tree represented by the Expression<Func<...>> type.

In your case, you apply Any() on d.employees. Since employees is an IEnumerable<T>, you will get the version of Any() that expects to get compiled code that it can run. Note that Any() is also available for queryables, and will in that case accept an expression.

You could try AsQueryable(), but I'm not sure it will work:

session.Query<Department>()
       .Where(d => d.employees.AsQueryable().Any(ex1.AndAlso(ex2)));

Otherwise, you have to rewrite it without using any.

0
Michael On

It occurred to me that, given that the problem revolved around the fact that IEnumerable methods take Func arguments, if I could get d.employees (in my example) to be an IQueryable rather than an IEnumerable then its Any would take an Expression and I'd be done.

So, I tried inserting an .AsQueryable() after d.employees, and that worked!