In our application, we have many function calls that we want to cache the results of. However, the result of these functions depends on database calls, so we want this cache to refresh every so often so that new data is pulled from the database on a regular basis. (Note: we know that these values are changing once a day, so we don't have to refresh the cache very often)
We currently have a caching system in place that stores the results in a ConcurrentDictionary
, so I want to replace that backing with a TimeSensitiveConcurrentDictionary
. If the current time is past the expiration time stored for the provided parameters, it will instead return the provided value. In our case, that provided value is a Lazy<R>
Below is the full code to create a cache
public static class FuncUtil_TimeSensitive
{
public static readonly TimeSpan ValidTime = TimeSpan.FromHours(6);
public static Func<A, R> Memoize<A, R>(Func<A, R> f)
{
var map = DictionaryHelper<Lazy<R>>.CreateConcurrent(new { a = default(A) });
return a => map.GetOrAdd(new { a }, new Lazy<R>(() => f(a))).Value;
}
public static Func<A, B, R> Memoize<A, B, R>(this Func<A, B, R> f)
{
var map = DictionaryHelper<Lazy<R>>.CreateConcurrent(new { a = default(A), b = default(B) });
return (a, b) => map.GetOrAdd(new { a, b }, new Lazy<R>(() => f(a, b))).Value;
}
//other methods
static class DictionaryHelper<Value>
{
public static TimeSensitiveConcurrentDictionary<Key, Value> CreateConcurrent<Key>(Key prototype)
{
return new TimeSensitiveConcurrentDictionary<Key, Value>(ValidTime);
}
}
}
/// <summary>
/// Stores a value based on the key for a set amoutn of time. After that time has passed, accessing the same key will remove the value
/// </summary>
/// <typeparam name="TKey">Type to use as a key</typeparam>
/// <typeparam name="TValue">Type to use as a value</typeparam>
public class TimeSensitiveConcurrentDictionary<TKey, TValue>
{
private readonly ConcurrentDictionary<TKey, TimeSensitiveConcurrentDictionaryValue<TValue>> cache
= new ConcurrentDictionary<TKey, TimeSensitiveConcurrentDictionaryValue<TValue>>();
private readonly TimeSpan expirationTime;
public TimeSensitiveConcurrentDictionary(TimeSpan validTime)
{
expirationTime = validTime;
}
public TValue GetOrAdd(TKey key, TValue newValue)
{
var now = DateTime.Now;
bool needsToUpdate = false;
var currentExpirationDate = now.Add(expirationTime);
if (cache.ContainsKey(key))
{
currentExpirationDate = cache[key].ExpirationDate;
if (now > currentExpirationDate)
{
TryRemove(key);
needsToUpdate = true;
}
}
else
{
needsToUpdate = true;
}
if (needsToUpdate)
{
currentExpirationDate = now.Add(expirationTime);
}
return cache.GetOrAdd(key, new TimeSensitiveConcurrentDictionaryValue<TValue>(currentExpirationDate, newValue)).Value;
}
public bool TryRemove(TKey a)
{
TimeSensitiveConcurrentDictionaryValue<TValue> result;
return cache.TryRemove(a, out result);
}
private struct TimeSensitiveConcurrentDictionaryValue<TTSValue>
{
private DateTime expirationDate;
private TTSValue value;
public DateTime ExpirationDate { get { return expirationDate; } }
public TTSValue Value { get { return value; } }
public TimeSensitiveConcurrentDictionaryValue(DateTime expirationDate, TTSValue value)
{
this.expirationDate = expirationDate;
this.value = value;
}
}
}
Example Usage:
Func<int, string> MyCache = FuncUtil_TimeSensitive.Memoize<int, string>(a => {
//retrieve a value from the database, return the result
return MyDBContext.Table.Single(x => x.Field == a).SomeStringValue;
});
string myDBValue = MyCache(1); //hits database
string sameValue = MyCache(1); //hits cache
Thread.Sleep(6 * 60 * 60 * 1000); //sleep for a while
string newValueFromDB = MyCache(1); //key has expired, hit database again for new value
Other notes: this does need to be thread-safe.
Does this implementation seem valid? I am not an expert on concurrency. I would also accept public implementations that I could use instead of writing my own.
One thought I had: Would I benefit from making it inherit from IDictionary<>
or from ConcurrentDictionary
? From what I hear, you shouldn't inherit from collections, and I don't plan on using this for other purposes, so I'm not sure if it's worth it to make this more robust.