Building a type-safe ASP.Net cache (and improve its thread-safety at the same time)
By George Mihaescu
Summary: this article makes the case for using a type safe cache object instead of the standard ASP.Net Cache and presents two solutions for this, one based on the Adapter design pattern, the other based on the Façade design pattern. In addition, both solutions have the extra benefit of encapsulating the synchronized access to those sequences of operations on the Cache that must be performed atomically, thus improving the thread-safety of the application.
I've made the same case and presented a very similar solution for the ASP.Net session in another article – this follows along the same lines.
ASP.Net offers the Cache (in System.Web.Caching) object for application that need to cache data for all users of the application. However, the provided Cache has a number of shortcomings:
By the way, none of these shortcomings are flaws in the ASP.Net Cache design: the Cache is a generic "bag" of any data types you may need to store. ASP.Net designers cannot know what each application will need to store in the cache – however, you do, because it's your application, and below you'll find what you can do to build a better cache for you particular application needs.
The goal is to find a design that would resolve the shortcomings listed above. I have found two such designs that only have minor differences:
using System;
using System.Data;
using System.Web.Caching;
/// <summary>
/// CacheAdapter implements a type-safe Adapter pattern over the ASP.Net Cache
/// </summary>
public class CacheAdapter
{
//the cache object we adapt
private readonly Cache m_cache;
//syncronizer object for atomic active user count operations
private static object m_active_user_count_synchronizer = new object ();
//Key names for objects stored in the cache
private const string ACTIVE_USERS_COUNT = "ActiveUsers";
private const string NEWS_CONTENT_TIMESTAMP = "NewsContentTimestamp";
private CacheAdapter(Cache c)
{
m_cache = c;
}
/// <summary>
/// Factory, constructs a cache adapter and returns it
/// </summary>
public static CacheAdapter GetCacheAdapter(Cache c)
{
return new CacheAdapter(c);
}
/// <summary>
/// Gets the active user count from the cache
/// </summary>
public int GetActiveUserCount()
{
int count = 0;
object c = m_cache[ACTIVE_USERS_COUNT];
if (c == null)
{
//not in the cache, load it from the permanent
//store / compute it / etc
Random r = new Random (DateTime.Now.Second);
count = r.Next(2, 20);
}
else
{
count = (int)c;
}
return count;
}
/// <summary>
/// Increments the active user count in the cache
/// </summary>
public void IncrementActiveUserCount()
{
lock (m_active_user_count_synchronizer)
{
int c = GetActiveUserCount();
m_cache[ACTIVE_USERS_COUNT] = c + 1;
}
}
/// <summary>
/// Decrements the active user count in the cache
/// </summary>
public void DecrementActiveUserCount()
{
lock (m_active_user_count_synchronizer)
{
int c = GetActiveUserCount();
if (c >= 1)
{
m_cache[ACTIVE_USERS_COUNT] = c - 1;
}
}
}
/// <summary>
/// Gets / sets the timestamp the news page was last updated.
/// May return null if not the cache.
/// Set it to null to remove it from the cache.
/// </summary>
public Nullable<DateTime> NewsContentTimestamp
{
get
{
object d = m_cache[NEWS_CONTENT_TIMESTAMP];
if (d == null)
{
return null;
}
else
{
return (DateTime)d;
}
}
set
{
if (value == null)
{
m_cache.Remove(NEWS_CONTENT_TIMESTAMP);
}
else
{
m_cache[NEWS_CONTENT_TIMESTAMP] = value;
}
}
}
}
{
//get the cache adapter and use it
CacheAdapter c = CacheAdapter.GetCacheAdapter(Cache);
int count = c.GetActiveUserCount();
c.IncrementActiveUserCount();
count = c.GetActiveUserCount();
c.DecrementActiveUserCount();
count = c.GetActiveUserCount();
}
using System;
using System.Web;
using System.Web.Caching;
/// <summary>
/// CacheAdapter implements a type-safe Facade pattern over the ASP.Net Cache
/// </summary>
public static class CacheFacade
{
//Key names for objects stored in the cache
private const string ACTIVE_USERS_COUNT = "ActiveUsers";
private const string NEWS_CONTENT_TIMESTAMP = "NewsContentTimestamp";
//syncronizer object for atomic active user count operations
private static object m_active_user_count_synchronizer = new object ();
/// <summary>
/// Gets the active user count from the cache
/// </summary>
public static int GetActiveUserCount()
{
//get the cache in the current context
Cache cache = HttpContext.Current.Cache;
int count = 0;
object c = cache[ACTIVE_USERS_COUNT];
if (c == null)
{
//not in the cache, load it from the permanent store /
//compute it / etc
Random r = new Random(DateTime.Now.Second);
count = r.Next(2, 20);
}
else
{
count = (int)c;
}
return count;
}
/// <summary>
/// Increments the active user count in the cache
/// </summary>
public static void IncrementActiveUserCount()
{
lock (m_active_user_count_synchronizer)
{
int c = GetActiveUserCount();
//get the cache in the current context
Cache cache = HttpContext.Current.Cache;
cache[ACTIVE_USERS_COUNT] = c + 1;
}
}
/// <summary>
/// Decrements the active user count in the cache
/// </summary>
public static void DecrementActiveUserCount()
{
lock (m_active_user_count_synchronizer)
{
int c = GetActiveUserCount();
if (c >= 1)
{
//get the cache in the current context
Cache cache = HttpContext.Current.Cache;
cache[ACTIVE_USERS_COUNT] = c - 1;
}
}
}
/// <summary>
/// Gets / sets the timestamp the news page was last updated.
/// May return null if not the cache.
/// Set it to null to remove it from the cache.
/// </summary>
public static Nullable<DateTime> NewsContentTimestamp
{
get
{
//get the cache in the current context
Cache cache = HttpContext.Current.Cache;
object d = cache[NEWS_CONTENT_TIMESTAMP];
if (d == null)
{
return null;
}
else
{
return (DateTime)d;
}
}
set
{
//get the cache in the current context
Cache cache = HttpContext.Current.Cache;
if (value == null)
{
cache.Remove(NEWS_CONTENT_TIMESTAMP);
}
else
{
cache[NEWS_CONTENT_TIMESTAMP] = value;
}
}
}
}
{
//use the cache Facade
int count = CacheFacade.GetActiveUserCount();
CacheFacade.IncrementActiveUserCount();
count = CacheFacade.GetActiveUserCount();
CacheFacade.DecrementActiveUserCount();
count = CacheFacade.GetActiveUserCount();
}
The .Net-provided Cache is already thread-safe; this means that reading or writing operations in the Cache from concurrent requests will be serialized. However, if you need a succession of read/write operations in the Cache to be atomic and thread-safe, you need to write your own synchronization mechanism; one example of such scenario is given above, where you need to read the current count of active users from the cache, increment it, and then write it back to the cache. Those three operations must be guaranteed atomic or you’ll get indeterminist behavior in the context of multiple concurrent requests. Regardless of the solution you adopt for this type of synchronization, never lock the Cache object itself; this is far too heavy-handed and will affect the performance and scalability of the application, because the Cache will be used from many contexts, and locking it only to perform one logical sequence will prevent other contexts from performing unrelated logical sequences.
One additional benefit of either solutions presented above is the ability to encapsulate synchronization mechanisms such as the one described above. The key to this mechanism is a static member object that will be used for locking over sequences of operations that must be atomic; this is called a synchronizer object. Create and use one such synchronizer for each type of logical sequence that must be atomic, as in the examples above: the m_active_user_count_synchronizer object is used to synchronize the logical sequences that increment and decrement the active user count in the cache; for another type of logical operations (e.g. count of currently logged on users) I would create and use another synchronizer, so that incrementing / decrementing the user count in the cache does not prevent other requests from incrementing / decrementing the count of logged on users.
The above implementations are equally good; the chances that you'll be mistakenly using the CacheFacade outside the context of a web request (e.g. worker thread, unit test, or certain functions in Global.asax) are pretty slim, and if you do, you'll find out during development very quickly – therefore I would be inclined to use the implementation based on the Façade pattern (for its convenience).
Note that in addition to meeting the goals I've outlined at the beginning of the article, those implementations also offer additional benefits:
One minor objection to the above implementations can be that they don't prevent a sloppy / unaware developer in the team from circumventing them and going directly to the ASP.Net Cache, bringing back in the issues listed at the top of the article.
If you want to prevent this, one possible solution could be to add code in the above implementations that checks the state of the underlying ASP.Net Cache every time an item is set / retrieved in the cache (i.e. at the top of each method and each get / set property implementation).
For instance, traverse all the keys and if you find one that's not a known key, throw an exception – that way you'll know immediately if someone has bypassed your type-safe implementation. But probably that's too heavy-handed for most cases.