Tuesday, 3 February 2015

Using .net caching with Sitecore

Introduction


Sitecore has an extensive caching framework that allows it to cache everything from items in its database all the way to the html output of its renderings.  When using Sitecore's cache framework it employs a number of techniques to ensure that the cached data remains valid, and if you use .net's own caching framework this can sometimes conflict with Sitecore.  For example, let's say you have a menu control that appears on every page so you use .net's output caching to cache the html.  If a content editor changes something in Sitecore that affects your menu such as creating new elements, renaming titles etc, then your site is going to continue serving up the cached version which is no longer valid.  If you use Sitecore's own caching framework by setting cache parameters on the rendering then Sitecore will clear these cached items when a publish happens, ensuring that the cached html is rebuilt using the new data.  Furthermore, Sitecore will disable caching when you are in preview or edit mode so you know your data is always fresh.

As well as html caching, you might want to cache data structures you have built from Sitecore items.  Let's say you have a list of countries and in each country is a list of cities and you use this data often, so would like to cache it.  If you use .net caching and the underlying data in Sitecore changes, your cached data is now invalid.  You could set an expiry on the cache, but that's far from ideal as you don't want to tell your content editors that they have to wait before they see their changes go live.
In this article I am going to show you how to hook into the publishing mechanism such that your cached items are removed when a publish occurs.


Contents

Sitecore's cache management


If you examine the events in your web.confg file you'll see the following
<event name="publish:end">
   <handler type="Sitecore.Publishing.HtmlCacheClearer, Sitecore.Kernel" method="ClearCache">
      <sites hint="list">
         <site>website</site>
      </sites>
    </handler>
</event>
<event name="publish:end:remote">
   <handler type="Sitecore.Publishing.HtmlCacheClearer, Sitecore.Kernel" method="ClearCache">
      <sites hint="list">
         <site>website</site>
      </sites>
   </handler>
</event>
This means that every time a publish or a remote publish happens, Sitecore calls the ClearCache method of its HtmlCacheClearer class, which is responsible for clearing the cache.  It only does this on publish because when you are looking at items in preview or edit mode the cache isn't used, so we only care about invalidating the cache on the live site, and as items can only get onto the live site via a publish, this is the best place to do it.

The solution detailed below is going to utilise the same technique where we will remove the relevant items from the cache when a publish has occurred.

Tracking items to be cleared on publish


In order to identify which items are to be removed from the cache we are going to use cache dependencies.  A cache dependency is like a parent and child relationship where you specify that one item is dependent on another.  This solution involves creating an arbitrary cache key that we will make all of our cached items dependent on.

HttpRuntime.Cache.Add("mySitecoreItems",
    DateTime.Now,
    null,
    Cache.NoAbsoluteExpiration,
    Cache.NoSlidingExpiration,
    CacheItemPriority.Default,
    null);

This adds an item to the cache called "mySitecoreItems" and its value is DateTime.Now.  The value is pretty irrelevant but using the current time can be handy if you ever want to interrogate how long items have been in the cache.

To add an item to the cache that is dependent on this item we write code like so

 
CacheDependency cacheDependency =
    new CacheDependency(null, new[] { "mySitecoreItems" });

HttpRuntime.Cache.Insert("cacheKeyForMyObject",
    myObject, cacheDependency);
 
What makes this solution work is that now cacheKeyForMyObject is dependent on mySitecoreItems, if I update mySitecoreItems then cacheKeyForMyObject is removed from the cache.  If I make sure every item I add to the cache has the same dependency then updating mySitecoreItems will remove all of those dependent items from the cache.

Putting the code together


Here is a sample cache manager class that employs these techniques.

using System;
using System.Web;
using System.Web.Caching;
namespace MyNamespace
{
    public interface ICacheManager
    {
        T Get<T>(string key) where T : class;
        void Insert<T>(T item, string key);
        void Insert<T>(T item, string key, DateTime expirationTime);
        void Clear();
    }

    public class CacheManager : ICacheManager
    {
        private const string DependencyKey = "mySitecoreItems";
        static CacheManager()
        {
            HttpRuntime.Cache.Add(DependencyKey,
                DateTime.Now,
                null,
                Cache.NoAbsoluteExpiration,
                Cache.NoSlidingExpiration,
                CacheItemPriority.Default,
                null);
        }

        public T Get<T>(string key) where T : class
        {
            // never cache unless in normal mode
            if (!Sitecore.Context.PageMode.IsNormal)
            {
                return null;
            }
            return HttpRuntime.Cache[GetKey(key)] as T;
        }

        public void Insert<T>(T item, string key)
        {
            CacheDependency cacheDependency = new CacheDependency(null, new[] { DependencyKey });
            HttpRuntime.Cache.Insert(GetKey(key), item, cacheDependency);
        }

        public void Insert<T>(T item, string key, DateTime expirationTime)
        {
            CacheDependency cacheDependency = new CacheDependency(null, new[] { DependencyKey });
            HttpRuntime.Cache.Insert(GetKey(key), item, cacheDependency, expirationTime, Cache.NoSlidingExpiration);
        }

        public void Clear()
        {
            Cache c = HttpRuntime.Cache;
            if (c != null)
            {
                c.Insert(DependencyKey,
                    DateTime.Now,
                    null,
                    Cache.NoAbsoluteExpiration,
                    Cache.NoSlidingExpiration,
                    CacheItemPriority.Default,
                    null);
            }
        }

        private static string GetKey(string baseKey)
        {
            return string.Format("{0}_{1}_{2}", Sitecore.Context.Site.Name, Sitecore.Context.Database.Name, baseKey);
        }
    }
}

Some things to note are that the Get method always returns null if we are not in normal mode, so there is no risk of using cached data in preview or edit mode.  Also worth noting is that we append the current site name and database to the cache key; this means that if you have multiple sites served by the same Sitecore instance they won't get each other's items from the cache.  The reason for adding the database is so that you can use caching for preview and normal if you want to, as adding the database name ensures that preview (database: master) and normal (database: web) maintain their own caches.  The Clear method simply updates the master dependency item, and that will remove all of your items from the cache.

Example usage


public List<Item> GetCountries()
{
    ICacheManager cacheManager = new CacheManager();
    string key = "allcountries";
    List<Item> countries = cacheManager.Get<List<Item>>(key);
    if (countries != null)
    {
        return countries;
    }
    // code to build a list of countries
    cacheManager.Insert(countries, key);
    return countries;
}

Hooking into the publish events


Now we have our working cache manager all that is left is to hook it up to the publish events.  Create a class that can be used to clear our .net cache.

namespace MyNamespace
{
    public class ClearCache
    {
        public void Process(object sender, System.EventArgs args)
        {
            ICacheManager manager = new CacheManager();
            manager.Clear();
        }
    }
}

Create a new config file in the /app_config/includes folder called MyEvents.config to hold your configution.  Any config files in the include folder are merged with the web.config.  If you want to you can update the web.config directly to add your new handler, however patching using config files is considered best practice.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <events timingLevel="custom">
      <event name="publish:end">
        <handler type="MyNamespace.ClearCache, MyAssembly" method="Process"/>
      </event>
      <event name="publish:end:remote">
        <handler type="MyNamespace.ClearCache, MyAssembly" method="Process"/>
      </event>
    </events>
  </sitecore>
</configuration>


Summary


When caching objects it is important to consider Sitecore's built-in caching so there is no value in caching single items as Sitecore caches these already.  Custom caching can be considered if you have to do any work to build your data from Sitecore items; maybe your data is a representation of a tree section, maybe you have to do filtering to work out which items are relevant, maybe you have to navigate up the tree or do some other recursive processing.  In these situations it can be handy to cache the results so that the work involved in fetching the data does not have to be repeated, especially if it is commonly-accessed data.

No comments:

Post a Comment