Server Side Caching using System.Runtime.Caching

Data elements such as location details or item prices are not subject to change on a transaction basis which means that they do not change frequently. If this kind of data is required for view and controller renderings in Sitecore, being able to get the data locally enhances web site performance manifold as there is no need to go to the external services or databases that house this data on every request.

One of the ways to achieve this performance gain is to identify and duplicate the needed data in Sitecore itself and reach out to Sitecore to get the same when needed - putting into effect Sitecore's inherent caching capabilities when fetching this data. However, this leads to 2 sources of truth, necessitating updates to the data in the real source as well as Sitecore.

Another way is to use server side caching - where the capabilities offered by the .Net assembly System.Runtime.Caching are utilized to create in memory caches that can store and retrieve custom objects. This is best suited to controller renderings that need externally sourced data to process information before rendering. This method depends on the Content Management and Content Delivery servers having sufficient memory to support the in-memory caches and also strength in terms of CPU. Depending on the available infrastructure and the type and size of the data you wish to store in the in-memory cache this may or may not be an issue.

In this blog post we will look at using System.Runtime.Caching to achieve server side caching for custom objects.

Models involved in the caching sample

In the sample application that I have developed, we will be dealing with a simple class whose objects we will be storing in the cache. This is the 'Person' class as shown below:

[sourcecode language="csharp"]

public class Person{public string FirstName { get; set; }

public string LastName { get; set; }

public string PhoneNumber { get; set; }}

[/sourcecode]

I will also be using a generic 'CacheResult' class to hold the value obtained from the cache and any errors that we will need to report while retreiving data from the cache as below:

[sourcecode language="csharp"]

public class CacheResult<TCachedModel>{public TCachedModel CacheValue { get; set; }

public CacheErrorType CacheError { get; set; }}

[/sourcecode]

'CacheErrorType' is an enumeration as follows:

[sourcecode language="csharp"]

public enum CacheErrorType{NoError,

ItemNotFoundInCache,

CacheDoesNotExist,

CacheKeyCouldNotBeConstructed}

[/sourcecode]

Setting up the cache

Setting up the System.Runtime.Caching.MemoryCache is as simple as creating a Singleton instance of the MemoryCache class. The constructor of the 'MemoryCache' class takes a NameValueCollection which contains the names and values of certain configurations that govern the working of the in-memory cache. Chief among them are the following:

  • cacheMemoryLimitMegabytes - The limit of the amount of memory the cache can use on the server
  • physicalMemoryLimitPercentage - The percentage of server memory to use
  • pollingInterval - The maximum time that can occur before memory statistics are updated

The values that one assigns to the these configurations vary by need and is a cache design time consideration. In the sample application the values are 15 MB, 10 MB and "02:00:00" respectively.

Another typical configuration that is made available (albeit for later use) is the individual cache item level configuration of timeout for the item. This is set through the 'AbsoluteExpiration' property of the class 'CacheItemPolicy'. Another Singleton property 'PersonCacheItemPolicy' in the sample application exposes this item level configuration. This property can be used when adding an item to the already created and configured cache.

How to do it in Sitecore?

One of the ways to instantiate the Singleton instance of System.Runtime.Caching.MemoryCache is to have it instantiated and configured in the 'initialize' pipeline of Sitecore. This way the instance is available to Sitecore and the logic in the controller renderings before any renderings need to add to or read from the cache.

The initialization and configuration of the cache is as depicted in the class below:

[sourcecode language="csharp"]

public class InitializePersonCache{// The limit of the amount of memory the cache can use on the serverprivate const string ConstCacheMemoryLimitMegabytes = "cacheMemoryLimitMegabytes";// Percentage of server memory to useprivate const string ConstPhysicalMemoryLimitPercentage = "physicalMemoryLimitPercentage";// The maximum time that can occur before memory statistics are updated.private const string ConstPollingInterval = "pollingInterval";

public static MemoryCache PersonCache;

public static CacheItemPolicy PersonCacheItemPolicy;

public void Initialize(){if (PersonCache == null){PersonCache = new MemoryCache("PersonCache", GetCacheConfig());PersonCacheItemPolicy = GetCacheItemPolicy();}else{MessageBox.Show("The cache is already set up and configured!");}}

private NameValueCollection GetCacheConfig(){NameValueCollection config = new NameValueCollection{{ ConstCacheMemoryLimitMegabytes, ConfigurationManager.AppSettings.Get("PersonCacheMemoryLimitMegabytes") },{ ConstPhysicalMemoryLimitPercentage, ConfigurationManager.AppSettings.Get("PersonCachePhysicalMemoryLimitPercentage") },{ ConstPollingInterval, ConfigurationManager.AppSettings.Get("PersonCachePollingInterval") }};

return config;}

private CacheItemPolicy GetCacheItemPolicy(){var cacheDefaultTimeout = Convert.ToInt32(ConfigurationManager.AppSettings.Get("PersonCacheTimeout"));CacheItemPolicy cachePolicy = new CacheItemPolicy{AbsoluteExpiration = ConvertIntToMinDateTimeOffSet(cacheDefaultTimeout)};

return cachePolicy;}

private static DateTimeOffset ConvertIntToMinDateTimeOffSet(int cacheExpiryIntervalInMinute){return new DateTimeOffset(DateTime.Now.AddMinutes(cacheExpiryIntervalInMinute));}}

[/sourcecode]

In the sample application the instantiation and configuration has been called on the manual click of a button.

Code infrastructure to interact with the in-memory cache

Typically one needs to read from and add to the cache. Depending on the situation and requirements there might also be a need to update a certain element in the cache. I recommend putting in place an interface based approach to enable these interactions with the cache. This allows the application to not depend directly on the System.Runtime.Caching.MemoryCache but on objects that implement certain contracts and guarantee a result. The biggest advantage of this is that one can easily replace the cache being used - today it is System.Runtime.Caching.MemoryCache tomorrow it may well be AppFabric caching - as long as the application gets the desired result from the cache.

With the above concept in mind the sample application contains the following interfaces:

[sourcecode language="csharp"]

public interface ICacheKeyConstructor<TInRequest, TOutKey>{TOutKey ConstructCacheKey(TInRequest incomingRequest);}

public interface IAddCacheItemAdapter<Tvalue>{bool Execute(Tvalue cacheItem);}

public interface IAddCacheItemAdapter<TKey, Tvalue>{bool Execute(TKey requestItem, Tvalue cacheItem);}

public interface IGetCacheItemAdapter<Tkey, Tvalue>{Tvalue Execute(Tkey cacheKey);}

[/sourcecode]

In the following sections we will look at using these interfaces to interact with the cache that has already been instantiated and configured.

Adding items to the cache

Adding items to the cache includes 2 main ideas:

  1. Generation of a unique that is able to identify a item accurately.
  2. Implementation of the interface IAddCacheItemAdapter<Tvalue>

Typically, the approach used to construct a key is to use an element or combination of elements within the item itself that can identify the item uniquely. In our sample I am using the combination of the First and Last Name of the Person object. This is sufficient for our controlled code but may well be insufficient for other, more complex cases. In a situation where we are storing prices for products in the cache an example key could be: Country Code + State Code + Item SKU - a bottle of soda in the US in California is different from the same bottle in Minnesota. This is a critical aspect of caching items and must be thought out carefully.

Another thing to keep in mind while determining an algorithm to generate a key is that all parts of the program should generate the key for a given type of item in exactly the same manner. This ensures that the entire application identifies the same item in the same way - increasing the chances of one part of the application successfully getting an item from cache which was stored in the cache by a different part of the application. Once again, going the interface driven route and having a single implementation for a particular item's key generation helps tremendously. If different parts of the application generate keys for the same item in different ways the chances of hitting the cache are lower - ultimately watering down all the performance benefits that we set out to achieve.

In our sample we have implemented a cache key generator for the Person object as follows:

[sourcecode language="csharp"]

public class PersonCacheKeyConstructor : ICacheKeyConstructor&lt;Person, string&gt;{// The key for the Person Cache is simple - the concatenated First Name and Last Namepublic string ConstructCacheKey(Person incomingRequest){var personCacheKey = string.Empty;

if (incomingRequest != null &amp;&amp;!string.IsNullOrEmpty(incomingRequest.FirstName) &amp;&amp;!string.IsNullOrEmpty(incomingRequest.LastName)){personCacheKey = string.Concat(incomingRequest.FirstName.Trim(), incomingRequest.LastName.Trim());}

return personCacheKey;}}

[/sourcecode]

In the process of adding a Person to the cache, one then uses the PersonCacheKeyConstructor to contruct the cache key for the incoming Person. If the key is not empty, the Person can be added to the cache as follows:

[sourcecode language="csharp"]

public class AddPersonToCache : IAddCacheItemAdapter&lt;Person&gt;{public bool Execute(Person cacheItem){ICacheKeyConstructor&lt;Person, string&gt; personCacheKeyConstructor = new PersonCacheKeyConstructor();var cacheKey = personCacheKeyConstructor.ConstructCacheKey(cacheItem);

if (!string.IsNullOrEmpty(cacheKey)){var personCacheItem = new CacheItem(cacheKey, cacheItem);if (InitializePersonCache.PersonCache != null){try{InitializePersonCache.PersonCache.Add(personCacheItem, InitializePersonCache.PersonCacheItemPolicy);return true;}catch{return false;}}else{return false;}}else{return false;}}}

[/sourcecode]

Note the use of the static property PersonCacheItemPolicy on the InitializePersonCache class that is used while adding the Person to the cache. This property governs the life of the Person item in the cache.

The code that orchestrates the addition of the Person item in the cache is as follows:

[sourcecode language="csharp"]

private void btnAddToCache_Click(object sender, EventArgs e){if (InitializePersonCache.PersonCache != null){// Construct the Person itemif (!string.IsNullOrEmpty(txtFirstName.Text) &amp;&amp;!string.IsNullOrEmpty(txtLastName.Text) &amp;&amp;!string.IsNullOrEmpty(txtPhoneNumber.Text)){var personToAdd = new Person{FirstName = txtFirstName.Text.Trim(),LastName = txtLastName.Text.Trim(),PhoneNumber = txtPhoneNumber.Text.Trim()};

IAddCacheItemAdapter&lt;Person&gt; addPersonToCacheAdapter = new AddPersonToCache();if (addPersonToCacheAdapter.Execute(personToAdd)){MessageBox.Show(&quot;Person added successfully to cache!&quot;);}}else{MessageBox.Show(&quot;First Name, Last Name and Phone Number are all required!&quot;);}}else{MessageBox.Show(&quot;Person cache has not been set up! Please set up and configure the cache first.&quot;);}}

[/sourcecode]

Typically the code to add an item to the cache is called in when the application fails to find a requested item in the cache in the first place. If the item is not found in the cache, then it should be fetched from the real source of the data and then added to the cache before being returned to the caller. In this way, the next time the item is searched it will be successfully found in the cache.

Getting items from the cache

In order to get an item from the cache, one needs to implement the interface IGetCacheItemAdapter<string, CacheResult<Person>> as follows:

[sourcecode language="csharp"]

public class GetPersonFromCache : IGetCacheItemAdapter&lt;string, CacheResult&lt;Person&gt;&gt;{public CacheResult&lt;Person&gt; Execute(string cacheKey){var personFromCache = new CacheResult&lt;Person&gt;();

if (InitializePersonCache.PersonCache != null){try{// Use the key to get the Person from CachepersonFromCache.CacheValue = InitializePersonCache.PersonCache.Get(cacheKey) as Person;if (personFromCache.CacheValue == null){personFromCache.CacheError = CacheErrorType.ItemNotFoundInCache;}else{personFromCache.CacheError = CacheErrorType.NoError;}}catch{personFromCache.CacheValue = null;personFromCache.CacheError = CacheErrorType.ItemNotFoundInCache;}}else{personFromCache.CacheValue = null;personFromCache.CacheError = CacheErrorType.CacheDoesNotExist;}

return personFromCache;}}

[/sourcecode]

Note how the CacheResult generic class has been instantiated for the Person type and how the code passes back the CacheErrorType if any.

The code that orchestrates the getting of an item from cache and getting the requested item from the real source if it not found in the cache is as follows. Note that the code also adds the item from the real source to the cache before returning it to the caller - incrementally improving site performance as more and more items of the same type are requested for processing by different parts of the application.

[sourcecode language="csharp"]

private void btnGetFromCache_Click(object sender, EventArgs e){// Construct the cache key from the First Name and Last Nameif (!string.IsNullOrEmpty(txtPersonFirstName.Text) &amp;&amp;!string.IsNullOrEmpty(txtPersonLastName.Text)){StringBuilder sb = new StringBuilder();var cacheKey = string.Concat(txtPersonFirstName.Text.Trim(), txtPersonLastName.Text.Trim());

IGetCacheItemAdapter&lt;string, CacheResult&lt;Person&gt;&gt; getPersonFromCacheAdapter = new GetPersonFromCache();var cacheResult = getPersonFromCacheAdapter.Execute(cacheKey);if (cacheResult != null &amp;&amp;cacheResult.CacheValue != null &amp;&amp;cacheResult.CacheError == CacheErrorType.NoError){sb.Clear();sb.AppendLine(&quot;Person found in cache!&quot;);sb.AppendLine(string.Concat(&quot;First Name: &quot;, cacheResult.CacheValue.FirstName));sb.AppendLine(string.Concat(&quot;Last Name: &quot;, cacheResult.CacheValue.LastName));sb.AppendLine(string.Concat(&quot;Phone Number: &quot;, cacheResult.CacheValue.PhoneNumber));MessageBox.Show(sb.ToString());}else{// Get the Person from the external source and add the same to the cache for the next time the Person is searchedvar personFromExternalSource = SamplePersonCollection.PersonsFromExternalSource.Where(person =&gt; person.FirstName == txtPersonFirstName.Text.Trim() &amp;&amp; person.LastName == txtPersonLastName.Text.Trim()).First();

sb.Clear();sb.AppendLine(&quot;Person not found in cache - getting from external source - will also add to the cache!&quot;);sb.AppendLine(string.Concat(&quot;First Name: &quot;, personFromExternalSource.FirstName));sb.AppendLine(string.Concat(&quot;Last Name: &quot;, personFromExternalSource.LastName));sb.AppendLine(string.Concat(&quot;Phone Number: &quot;, personFromExternalSource.PhoneNumber));MessageBox.Show(sb.ToString());

IAddCacheItemAdapter&lt;Person&gt; addPersonToCacheAdapter = new AddPersonToCache();if (addPersonToCacheAdapter.Execute(personFromExternalSource)){MessageBox.Show(&quot;Person added successfully to cache!&quot;);}}}else{MessageBox.Show(&quot;Both First and Last Names are required to get a Person!&quot;);}}

[/sourcecode]

Conclusion

Some of the important concepts we looked at were the use of the capabilities offered by the .Net assembly System.Runtime.Caching as another way of implementing server side in-memory caching for objects sourced externally, the use of an interface based approach for interacting with the cache, the importance of appropriately generating cache item keys to keep the intended performance gains and also ways to interact with the cache that help in incrementally increasing performance as more and more objects of the same type keep getting added to the application's memory space.