Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generating Cache Keys from IQueryable For Caching Results of EF Code First Queries

I'm trying to implement a caching scheme for my EF Repository similar to the one blogged here. As the author and commenters have reported the limitation is that the key generation method cannot produce cache keys that vary with a given query's parameters. Here is the cache key generation method:

private static string GetKey<T>(IQueryable<T> query)
{
    string key = string.Concat(query.ToString(), "\n\r",
        typeof(T).AssemblyQualifiedName);
    return key;
}

So the following queries will yield the same cache key:

var isActive = true;
var query = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive == isActive).AsCacheable();

and

var isActive = false;
var query = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive == isActive).AsCacheable();

Notice that the only difference is that isActive = true in the first query and isActive = false in the second.

Any suggestions/insight to efficiently generating cache keys which vary by IQueryable parameters would be truly appreciated.

Kudos to Sergey Barskiy for sharing the EF CodeFirst caching scheme.

Update

I took the approach of traversing the IQueryable's expression tree myself with the goal of resolving the values of the parameters used in the query. With maxlego's suggestion, I extended the System.Linq.Expressions.ExpressionVisitor class to visit the expression nodes that we're interested in - in this case, the MemberExpression. The updated GetKey method looks something like this:

public static string GetKey<T>(IQueryable<T> query)
{
    var keyBuilder = new StringBuilder(query.ToString());
    var queryParamVisitor = new QueryParameterVisitor(keyBuilder);
    queryParamVisitor.GetQueryParameters(query.Expression);
    keyBuilder.Append("\n\r");
    keyBuilder.Append(typeof (T).AssemblyQualifiedName);

    return keyBuilder.ToString();
}

And the QueryParameterVisitor class, which was inspired by the answers of Bryan Watts and Marc Gravell to this question, looks like this:

/// <summary>
/// <see cref="ExpressionVisitor"/> subclass which encapsulates logic to 
/// traverse an expression tree and resolve all the query parameter values
/// </summary>
internal class QueryParameterVisitor : ExpressionVisitor
{
    public QueryParameterVisitor(StringBuilder sb)
    {
        QueryParamBuilder = sb;
        Visited = new Dictionary<int, bool>();
    }

    protected StringBuilder QueryParamBuilder { get; set; }
    protected Dictionary<int, bool> Visited { get; set; }

    public StringBuilder GetQueryParameters(Expression expression)
    {
        Visit(expression);
        return QueryParamBuilder;
    }

    private static object GetMemberValue(MemberExpression memberExpression, Dictionary<int, bool> visited)
    {
        object value;
        if (!TryGetMemberValue(memberExpression, out value, visited))
        {
            UnaryExpression objectMember = Expression.Convert(memberExpression, typeof (object));
            Expression<Func<object>> getterLambda = Expression.Lambda<Func<object>>(objectMember);
            Func<object> getter = null;
            try
            {
                getter = getterLambda.Compile();
            }
            catch (InvalidOperationException)
            {
            }
            if (getter != null) value = getter();
        }
        return value;
    }

    private static bool TryGetMemberValue(Expression expression, out object value, Dictionary<int, bool> visited)
    {
        if (expression == null)
        {
            // used for static fields, etc
            value = null;
            return true;
        }
        // Mark this node as visited (processed)
        int expressionHash = expression.GetHashCode();
        if (!visited.ContainsKey(expressionHash))
        {
            visited.Add(expressionHash, true);
        }
        // Get Member Value, recurse if necessary
        switch (expression.NodeType)
        {
            case ExpressionType.Constant:
                value = ((ConstantExpression) expression).Value;
                return true;
            case ExpressionType.MemberAccess:
                var me = (MemberExpression) expression;
                object target;
                if (TryGetMemberValue(me.Expression, out target, visited))
                {
                    // instance target
                    switch (me.Member.MemberType)
                    {
                        case MemberTypes.Field:
                            value = ((FieldInfo) me.Member).GetValue(target);
                            return true;
                        case MemberTypes.Property:
                            value = ((PropertyInfo) me.Member).GetValue(target, null);
                            return true;
                    }
                }
                break;
        }
        // Could not retrieve value
        value = null;
        return false;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        // Only process nodes that haven't been processed before, this could happen because our traversal
        // is depth-first and will "visit" the nodes in the subtree before this method (VisitMember) does
        if (!Visited.ContainsKey(node.GetHashCode()))
        {
            object value = GetMemberValue(node, Visited);
            if (value != null)
            {
                QueryParamBuilder.Append("\n\r");
                QueryParamBuilder.Append(value.ToString());
            }
        }

        return base.VisitMember(node);
    }
}

I'm still doing some performance profiling on the cache key generation and hoping that it isn't too expensive (I'll update the question with the results once I have them). I'll leave the question open, in case anyone has suggestions on how to optimize this process or has a recommendation for a more efficient method for generating cache keys with vary with the query parameters. Although this method produces the desired output, it is by no means optimal.

like image 224
HOCA Avatar asked Nov 26 '11 02:11

HOCA


3 Answers

i suggest to use ExpressionVisitor http://msdn.microsoft.com/en-us/library/bb882521(v=vs.90).aspx

like image 131
maxlego Avatar answered Oct 27 '22 17:10

maxlego


Just for the record, "Caching the results of LINQ queries" works well with the EF and it's able to work with parameters correctly, so it can be considered as a good second level cache implementation for EF.

like image 35
VahidN Avatar answered Oct 27 '22 17:10

VahidN


While the solution of the OP works quite well, I found that the performance of the solution is a little bit poor.

The duration of the key generation varied between 300ms and 1200ms for my queries.

However, I've found another solution that has quite better performance (<10ms).

    public static string ToTraceString<T>(DbQuery<T> query)
    {
        var internalQueryField = query.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance).Where(f => f.Name.Equals("_internalQuery")).FirstOrDefault();

        var internalQuery = internalQueryField.GetValue(query);

        var objectQueryField = internalQuery.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance).Where(f => f.Name.Equals("_objectQuery")).FirstOrDefault();

        var objectQuery = objectQueryField.GetValue(internalQuery) as ObjectQuery<T>;

        return ToTraceStringWithParameters(objectQuery);
    }

    private static string ToTraceStringWithParameters<T>(ObjectQuery<T> query)
    {
        string traceString = query.ToTraceString() + "\n";

        foreach (var parameter in query.Parameters)
        {
            traceString += parameter.Name + " [" + parameter.ParameterType.FullName + "] = " + parameter.Value + "\n";
        }

        return traceString;
    }
like image 42
flash Avatar answered Oct 27 '22 15:10

flash