Generating a unique cache key based on method arguments
I have a basic repository framework that eve开发者_开发知识库ntually 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?
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});
}
I see a few options
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.
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.
精彩评论