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:
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?
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With