Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generating a unique cache key based on method arguments

I have a basic repository framework that eventually executes a query and maps the results back into a object:

For Example:

    public SomeEntity Get(id)
    {
        return base.GetItem<SomeEntity>
                   ("select * from SomeEntities where id = @idParam",
                    new { idParam = id}); 
    }

If this looks like Dapper, it is because under the hood GetItem is wrapping Dapper.

I'd like to add automatic caching to GetItem, I have two arguments that come in:

  • A string containing the query.
  • a anonymous dictionary containing any parameters.

I'm worried that doing a simple prime hash on these parameters would cause cache key collisions, and when you are pulling data from a cache, a collision can be very very bad (I.E. leaking sensitive information).

So, what techniques do I have that would generate a reasonably sized cache key, while guaranteeing uniqueness based on the input of a query and parameters?

like image 839
Jonathan Holland Avatar asked May 30 '11 19:05

Jonathan Holland


People also ask

What are key cache parameters?

The cache key is the unique identifier for every object in the cache, and it determines whether a viewer request results in a cache hit.

What is a cache key?

The cache key is the unique identifier for an object in the cache. Each object in the cache has a unique cache key. A cache hit occurs when a viewer request generates the same cache key as a prior request, and the object for that cache key is in the edge location's cache and valid.


2 Answers

I use the following extension methods to make cached versions of delegates:

    public static Func<T, TResult> AsCached<T, TResult>(this Func<T, TResult> function)
    {
        var cachedResults = new Dictionary<T, TResult>();
        return (argument) =>
        {
            TResult result;
            lock (cachedResults)
            {
                if (!cachedResults.TryGetValue(argument, out result))
                {
                    result = function(argument);
                    cachedResults.Add(argument, result);
                }
            }
            return result;
        };
    }

    public static Func<T1, T2, TResult> AsCached<T1, T2, TResult>(this Func<T1, T2, TResult> function)
    {
        var cachedResults = new Dictionary<Tuple<T1, T2>, TResult>();
        return (value1, value2) =>
        {
            TResult result;
            var paramsTuple = new Tuple<T1, T2>(value1, value2);
            lock(cachedResults)
            {
                if (!cachedResults.TryGetValue(paramsTuple, out result))
                {
                    result = function(value1, value2);
                    cachedResults.Add(paramsTuple, result);
                }
            }
            return result;
        };
    }

    public static Func<T1, T2, T3, TResult> AsCached<T1, T2, T3, TResult>(this Func<T1, T2, T3, TResult> function)
    {
        var cachedResults = new Dictionary<Tuple<T1, T2, T3>, TResult>();
        return (value1, value2, value3) =>
        {
            TResult result;
            var paramsTuple = new Tuple<T1, T2, T3>(value1, value2, value3);
            lock(cachedResults)
            {
                if (!cachedResults.TryGetValue(paramsTuple, out result))
                {
                    result = function(value1, value2, value3);
                    cachedResults.Add(paramsTuple, result);
                }
            }
            return result;
        };
    }

And so on for N parameters...

In case it's not clear from the code, I create a tuple with the arguments, and use the tuple as a key to a dictionary that holds the return values for each set of arguments. Note that every time you call AsCached, you create a separate cache.

You can use these methods as follows:

private Func<int, SomeEntity> _getCached;

public SomeEntity Get(int id)
{
    if (_getCached == null)
    {
        Func<int, SomeEntity> func = GetImpl;
        _getCached = func.AsCached();
    }
    return _getCached(id);
}

private SomeEntity GetImpl(int id)
{
    return base.GetItem<SomeEntity>
               ("select * from SomeEntities where id = @idParam",
                new { idParam = id}); 
}
like image 69
Thomas Levesque Avatar answered Oct 12 '22 23:10

Thomas Levesque


I see a few options

  1. Pack the data into a class, use a BinaryFormatter to serialize the class and perform a SHA1 hash on the serialized data to to give you a hash key.

  2. Pack the data into a class, implement IEqualityComparer which can then be stored in a Dictionary. By implementing IEqualityComparer you will control the generation of the Hash and the data comparison performed to identify the unique data when collisions occur.

like image 21
Chris Taylor Avatar answered Oct 12 '22 22:10

Chris Taylor