Search code examples
c#entity-frameworklinqlambdaexpression-trees

Evaluator.PartialEval reduce provided expression


In one of my projects I have an ExpressionVisitor to translate provided expression into some query string. But before translating it I need to evaluate all refferences in the expression to real values. To do that I use Evaluator.PartialEval method from EntityFramework Project.

Assuming I have this query:

 var page = 100;
 var query = myService.AsQueryable<Product>()
              //.Where(x=>x.ProductId.StartsWith(p.ProductId))
                .Skip(page)
                .Take(page);

var evaluatedQueryExpr = Evaluator.PartialEval(query.Expression);

As you can see I have commented Where method. In this case evaluatedQueryExpr will not contain the methods Take and Skip.

However, if I use any other method with Expression before Take or Skip everything works, Evaluator evaluates an expression correctly and return it fully.

I found out that the problem occurs in the line 80 of the Evaluator class:

return Expression.Constant(fn.DynamicInvoke(null), e.Type);

Could you explain why this happens and suggest a workaround?

Update here is a project on github

LinqToSolrQueriable inherited from IOrderedQueryable LinqToSolrProvider inherited from IQueryProvider including line range causing the issue


Solution

  • The good news are that the expression is not really reduced (Skip and Take are still there :), but is simply converted from MethodCallExpression to ConstantExpression containing the original expression:

    query.Expression:

    .Call System.Linq.Queryable.Take(
        .Call System.Linq.Queryable.Skip(
            .Constant<LinqToSolr.Query.LinqToSolrQueriable`1[LinqToSolrTest.Product]>(LinqToSolr.Query.LinqToSolrQueriable`1[LinqToSolrTest.Product]),
            100),
        100)
    

    evaluatedQueryExpr:

    .Constant<System.Linq.IQueryable`1[LinqToSolrTest.Product]>(LinqToSolr.Query.LinqToSolrQueriable`1[LinqToSolrTest.Product])
    

    Here the debug display is giving you a wrong impression. If you take the ConstaintExpression.Value, you'll see that it's a IQueryable<Product> with Expression property being exactly the same as the original query.Expression.

    The bad news are that this is not what you expect from PartialEval - in fact it doesn't do anything useful in this case (except potentially breaking your query translation logic).

    So why is this happening?

    The method you are using from EntityFramework.Extended library is in turn taken (as indicated in the comments) from MSDN Sample Walkthrough: Creating an IQueryable LINQ Provider. It can be noticed that the PartialEval method has two overloads - one with Func<Expression, bool> fnCanBeEvaluated parameter used to identify whether a given expression node can be part of the local function (in other words, to be partially evaluated or not), and one without such parameter (used by you) which simply calls the first passing the following predicate:

    private static bool CanBeEvaluatedLocally(Expression expression)
    {
        return expression.NodeType != ExpressionType.Parameter;
    }
    

    The effect is that it stops evaluation of ParameterExpression type expressions and any expressions containing directly or indirectly ParameterExpression. The last should explain the behavior you are observing. When the query contains Where (and basically any LINQ operator) with parametrized lambda expression (hence parameter) before the Skip / Take calls, it would stop evaluation of the containing methods (which you can see from the above query.Expression debug view - the Where call will be inside the Skip).

    Now, this overload is used by the MSDN example to evaluate a concrete nested Where method lambda expression and is not generally applicable for any type of expression like IQueryable.Expression. In fact the linked project is using the PartialEval method in a single place inside QueryCache class, and also calling the other overload passing a different predicate which in addition to ParameterExpressions stops the evaluation of any expression with result type of IQueryable.

    Which I think is the solution of your problem as well:

    var evaluatedQueryExpr = Evaluator.PartialEval(query.Expression,
        // can't evaluate parameters or queries
        e => e.NodeType != ExpressionType.Parameter &&
            !typeof(IQueryable).IsAssignableFrom(e.Type)
    );