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.
The problem
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:
- It is not type-safe: the type of item stored in the cache
is Object; you always need to cast it to its actual type when you
retrieve it. This cast is always based on the developer knowing the actual
type of the object stored for that key. If there is a design change and
the type of that item stored has changed, you MUST make sure that all
casts when retrieving the item are updated to the correct type, otherwise
you get runt-time cast exceptions.
- It is open to key name collisions: developers can use by
mistake the same key for two different purposes.
- It is open to key name mistypes: developers can by mistake
mistype the key and then the expected object will not be found in the cache.
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 solution
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:
- The design based on the Adapter pattern is less
convenient: you always have to pass in the ASP.Net Cache to get your type-safe
CacheAdapter.
But is safer to use because by having you pass the Cache in, the
call is guaranteed to be in the context of a web request.
- The design based on the Façade pattern is more convenient:
you don't have to pass anything to get your type-safe CacheFacade
(it is just a static class implementation). But it assumes that it's being
called in the context of a web request – if not, the current HttpContext
will be null and you'll get run-time exceptions.
The Adapter pattern implementation
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;
}
}
}
}
Example of use:
{
//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();
}
The Façade pattern implementation
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;
}
}
}
}
Example of use:
{
//use the cache Facade
int count = CacheFacade.GetActiveUserCount();
CacheFacade.IncrementActiveUserCount();
count = CacheFacade.GetActiveUserCount();
CacheFacade.DecrementActiveUserCount();
count = CacheFacade.GetActiveUserCount();
}
Thread safety
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.
Conclusion:
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:
- The usual benefits of having setters (e.g. validate what's
getting into the cache; log the value if in DEBUG mode, etc).
- The usual benefits of having getters (e.g. initialize
value to a default if not present in the cache, etc).
- They hide the internal implementation of the cache
workings, therefore reducing the chances of errors in the application and
decoupling the application code from changes in the cache internal
workings. For example, it encapsulates the synchronized access to
the Cache for those operations that must be performed atomically – see the
IncrementActiveUserCount and DecrementActiveUserCount
methods in the examples above.
- Allow for retrieval of value-types (such as the DateTime
in the examples above) or null if the value is not present through the use
of the generic Nullable<T> (available in .Net 2.0 only).
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.