7

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?

1
  • To me it looks like you are describing an identity map of sorts. In this case your key can simply be the typehandle combined with the id. The thing with identity maps is that you want to make sure they are bound to some sort of context, a global identity map is incredibly difficult to invalidate Commented May 31, 2011 at 2:56

2 Answers 2

6

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}); 
}
Sign up to request clarification or add additional context in comments.

Comments

3

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.