Saturday, June 13, 2009

ASP.NET Data Caching Helper Extension Method

Sometimes ASP.NET output caching doesn’t do the trick and you need to implement data caching to avoid trips to the database or to external API’s.  Data caching is pretty simple- check the cache via a cache key and if the data doesn’t exist, get it and store it in the cache.  The code you’ll write to implement that pattern is simple, but instead of copy and pasting each time you need to use cache, why not write an extension method to help out?

Here’s the typical pattern to implement data caching

// get from cache
var items = context.Cache[cacheKey] as IEnumerable<Foo>;
if (items == null)
{
    // get from database
    items = GetData();

    // insert into cache
   context.Cache.Insert(cacheKey, items, null, DateTime.Now.AddSeconds(cacheSeconds), 
Cache
.NoSlidingExpiration); }

Here’s data caching with my helper extension method

// get from cache
var items = context.Cache.Data(cacheKey, cacheSeconds, () =>
{
    return GetData();
});

Data caching extension method source

I wrote a simple extension method that caches the return value of a method (you can use a lambda expression here).  It prepends the hash code of that method to the cache key to isolate a method’s set of cached data.  I wrote this to be thread safe, although you’ll need to make sure your method is thread safe as well.

namespace System.Web.Caching
{
    public static class CacheExtensions
    {
        static object sync = new object();

        /// <summary>
        /// Executes a method and stores the result in cache using the given cache key.  
If the data already exists in cache, it returns the data
/// and doesn't execute the method. Thread safe, although the method parameter
isn't guaranteed to be thread safe.
/// </summary> /// <typeparam name="T"></typeparam> /// <param name="cache"></param> /// <param name="cacheKey">Each method has it's own isolated set of cache items,
so cacheKeys won't overlap accross methods.
</param> /// <param name="method"></param> /// <param name="expirationSeconds">Lifetime of cache items, in seconds</param> /// <returns></returns> public static T Data<T>(this Cache cache, string cacheKey,
int expirationSeconds, Func<T> method) { var hash = method.GetHashCode().ToString(); var data = (T)cache[hash + cacheKey]; if (data == null) { data = method(); if (expirationSeconds > 0 && data != null) lock (sync) { cache.Insert(hash + cacheKey, data, null, DateTime.Now.AddSeconds
(expirationSeconds), Cache.NoSlidingExpiration); } } return data; } } }

3 comments:

  1. Looks good! I was just sitting down to write a similar method and you have already taken care of it. Great Work!

    ReplyDelete
  2. This is great but could you tell me how I would go about using the same approach where my GetData() method takes parameters?

    I know that I'd need to change the cache key to include any parameters and also pass any parameters when invoking the method but I'm just not sure how as I'm new to lambda, delegates, etc.

    ReplyDelete
  3. As long as the lambda expression is executed on the same thread, you can access any variables in scope. So this should work:

    var parameter = "hello";
    var items = context.Cache.Data(cacheKey, cacheSeconds, () =>
    {
    return GetData(parameter);
    });

    ReplyDelete