Caching Service Calls – The Lazy Way


Through various blog posts and contributions by colleagues that I have mostly stolen, butchered, and maybe improved upon, I bring to you a simple Caching utility. DISCLAIMER: The title “Lazy” is meant only for amount of effort, not actual “lazy loading.”

First, the cache:

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Web;

namespace LeftyFTW.Utility
{
    /// <summary>
    /// Front-end to the <see cref="System.Web.Caching.Cache"/>
    /// that allows duplicate keys across different classes.
    /// Said differently, class Foo and class Bar may both use
    /// the key "FooBar" and still get unique storage in the cache.
    /// Provides for both absolute and sliding expiration.
    /// </summary>
    [SuppressMessage("Microsoft.StyleCop.CSharp.OrderingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "Style Cop Bug Workaround")]
    public static class Cache
    {
        #region Fields
        private const string Delimiter = ":";
        private const string DefaultExpiryKey = "DefaultSlidingExpiry";

        /// <summary>Default sliding expiration.</summary>
        /// <remarks>Note that this is overwritten in the static constructor</remarks>
        public static readonly TimeSpan DefaultSlidingExpiry = TimeSpan.FromMinutes(10);

        public static readonly string PREFIX = typeof(Cache).FullName + Delimiter + Delimiter;

        private static readonly System.Reflection.MethodInfo PrependKeyMetod = null;

        private static readonly object LOCK = new object();

        #endregion Fields

        #region Constructor
        static Cache()
        {
            PrependKeyMetod = (from mi in typeof(Cache).GetMethods(BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)
                               where mi.Name == "PrependKey" && mi.IsGenericMethod
                               select mi).First();

            string defaultExpirySetting = System.Configuration.ConfigurationManager.AppSettings[DefaultExpiryKey];
            int defaultExpiry = 0;
            if (!string.IsNullOrEmpty(defaultExpirySetting) && int.TryParse(defaultExpirySetting, out defaultExpiry))
            {
                Cache.DefaultSlidingExpiry = TimeSpan.FromMinutes(defaultExpiry);
            }
        }
        #endregion

        #region Public Properties
        public static DateTime DefaultFixedCacheTime
        {
            get
            {
                return DateTime.Now.AddSeconds(90);
            }
        }
        #endregion

        #region Public Methods

        /// <summary>
        /// Gets an item from the cache.
        /// </summary>
        /// <typeparam name="T">Type of item to retrieve</typeparam>
        /// <param name="key">Key of the item to retrieve</param>
        /// <returns>Item from the cache</returns>
        public static T GetFromCache<T>(string key) where T : class
        {
            if (HttpContext.Current == null)
            {
                return null;
            }

            return HttpRuntime.Cache.Get(PrependKey<T>(key)) as T;
        }

        /// <summary>
        /// Determines if something is in the cache.
        /// </summary>
        /// <typeparam name="T">Type of data in the cache to check on</typeparam>
        /// <param name="key">Key of the item to check</param>
        /// <returns>Flag indicating if the item is in the cache or not</returns>
        public static bool IsInCache<T>(string key) where T : class
        {
            if (HttpContext.Current == null)
            {
                return false;
            }

            // If we're disabling the cache, return false.
            if (!string.IsNullOrEmpty(HttpContext.Current.Request["disableCache"]))
            {
                return false;
            }

            // Otherwise, just ask the cache.
            return GetFromCache<T>(key) != null; //HttpRuntime.Cache.Get(PrependKey<T>(key)) != null;
        }

        /// <summary>
        /// Places an item into the cache using absolute expiration.
        /// </summary>
        /// <param name="key">Key of the item being inserted</param>
        /// <param name="item">Item to insert</param>
        /// <param name="expireTime">Absolute expiration time of the item</param>
        public static void InsertIntoCacheAbsoluteExpiration(string key, object item, DateTime expireTime)
        {
            if (HttpContext.Current == null)
            {
                return;
            }

            lock (LOCK)
            {
                key = PrependKey(key, item);
                HttpRuntime.Cache.Remove(key);
                HttpRuntime.Cache.Add(
                    key,
                    item,
                    null,
                    expireTime,
                    System.Web.Caching.Cache.NoSlidingExpiration,
                    System.Web.Caching.CacheItemPriority.Normal,
                    null);
            }
        }

        /// <summary>
        /// Places an item into the cache using sliding expiration.
        /// </summary>
        /// <param name="key">Key of the item being inserted</param>
        /// <param name="item">Item to insert</param>
        /// <param name="slidingExpiry">
        /// Sliding expiration timeout.  This is the amount of time 
        /// SINCE LAST ACCESS that will trigger a removal from the cache.</param>
        public static void InsertIntoCacheSlidingExpiration(string key, object item, TimeSpan slidingExpiry)
        {
            if (HttpContext.Current == null)
            {
                return;
            }

            lock (LOCK)
            {
                key = PrependKey(key, item);
                HttpRuntime.Cache.Remove(key);
                HttpRuntime.Cache.Add(
                    key,
                    item,
                    null,
                    System.Web.Caching.Cache.NoAbsoluteExpiration,
                    slidingExpiry,
                    System.Web.Caching.CacheItemPriority.Normal,
                    null);
            }
        }

        /// <summary>
        /// Removes an object from the cache.
        /// </summary>
        /// <typeparam name="T">Type of object to remove from the cache</typeparam>
        /// <param name="key">Key to remove</param>
        public static void RemoveObject<T>(string key)
        {
            if (HttpContext.Current == null)
            {
                return;
            }

            lock (LOCK)
            {
                HttpRuntime.Cache.Remove(PrependKey<T>(key));
            }
        }

        /// <summary>
        /// Removes an object from the cache.
        /// </summary>
        /// <param name="typeName">Fully qualified type of object to remove from the cache</param>
        /// <param name="key">Key to remove</param>
        public static void RemoveObject(string typeName, string key)
        {
            if (HttpContext.Current == null)
            {
                return;
            }

            lock (LOCK)
            {
                HttpRuntime.Cache.Remove(PrependKey(typeName, key));
            }
        }

        /// <summary>
        /// Removes all objects in the cache of the specified type.
        /// </summary>
        /// <typeparam name="T">Type of objects to remove</typeparam>
        public static void RemoveAllObjects<T>()
        {
            if (HttpContext.Current == null)
            {
                return;
            }

            lock (LOCK)
            {
                RemoveMatchedPrefixes(PrefixForType<T>());
            }
        }

        /// <summary>
        /// Removes all objects in the cache of the specified type.
        /// </summary>
        /// <param name="typeName">Fully qualified type name</param>
        public static void RemoveAllObjects(string typeName)
        {
            if (HttpContext.Current == null)
            {
                return;
            }

            lock (LOCK)
            {
                RemoveMatchedPrefixes(PrefixForType(typeName));
            }
        }

        /// <summary>
        /// Removes all objects in the cache.
        /// </summary>
        public static void RemoveAllObjects()
        {
            if (HttpContext.Current == null)
            {
                return;
            }

            lock (LOCK)
            {
                RemoveMatchedPrefixes(PREFIX);
            }
        }

        /// <summary>
        /// Gets all the existing keys for a given type.
        /// </summary>
        /// <typeparam name="T">Type to get keys for</typeparam>
        /// <returns>Array of keys for the given type</returns>
        public static string[] GetKeysForType<T>()
        {
            if (HttpContext.Current == null)
            {
                return new string[0];
            }

            return GetKeysForType(typeof(T).FullName);
        }

        /// <summary>
        /// Gets all the existing keys for a given type.
        /// </summary>
        /// <param name="typeName">Type to get keys for</param>
        /// <returns>Array of keys for the given type</returns>
        public static string[] GetKeysForType(string typeName)
        {
            if (HttpContext.Current == null)
            {
                return new string[0];
            }

            var retVal = new List<string>();
            var en = HttpRuntime.Cache.GetEnumerator();
            string prefix = PrefixForType(typeName);

            while (en.MoveNext())
            {
                string key = (string)en.Key;
                if (key.StartsWith(prefix))
                {
                    retVal.Add(key.Substring(prefix.Length));
                }
            }

            return retVal.ToArray();
        }

        /// <summary>
        /// Gets all the types currently stored in the cache.
        /// </summary>
        /// <returns>All types currently in the cache.</returns>
        public static string[] GetAllStoredTypes()
        {
            if (HttpContext.Current == null)
            {
                return new string[0];
            }

            var retVal = new List<string>();
            var en = HttpRuntime.Cache.GetEnumerator();

            while (en.MoveNext())
            {
                string key = (string)en.Key;
                if (key.StartsWith(PREFIX))
                {
                    int delimiterLocation = key.IndexOf(Delimiter, PREFIX.Length);
                    retVal.Add(key.Substring(PREFIX.Length, delimiterLocation - PREFIX.Length));
                }
            }

            return retVal.Distinct().ToArray();
        }

        /// <summary>
        /// Counts the number of objects in the cache of a given type.
        /// </summary>
        /// <typeparam name="T">Type to get count of</typeparam>
        /// <returns>Total number of objects of the specified type in the cache</returns>
        public static int Count<T>()
        {
            return Count(PrefixForType<T>());
        }

        /// <summary>
        /// Counts the number of objects in the cache.
        /// </summary>
        /// <returns>Total number of objects in the cache</returns>
        public static int Count()
        {
            return Count(PREFIX);
        }

        /// <summary>
        /// Counts all the itmes with the provided prefix
        /// </summary>
        /// <param name="prefix">Prefix</param>
        /// <returns>Count</returns>
        public static int Count(string prefix)
        {
            if (HttpContext.Current == null)
            {
                return 0;
            }

            var e = HttpRuntime.Cache.GetEnumerator();
            int retVal = 0;

            while (e.MoveNext())
            {
                string key = (string)e.Key;
                if (key.StartsWith(prefix))
                {
                    ++retVal;
                }
            }

            return retVal;
        }
        #endregion Public Methods

        #region Private Methods
        private static string PrependKey(string typeName, string key)
        {
            return PrefixForType(typeName) + key;
        }

        private static string PrependKey(string key, object item)
        {
            // Change PrependKey(key) to PrependKey<item.GetType()>(key)
            MethodInfo prependKey = PrependKeyMetod.MakeGenericMethod(item.GetType());
            // Call the PrependKey method and return the result
            return prependKey.Invoke(null, new object[] { key }) as string;
        }

        private static string PrependKey<T>(string key)
        {
            // Sample, for type SO.Product with key "foo":
            // Ntelos.Web.Commerce.Common.SimpleObjects.Cache::Ntelos.Web.Commerce.Common.SimpleObjects.Product:foo"
            return PrefixForType<T>() + key;
        }

        private static string PrefixForType<T>()
        {
            return PrefixForType(typeof(T).FullName);
        }

        private static string PrefixForType(string typeName)
        {
            // Sample, for type SO.Product:
            // Ntelos.Web.Commerce.Common.SimpleObjects.Cache::Ntelos.Web.Commerce.Common.SimpleObjects.Product:"
            return string.Format("{0}{1}{2}",
                PREFIX,
                typeName,
                Delimiter);
        }

        private static void RemoveMatchedPrefixes(string prefix)
        {
            var e = HttpRuntime.Cache.GetEnumerator();
            var toRemove = new System.Collections.Generic.List<string>();

            while (e.MoveNext())
            {
                string key = (string)e.Key;
                if (key.StartsWith(prefix))
                {
                    toRemove.Add(key);
                }
            }

            foreach (string key in toRemove)
            {
                HttpRuntime.Cache.Remove(key);
            }
        }
        #endregion Private Methods
    }
}

This basic utility that I now use on all SharePoint 2010 projects provides a very creative way to cache data that needs to be accessed frequently, and most likely by multiple parties. I’ll save any deep explanation of the code as it is fairly simple, but we will soldier on and show it’s utility.

Service Proxy:

using System;
using System.ServiceModel;
using Microsoft.SharePoint.Utilities;

namespace LeftyFTW.Utility
{
    /// <summary>
    /// Proxy for executing generic service methods
    /// </summary>
    public class ServiceProxy
    {
        /// <summary>
        /// Execute service method and get return value
        /// </summary>
        /// <typeparam name="C">Type of service</typeparam>
        /// <typeparam name="T">Type of return value</typeparam>
        /// <param name="action">Delegate for implementing the service method</param>
        /// <param name="cacheKey">Primary key for return value/type pair</param>
        /// <returns>Object of type T</returns>
        public static T ExecuteCached<C, T>(Func<C, T> action, string cacheKey) where C : class, ICommunicationObject, new()
                                                                                where T : class
        {
            if (!string.IsNullOrEmpty(cacheKey) && Cache.IsInCache<T>(cacheKey))
            {
                return Cache.GetFromCache<T>(cacheKey);
            }

            T result = Execute<C, T>(action);

            if (result != null && !string.IsNullOrEmpty(cacheKey))
            {
                Cache.InsertIntoCacheSlidingExpiration(cacheKey, result, Cache.DefaultSlidingExpiry);
            }

            return result;            
        }

        /// <summary>
        /// Execute service method and get return value
        /// </summary>
        /// <typeparam name="C">Type of service</typeparam>
        /// <typeparam name="T">Type of return value</typeparam>
        /// <param name="action">Delegate for implementing the service method</param>
        /// <param name="cacheKey">Primary key for return value/type pair</param>
        /// <param name="cacheExpirationTime">When items should be cleared from cache</param>
        /// <returns>Object of type T</returns>
        public static T ExecuteCached<C, T>(Func<C, T> action, string cacheKey, DateTime cacheExpirationTime)
            where C : class, ICommunicationObject, new()
            where T : class
        {
            if (!string.IsNullOrEmpty(cacheKey) && Cache.IsInCache<T>(cacheKey))
            {
                return Cache.GetFromCache<T>(cacheKey);
            }

            T result = Execute<C, T>(action);

            if (result != null && !string.IsNullOrEmpty(cacheKey))
            {
                Cache.InsertIntoCacheAbsoluteExpiration(cacheKey, result, cacheExpirationTime);
            }

            return result;
        }

        /// <summary>
        /// Execute service method and get return value
        /// </summary>
        /// <typeparam name="C">Type of service</typeparam>
        /// <typeparam name="T">Type of return value</typeparam>
        /// <param name="action">Delegate for implementing the service method</param>
        /// <returns>Object of type T</returns>
        public static T Execute<C, T>(Func<C, T> action) where C : class, ICommunicationObject, new()
        {
            using (new SPMonitoredScope(string.Format("Proxying Service Call {0}.{1}", typeof(C).Name, action.Method.Name)))
            {
                C svc = null;

                T result = default(T);

                try
                {
                    svc = new C();

                    result = action.Invoke(svc);

                    svc.Close();
                }
                catch (FaultException ex)
                {
                    //Log this... and abort
                    if (svc != null)
                    {
                        svc.Abort();
                    }

                    throw;
                }
                catch (Exception ex)
                {
                    //Log this.... abort
                    if (svc != null)
                    {
                        svc.Abort();
                    }

                    throw;
                }

                return result;
            }
        }

        /// <summary>
        /// Execute service method with no return value
        /// </summary>
        /// <typeparam name="C">Type of service</typeparam>
        /// <param name="action">Delegate for implementing the service method</param>
        public static void Execute<C>(Action<C> action) where C : class, ICommunicationObject, new()
        {
            C svc = null;

            try
            {
                svc = new C();

                action.Invoke(svc);

                svc.Close();
            }
            catch (FaultException ex)
            {
                //Log this... something bad happened at the service
                throw;
            }
            catch (Exception ex)
            {
                //Log this... something bad happened while calling the service
                svc.Abort();

                throw;
            }
        }
    }
}

And now, for the pièce de résistance, making a service call:

        public static List FindSomeone(string criteria)
        {
            ServiceProxy.ExecuteCached<PeopleServiceClient, List<People>>
            (
                svc => svc.FindSomeone(criteria),
                criteria
            );
        }

What we have here is an abstracted, self-cleaning service call that will cache the results based on return type and arguments for a  sliding cache.  If an absolute cache is required, the call could be:

        public static List FindSomeone(string criteria)
        {
            ServiceProxy.ExecuteCached<PeopleServiceClient, List<People>>
            (
                svc => svc.FindSomeone(criteria),
                criteria,
                DateTime.Now.AddMinutes(90)
            );
        }

Happy coding!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s