Evaluator.PartialEval decrements the supplied expression
In one of my projects, I have an ExpressionVisitor to translate a provided expression into some query string. But before translating it, I need to evaluate all repression in terms of real values. For this I am using the Evaluator.PartialEval method from the EntityFramework project.
Assuming I have this request:
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 commented out the Where method. In this case, the evaluatedQueryExpr will not contain the Take and Skip methods.
However, if I use any other method with the expression before executing or skipping everything works, the Evaluator evaluates the expression correctly and returns it in full.
I found out that the problem occurs on line 80 of the Evaluator class:
return Expression.Constant(fn.DynamicInvoke(null), e.Type);
Could you please explain why this is happening and suggest a workaround?
Update here project on github
LinqToSolrQueriable inherited from IOrderedQueryable LinqToSolrProvider inherited from IQueryProvider including linear range causing issue
source to share
The good news is that the expression is not really minified ( Skip
and Take
still is :), but 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 the wrong impression. If you take ConstaintExpression.Value
, you will see that the IQueryable<Product>
c property Expression
is the same as the original query.Expression
.
The bad news is that this is not what you expect from it PartialEval
- in fact, in this case, it does nothing useful (other than potentially breaking the query translation logic).
So why is this happening?
The method you are using from EntityFramework.Extended is in turn taken (as pointed out in the comments) from the MSDN example Walkthrough. Creating an IQueryable LINQ Provider . You may notice that the method PartialEval
has two overloads - one with a parameter Func<Expression, bool> fnCanBeEvaluated
used to determine if a given node expression can be part of a local function (in other words, partially evaluated or not) and one without such a parameter (used by you), which just calls the first pass of the following predicate:
private static bool CanBeEvaluatedLocally(Expression expression)
{
return expression.NodeType != ExpressionType.Parameter;
}
The effect is that it stops the evaluation of type expressions ParameterExpression
and any expressions containing directly or indirectly ParameterExpression
. The latter should explain the behavior that you observe. When a query contains Where
(and basically any LINQ statement) with a parameterized lambda expression (hence a parameter) before calls to Skip
/ Take
, it will stop evaluating the contained methods (as you can see from the above query.Expression
debug view - the call Where
will be inside Skip
).
This overload is now used by the MSDN example to evaluate a specific nested lambda method Where
expression, and is generally not applicable to any type of expression such as IQueryable.Expression
. The actually linked project uses a method PartialEval
in one place within the QueryCache class and also invokes another overload, passing through another predicate , which in addition to ParameterExpressions
stops evaluating any expression with a result type IQueryable
.
I think this is the solution to your problem:
var evaluatedQueryExpr = Evaluator.PartialEval(query.Expression,
// can't evaluate parameters or queries
e => e.NodeType != ExpressionType.Parameter &&
!typeof(IQueryable).IsAssignableFrom(e.Type)
);
source to share