Click to search the site Click to log in
Online articles
Download free tools
Support pages, per product
Services
Frequently asked questions, per product
Building a type-safe ASP.Net cache (and improve its thread-safety at the same time)
Author: George Mihaescu
Published: November 10, 2006
Category: Best practices / Design pattern / ASP.Net
Notes:
Description: This article makes the case for having a type-safe implementation of a cache object in your ASP.Net application and describes two very simple design patterns to achieve this (and replace the untyped collection offered by ASP.Net). The solutions presented also improve the thread-saftey of the application, while preserving performance and scalability.
View count: 2,209
Comments: 4 Read comments or post your own

  Print viewOpens in new window
 Building a type safe cache

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.

 

 


Reader comments:
Name: (optional)
Verification text:    
(type as in image next to it)
Comment: max 2,000 characters; for security reasons no active content / no HTML formatting is supported.
Please stick to the subject of the article; comments are reviewed and unrelated / inappropriate ones will be deleted.

On Apr 17, 2007 at 12:44 EST George said:

Thanks for the kind remarks, actually I have posted at the same time an article about the Session as well (it's right under this one in the list of articles). Along the same lines, I present there both the Adapter and Facade patterns used for the ASP.Net Session.

On Apr 16, 2007 at 14:42 EST Anonymous said:

Thanks for the article, I've never gave it much tought, but you are right, it makes perfect sense. I've seen a similar article about the Session on codeproject.com, which suggests the same pattern for the session implementation (although the author there was describing only the facade pattern, not the adapter one - but I prefer the facade, too).

On Apr 12, 2007 at 14:45 EST Anonymous said:

*** The comment was removed due to unrelated or inappropriate content. ***

On Jan 12, 2007 at 13:38 EST Anonymous said:

Excellent article, definitely a pattern that should be used in any site that's more than the most basic. Once you have multiple developers working on a site, this is must.
Copyright 2308 registered users, 24 users online now