1
Vote

Query Cache Does Not Recursively Parameterize

description

Query cache parameterizer does not recursively evaluate nested queries, leading to a situation where cached queries will not be properly executed for different sets of paramters.
 
var inner1 = db.Customers.Where(c=>c.CustomerID == "ALFKI")
var outer1 = from o in db.Orders
               from c in inner1
               select new { c, o }
 
var inner2 = db.Customer.Where(c=>c.CustomerID == "ANATR");
var outer2 = from o in db.Orders
               from c in inner2
               select new { c, o }
 
If outer1 were to be executed first, and loaded into the query cache, then outer 2 were to be executed through the same query cache, the query, and the results, would be identical.
 
The query cache will not properly parameterize the "ALFKI" constant, this is because the expression for outer is of the form db.Orders.SelectMany(c=>CONSTANT EXPRESSION (inner)).
 
The query cache parameterizer does not expand or inspect this constant expression and does not evaluate the data inside of it (e.g the "ALFKI" / "ANATR") constant. When outer1 was loaded into the cache, a complete reference to inner1 was saved, so outer2 will be executed using the query from inner1 instead of inner2.

comments

smokingrope wrote Sep 24, 2012 at 8:16 AM

Original code in QueryCache which is affected by this issue looks as follows:
        var body = PartialEvaluator.Eval(query, fn, c =>
        {
            bool isQueryRoot = c.Value is IQueryable;
            if (!isQueryRoot && ep != null && !ep.CanBeParameter(c))
                return c;
            var p = Expression.Parameter(c.Type, "p" + parameters.Count);
            parameters.Add(p);
            values.Add(c.Value);
            // if query root then parameterize but don't replace in the tree 
            if (isQueryRoot)
                return c;
            return p;
        });
It's possible to rewrite this slightly as:
        Func<ConstantExpression, Expression> partialEvalFunc = null;
        partialEvalFunc = c =>
        {
            bool isQueryRoot = c.Value is IQueryable;
            if (!isQueryRoot && ep != null && !ep.CanBeParameter(c))
                return c;
            ParameterExpression p = null;

            if (isQueryRoot && !typeof(IEntityTable).IsAssignableFrom(c.Type.GetGenericTypeDefinition()))
            {
                // parameterize inner queryables
                IQueryable q = (IQueryable)c.Value;
                if (q.Provider is IEntityProvider)
                {
                    q = q.Provider.CreateQuery(PartialEvaluator.Eval(q.Expression, fn, partialEvalFunc));
                    c = Expression.Constant(q, c.Type);
                }
            }

            p = Expression.Parameter(c.Type, "p" + parameters.Count);
            parameters.Add(p);
            values.Add(c.Value);

            // if query root then parameterize but don't replace in the tree 
            if (isQueryRoot)
                return c;
            return p;
        };

        var body = PartialEvaluator.Eval(query, fn, partialEvalFunc);

smokingrope wrote Sep 24, 2012 at 8:21 AM

In addition to changes mentioned above, you must also modify the ExplcitToObjectArray class if you want things to work right when you have more than 4 parameterized values in your query.

Add the following function override:
        protected override Expression VisitConstant(ConstantExpression c)
        {
            var queryable = c.Value as IQueryable;

            // rewrite parameters for queryables nestled inside a constant expression
            if (queryable != null && !typeof(IEntityTable).IsAssignableFrom(c.Type.GetGenericTypeDefinition()))
            {
                if (queryable.Provider is IEntityProvider)
                {
                    queryable = queryable.Provider.CreateQuery(this.Visit(queryable.Expression));
                    c = Expression.Constant(queryable, c.Type);
                    return c;
                }
            }

            return base.VisitConstant(c);
        }

wrote Feb 14, 2013 at 7:44 PM