I've been using a simple caching system on the project i'm currently working on for some time. But i realized some limitation of this system.
I wanted a more standardized way to define the data being cached. The data I cache are of 2 main types, data about certain users that is accessed many times for a limited time span, and data accessed a lot by all users. This data is either dependent on changes in the database or its not that important that the data is
up to date.
The second type of data can be expensive to create, it comes for web services and heavy database queries. A problem with the caching method above is we can have situations where several threads request the data at the same time, doesn't find it and queries the database at the same time. I don't want this to happen, it can be resolved in two ways, either wait on the first thread to get the data, or show the old data until the new is available. And if we store the old data, we might as well automatically fetch updateed data when the cache expires.
public class CachedItemMetadata
{
public string BaseKey { get; set; }
public ObjectLoader Loader { get; set; }
public bool AutoReload { get; set; }
public int AbsoluteExpiryMinutes { get; set; }
public int SlidingExpiryMinutes { get; set; }
public string[] TableDependency { get; set; }
}
public delegate object ObjectLoader(object[] param);
public class CachingService
{
private class CachedItem
{
public Dictionary<string, object> Items { get; set; }
public Dictionary<string, object> OldItems { get; set; }
public Dictionary<string, Mutex> Locks { get; set; }
public Dictionary<string, object[]> Params { get; set; }
public CachedItemMetadata Metadata { get; set; }
public CachedItem(CachedItemMetadata i)
{
Items = new Dictionary<string, object>();
OldItems = new Dictionary<string, object>();
Locks = new Dictionary<string, Mutex>();
Params = new Dictionary<string, object[]>();
Metadata = i;
}
}
private static Dictionary<string, CachedItem> items;
static CachingService()
{
items = new Dictionary<string, CachedItem>();
Init();
}
protected static void Init()
{
Add(new CachedItemMetadata()
{
AbsoluteExpiryMinutes = 10,
AutoReload = true,
BaseKey = "CalculatedStats",
TableDependency = null,
SlidingExpiryMinutes = 0,
Loader = delegate(object[] param)
{
return Databroker.CalculateStatistics(
DateTime.Now.AddHours(-(int)param[0]),
DateTime.Now);
},
});
}
public static void Add(CachedItemMetadata item)
{
items.Add(item.BaseKey, new CachedItem(item));
}
public static string CreateFullKey(string key, object[] param)
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.Append(key);
sb.Append('(');
bool isFirst = true;
if (param != null)
{
foreach (object o in param)
{
if (!isFirst)
sb.Append(',');
else
isFirst = false;
sb.Append(o.ToString());
}
}
sb.Append(')');
return sb.ToString();
}
public static T Get<T>(string key, object[] param) where T : class
{
object obj = Get(key, param);
return obj as T;
}
public static object Get(string key, object[] param)
{
Cache objCache = HttpContext.Current.Cache;
string fullKey = CreateFullKey(key, param);
object obj = objCache[fullKey];
if (obj == null && items.ContainsKey(key))
{
CachedItem objItemInfo = items[key];
// check that the lock exists
if (!objItemInfo.Locks.ContainsKey(fullKey))
{
lock (objItemInfo.Locks)
{
if (!objItemInfo.Locks.ContainsKey(fullKey))
objItemInfo.Locks.Add(fullKey, new Mutex());
}
}
Mutex objLock = objItemInfo.Locks[fullKey];
bool didLock = objLock.WaitOne(0, false);
if (!didLock)
{
// if autoreloaded item try and see if we can return the old item
if (objItemInfo.Metadata.AutoReload
&& objItemInfo.OldItems.ContainsKey(fullKey))
{
obj = objItemInfo.OldItems[fullKey];
if (obj != null)
return obj;
}
// wait for load to complete
if (objLock.WaitOne())
{
objLock.ReleaseMutex();
return objItemInfo.Items[fullKey];
}
else
return null;
}
else
{
obj = objItemInfo.Metadata.Loader(param);
CacheDependency dep = null;
TimeSpan sliding = Cache.NoSlidingExpiration;
DateTime abs = Cache.NoAbsoluteExpiration;
if (objItemInfo.Metadata.TableDependency != null)
dep = CreateSqlDependency(objItemInfo.Metadata.TableDependency);
if (objItemInfo.Metadata.SlidingExpiryMinutes > 0)
sliding = new TimeSpan(0, objItemInfo.Metadata.SlidingExpiryMinutes, 0);
if (objItemInfo.Metadata.AbsoluteExpiryMinutes > 0)
abs = DateTime.Now.AddMinutes(objItemInfo.Metadata.AbsoluteExpiryMinutes);
objCache.Insert(fullKey,
obj,
dep,
abs,
sliding,
CacheItemPriority.Normal,
new CacheItemRemovedCallback(delegate(string key2, object o,
CacheItemRemovedReason reason)
{
ItemRemoved(objItemInfo, key2, o, reason);
}));
objItemInfo.Items[fullKey] = obj;
objItemInfo.Params[fullKey] = param;
objLock.ReleaseMutex();
objItemInfo.OldItems.Remove(fullKey);
}
}
return obj;
}
private static void ItemRemoved(CachedItem item, string key,
object o, CacheItemRemovedReason reason)
{
if (item.Metadata.AutoReload)
{
item.OldItems[key] = o;
}
item.Items.Remove(key);
if (item.Metadata.AutoReload && item.Params.ContainsKey(key))
{
Get(item.Metadata.BaseKey, item.Params[key]);
}
}
}