Implement caching on class methods with a custom attribute

14 January 2014 at 00:00 by Ruud van Falier - Post a comment

A common requirement for many Sitecore solutions is applying caching to certain areas of the application.
For example, on a service manager, an abstraction layer inbetween external services and the web application.

This blog post describes an example of an implementation of a caching layer that is reusable and does not require the existing logic to be modified.
It works by adding an attribute to the method that needs to be cached, marking it as cacheable.

My goal for this caching layer was to implement it in such way that:

  • There is no need to modify existing logic of the cached methods.
  • Caching parameters can be set per method.
  • The caching layer can be unit tested separately from its implementation.
  • You are able to implement different caching handlers (e.g. one storing cache to disk and one storing to memory).

How it works

I'm using Castle DynamicProxy to create proxy classes that implement an IInterceptor interface.
The interceptor will intercept any invocations on the proxy object.
So whenever a method is called, it will go through the interceptor first.
At that point we can check if the method has the CacheAttribute assigned to it and apply caching.

Here is an example to give you a better picture.
Let's say you have a service class called DummyService that you need to be cacheable.

Instead of doing:


var service = new DummyService(); 

You do this:


var service = new Castle.DynamicProxy.ProxyGenerator().CreateClassProxy(new CacheHandler());

The result will be an object of the type Castle.Proxies.DummyServiceProxy that inherits from DummyService (and thus has all the members that DummyService exposes), but all method invocations will be intercepted by the CacheHandler class.

CacheHandler is a class that implement Castle.DynamicProxy.IInterceptor which adds the Intercept() method that intercepts all invocations on the proxy object.

Using the CacheAttribute

To make a method cacheable, simply apply the CacheAttribute:


[Cache(CacheKey = “MyMethodName”, Expiration = 3600)]

You must also mark your method to be virtual.
This is very important as the proxy class must be able to override to method.

There are two parameters for the Cache attribute:

Expiration: the expiration time in seconds.
CacheKey (optional): the unique identifier for the cache that is used to store the cached data in a dictionary.

The CacheKey is optional and leaving it empty will result in the key being automatically generated based on the namespace, class and method name.

Here is an example of a generated cache key: Rakuten.Web.Tests.Library.Caching.DummyService.DummyMethodWithArguments({ number : 1, text : "Hello world!" })

If you are using objects in the method arguments, make sure to implement the ToString() method properly as this is used when generating the cache key!

Download the example project

I've created an example project that includes a simple CacheHandler and contains a Unit Test project that shows you exactly how to use it.
If you debug the Unit Test project and put a breakpoint at the end of it, you can see the results in the Debug output window.

You can find it on GitHub here: https://github.com/ParTech/CacheAttributeExample.

Let me know what you think of it!

Test code and results

This is the test code that I've used in the Unit Test project:


private void DebugServiceMethods()
{
    // Initializes a new DummyServiceProxy instance that implements the CacheHandler interceptor.
    var service = DummyService.Create();

    // Call a method without caching.
    Debug.WriteLine(service.DummyMethodNoCaching());

    // Call a cached method twice.
    Debug.WriteLine(service.DummyMethodCached());
    Debug.WriteLine(service.DummyMethodCached());

    // Call a cached method with one second expiration 6 times with an interval of 500 miliseconds.
    for (int i = 0; i < 6; i++)
    {
        Debug.WriteLine(service.DummyMethodCachedExpireInOneSecond());
        Thread.Sleep(500);
    }

    // Call a cached method with arguments twice.
    Debug.WriteLine(service.DummyMethodWithArguments(1, "Hello world!"));
    Debug.WriteLine(service.DummyMethodWithArguments(1, "Hello world!"));

    // Call the same cached method with different arguments twice.
    Debug.WriteLine(service.DummyMethodWithArguments(2, "Hello unit test!"));
    Debug.WriteLine(service.DummyMethodWithArguments(2, "Hello unit test!"));

    // Output a list of all the stored cache keys.
    Debug.WriteLine(string.Join(Environment.NewLine, new CacheHandler().GetAllCachedObjects()
        .Select(x => string.Concat("Key: ", x.Key, " Expires on: ", x.Expiration.ToString("yyyy-MM-dd HH:mm:ss:fff")))));
}

And this is the result that is written to the debug output:


DummyMethodNoCaching: Return value created on tick 635253038286298136
DummyMethodCached: Return value created on tick 635253038286358125
DummyMethodCached: Return value created on tick 635253038286358125
DummyMethodCachedExpireInOneSecond: Return value created on tick 635253038286378138
DummyMethodCachedExpireInOneSecond: Return value created on tick 635253038286378138
DummyMethodCachedExpireInOneSecond: Return value created on tick 635253038296404846
DummyMethodCachedExpireInOneSecond: Return value created on tick 635253038296404846
DummyMethodCachedExpireInOneSecond: Return value created on tick 635253038306451553
DummyMethodCachedExpireInOneSecond: Return value created on tick 635253038306451553
DummyMethodWithArguments: Return object for arguments { number: 1, text: "Hello world!" } created on tick 635253038316488268
DummyMethodWithArguments: Return object for arguments { number: 1, text: "Hello world!" } created on tick 635253038316488268
DummyMethodWithArguments: Return object for arguments { number: 2, text: "Hello unit test!" } created on tick 635253038316508281
DummyMethodWithArguments: Return object for arguments { number: 2, text: "Hello unit test!" } created on tick 635253038316508281
Key: CacheAttributeExample.DummyService.DummyMethodCached() Expires on: 2014-01-14 13:48:48:635
Key: CacheAttributeExample.DummyService.DummyMethodCachedExpireInOneSecond() Expires on: 2014-01-14 13:43:51:645
Key: CacheAttributeExample.DummyService.DummyMethodWithArguments({ number : 1, text : "Hello world!" }) Expires on: 2014-01-14 13:48:51:648
Key: CacheAttributeExample.DummyService.DummyMethodWithArguments({ number : 2, text : "Hello unit test!" }) Expires on: 2014-01-14 13:48:51:650

Latest