Custom Caching Done Fluently

September 3 2010 3 comments

SharePoint includes many out-of-the-box cache mechanisms that can be configured on site and site collection levels. It is not uncommon, however, that you want to make some custom caching in code. Often you simply need more fine-grained control over what is cached and exactly what parameters define the cache key. One scenario where cache profiles are insufficient is if you want to filter content based on user profile information, such as business unit (varying cache by effective rights can be accomplished using cache profiles).

HttpRuntime.Cache provides the access point for standard ASP.NET caching. I wanted to provide a more elegant and easy-to-use interface for caching, although the actual implementation would still rely on HttpRuntime.Cache. I first considered an AOP approach so that cache rules could be defined using attributes and applied with interceptors (there should be several implementations out there already). But that approach would have required some structural changes to existing codebase and including Castle DynamicProxy assemblies in our projects. Not a bad idea though, but I was looking for a leaner approach.

I love the Fluent Interface pattern because it usually results in beautifully readable and compact code. What I wanted to achieve was something like this:

var cachedObject = FluentCache.Cache(() => LoadSomethingFromDatabase())
                              .By(CacheKey1, CacheKey2)
                              .ByCurrentUser()
                              .For(TimeSpan.FromMinutes(1))
                              .CachedObject;

And that’s it, no checking whether the object is already cached, no casting, no concatenating the cache key etc. The idea is that the CachedObject property reads the object from cache if it’s found there, otherwise the loader delegate (in this case, the LoadSomethingFromDatabase method) gets called and the result is saved to cache. Here is the implementation:

namespace SharePoint.Accelerator.Common
{
    using System;
    using System.Collections.Generic;
    using System.Web.Caching;
    using System.Web;
    using Microsoft.SharePoint;
    using System.Linq;

    /// <summary>
    /// Represents a cached object.
    /// </summary>
    /// <typeparam name="T">Type of the cached object</typeparam>
    public interface ICachedObject<T>
    {
        /// <summary>
        /// Adds new keys that identify the cached object.
        /// </summary>
        /// <param name="cacheKeys">The cache keys</param>
        /// <returns>Fluent interface for further configuring the cache</returns>
        ICachedObject<T> By(params string[] cacheKeys);
        /// <summary>
        /// Specifies that the cache should be keyed by the current user name.
        /// </summary>
        /// <returns>Fluent interface for further configuring the cache</returns>
        ICachedObject<T> ByCurrentUser();
        /// <summary>
        /// Specifies that the cache should be keyed by SPContext.Current.Item.
        /// </summary>
        /// <returns>Fluent interface for further configuring the cache</returns>
        ICachedObject<T> ByCurrentItem();
        /// <summary>
        /// Specifies that the cache should be keyed by  SPContext.Current.Web.
        /// </summary>
        /// <returns>Fluent interface for further configuring the cache</returns>
        ICachedObject<T> ByCurrentWeb();
        /// <summary>
        /// Specifies the absolute duration of the cache.
        /// </summary>
        /// <returns>Fluent interface for further configuring the cache</returns>
        ICachedObject<T> For(TimeSpan cacheTime);
        /// <summary>
        /// Specifies the sliding duration of the cache, i.e., the time after which the cache will be removed
        /// if it has not been accessed.
        /// </summary>
        /// <returns>Fluent interface for further configuring the cache</returns>
        ICachedObject<T> ForSliding(TimeSpan cacheTime);
        /// <summary>
        /// Specifies the cache priority.
        /// </summary>
        /// <seealso cref="CacheItemPriority"/>
        /// <returns>Fluent interface for further configuring the cache</returns>
        ICachedObject<T> Priority(CacheItemPriority cachePriority);

        /// <summary>
        /// Retrieves the cached object. If found from cache (by the key) then the cached object is returned.
        /// Otherwise, the cachedObjectLoader is called to load the object, and it is then added to cache.
        /// </summary>
        T CachedObject { get; }

        /// <summary>
        /// Returns the cache key for the current cached object.
        /// </summary>
        string CacheKey { get; }
    }


    /// <summary>
    /// Usage:
    /// <code>var cachedPages = FluentCache.Cache(() => LoadPages())
    ///                   .ByCurrentUser()
    ///                   .By(GetCurrentUserBusinessUnit())
    ///                   .By(cacheToken1, cacheToken2)
    ///                   .For(TimeSpan.FromMinutes(1))
    ///                   .CachedObject;
    /// </code>
    /// </summary>
    public static class FluentCache
    {
        /// <summary>
        /// Cache results of <paramref name="cachedObjectLoader"/> into HttpRuntime.Cache.
        /// </summary>
        /// <typeparam name="T">Type of the object being cached</typeparam>
        /// <param name="cachedObjectLoader">Code that loads the to-be-cached object</param>
        /// <returns>Fluent interface for configuring the cache</returns>
        public static ICachedObject<T> Cache<T>(Func<T> cachedObjectLoader)
        {
            return new CachedObjectImpl<T>(cachedObjectLoader);
        }


        private class CachedObjectImpl<T> : ICachedObject<T>
        {
            Func<T> loader;
            List<string> keys = new List<string>();
            CacheItemPriority priority = CacheItemPriority.Normal;
            DateTime absoluteExpiration = System.Web.Caching.Cache.NoAbsoluteExpiration;
            TimeSpan slidingExpiration = System.Web.Caching.Cache.NoSlidingExpiration;

            public T CachedObject
            {
                get { return GetObject(); }
            }

            private T GetObject()
            {
                var key = this.CacheKey;

                // try to get the query result from the cache
                var result = (T)HttpRuntime.Cache.Get(key);

                if (result == null)
                {
                    result = loader();

                    HttpRuntime.Cache.Insert(
                        key,
                        result,
                        null, // no cache dependency
                        absoluteExpiration,
                        slidingExpiration,
                        priority,
                        null); // no removal notification
                }

                return result;
            }
            public CachedObjectImpl(Func<T> cached)
            {
                loader = cached;
                By(typeof(T).FullName);
            }


            public ICachedObject<T> By(params string[] cacheKeys)
            {
                keys.AddRange(cacheKeys);
                return this;
            }
            public ICachedObject<T> ByCurrentUser()
            {
                if (SPContext.Current == null) return this;
                return By(SPContext.Current.Web.CurrentUser.Name);
            }
            public ICachedObject<T> ByCurrentItem()
            {
                if (SPContext.Current == null) return this;
                return By(SPContext.Current.Item.ID.ToString());
            }

            public ICachedObject<T> ByCurrentWeb()
            {
                if (SPContext.Current == null) return this;
                return By(SPContext.Current.Web.Url);
            }

            public ICachedObject<T> For(TimeSpan cacheTime)
            {
                absoluteExpiration = DateTime.UtcNow.Add(cacheTime);
                slidingExpiration = System.Web.Caching.Cache.NoSlidingExpiration;
                return this;
            }
            public ICachedObject<T> ForSliding(TimeSpan cacheTime)
            {
                slidingExpiration = cacheTime;
                absoluteExpiration = System.Web.Caching.Cache.NoAbsoluteExpiration;
                return this;
            }

            public ICachedObject<T> Priority(CacheItemPriority cachePriority)
            {
                priority = cachePriority;
                return this;
            }

            public string CacheKey
            {
                get { return string.Join("_", keys.OrderBy(x => x).ToArray()); }
            }

        }

    }

}

The code works at least with SharePoint 2010 (.NET 3.5 required), although there is really nothing SharePoint-specific except the ByCurrentUser, ByCurrentWeb and ByCurrentItem methods. Any comments or improvements are more than welcome!

Popularity: 1% [?]

3 comments to “Custom Caching Done Fluently”

  1. drian says:

    Brilliant.. thanks.

  2. Ciaran Colgan says:

    Excellent article – great idea, execution and very clearly and concisely written. Good stuff!

  3. Arnab says:

    Can we remove the cache from the server manually, or do we have explicitly remove using the Remove function

Leave a Reply