Caching Aspect

Following on from logging, the other aspect we’re using is caching.  Goal is similar, make it really easy to cache but not litter the code with lots of calls to caches (or worse, random static dictionaries hidden in classes).  The motivation here is to solve the problem where you go off and look up a bit of meta-data and don’t want to keep going to the DB, or disk or wherever to get it.  Also, I wanted it to be easy to apply, after running some profile traces you see some stuff that’d benefit from caching and you just turn it on.

This is not a my version of CouchDb, Mongo or whatever (I’ll leave that up to people with much more cranial capacity than myself!).  It could easily use couch et al. as a cache store, but I’ve not really made my mind up on whether that would be a good idea.  I think I’ll have a chat with some people over some beers one evening.

Keeping it simple was key here, it’s more complicated than the logging aspect, but I hope not much.  One thing that made me quite happy was the new concurrent dictionary in .Net 4.

public class CachingAspect : MethodInvocationAspect
{
    private readonly ICacheContainer cacheContainer;
 
    public CachingAspect(ICacheContainer cacheContainer)
    {
        if (cacheContainer == null) throw new ArgumentNullException("cacheContainer");
        this.cacheContainer = cacheContainer;
    }
 
    public override bool HandleForMethodCall(MethodInvocationContext invocationContext)
    {
        var methodCacheSettings = CachingConfiguration.GetMethodCacheSettings(invocationContext.ServiceType,
                                                                              invocationContext.Invocation.MethodInvocationTarget,
                                                                              CreateListOfArgumentTypes(invocationContext));
        var shouldHandle = base.HandleForMethodCall(invocationContext)
                           && methodCacheSettings.ShouldCache;
 
        if (shouldHandle && !invocationContext.ContainsStateItem<CacheMethodSettings>())
        {
            invocationContext.AddStateItem(methodCacheSettings);
        }
 
        return shouldHandle;
    }
 
    private Type[] CreateListOfArgumentTypes(MethodInvocationContext invocationContext)
    {
        var types = new Type[invocationContext.Invocation.Arguments.Length];
        for (int i = 0; i < types.Length; i++)
        {
            var arg = invocationContext.Invocation.Arguments[i];
            if (arg != null)
            {
                types[i] = arg.GetType();
            }
            else
            {
                types[i] = typeof(UnkownType);
            }
        }
 
        return types;
    }
 
    private class UnkownType { }
 
    public override MethodVoteOptions PreCall(MethodInvocationContext invocation)
    {
        object cachedItem;
 
        var settings = invocation.GetStateItem<CacheMethodSettings>();
        
        var key = GetKeyFromInvocationParameters(invocation.Invocation, settings);
 
        var cache = cacheContainer.GetCache(invocation,
                                            settings);
 
        if (cache.TryGetValue(key, out cachedItem))
        {
            invocation.Invocation.ReturnValue = cachedItem;
            return MethodVoteOptions.Halt;
        }
 
        invocation.AddStateItem(cache);
        invocation.AddStateItem(key);
 
        return MethodVoteOptions.Continue;
    }
 
    public override void PostCall(MethodInvocationContext invocation)
    {
        if (invocation.Invocation.ReturnValue != null)
        {
            var cache = invocation.GetStateItem<ICache>();
            var key = invocation.GetStateItem<KeyValuePair<string, object>[]>();
            var settings = invocation.GetStateItem<CacheMethodSettings>();
 
            cache.AddOrReplace(key, invocation.Invocation.ReturnValue, settings.ExpirySettings);
        }
    }
 
    public override void OnException(MethodInvocationContext invocation, Exception e)
    {
    }
 
    private static KeyValuePair<string, object>[] GetKeyFromInvocationParameters(IInvocation invocation, CacheMethodSettings settings)
    {
        var i = 0;
        var parameters = invocation.Method.GetParameters();
        var key = new KeyValuePair<string, object>[parameters.Length];
        foreach (var param in parameters)
        {
            var keyValue = invocation.GetArgumentValue(i);
            if (settings.HasCustomCacheKey(i))
            {
                var customKey = settings.GetCustomCacheKey(i);
                keyValue = customKey.KeyFunc.DynamicInvoke(keyValue);
            }
            key[i] = new KeyValuePair<string, object>(param.Name, keyValue);
            i++;
        }
        return key;
    }
}

Here’s a few ways it can be configured:

[TestMethod]
public void When_Registering_With_Container_Can_Enable_Caching_For_Service()
{
    var container = new WindsorContainer();
    container.Register(Component.For<ITestService>()
                           .ImplementedBy<TestServiceImpl>()
                           .EnableCaching());
 
    Assert.IsTrue(CachingConfiguration.ShouldCacheForService(typeof(ITestService)));
}
 
[TestMethod]
public void When_Enabling_Logging_Can_Include_Specifc_Methods()
{
    var container = new WindsorContainer();
    container.Register(Component.For<ITestService>()
                           .ImplementedBy<TestServiceImpl>()
                           .EnableCaching(config => config
                               .CacheMethod(s => s.DoSomething())));
 
    var doSomethingMethodInfo = typeof(ITestService).GetMethod("DoSomething");
 
    Assert.AreNotEqual(false,
        CachingConfiguration.GetMethodCacheSettings(typeof(ITestService), doSomethingMethodInfo, new Type[0]).ShouldCache);
}
 
[TestMethod]
public void When_Enabling_Logging_Can_Include_All_Methods()
{
    var container = new WindsorContainer();
    container.Register(Component.For<ITestService>()
                           .ImplementedBy<TestServiceImpl>()
                           .EnableCaching(config => config
                               .CacheAllMethods()));
 
    var doSomethingMethodInfo = typeof(ITestService).GetMethod("DoSomething");
 
    Assert.AreNotEqual(false,
        CachingConfiguration.GetMethodCacheSettings(typeof(ITestService), doSomethingMethodInfo, new Type[0]).ShouldCache);
}
 
[TestMethod]
public void When_Caching_Method_Can_Add_Method_With_Multiple_Parameters()
{
    var container = new WindsorContainer();
    container.Register(Component.For<ITestService>()
                           .ImplementedBy<TestServiceImpl>()
                           .EnableCaching(config => config
                               .CacheMethod(s => s.DoSomethingDifferent(CacheArg<string>.Value))));
 
    var doSomethingMethodInfo = typeof(ITestService).GetMethod("DoSomethingDifferent");
 
    Assert.AreNotEqual(false,
        CachingConfiguration.GetMethodCacheSettings(typeof(ITestService), doSomethingMethodInfo, new Type[0]).ShouldCache);
}
 
[TestMethod]
public void When_Caching_Method_Can_Specify_How_To_Generate_Keys_For_Parameters()
{
    var container = new WindsorContainer();
    container.Register(Component.For<ITestService>()
                           .ImplementedBy<TestServiceImpl>()
                           .EnableCaching(
                               c => c.CacheMethod(
                                   s => s.DoSomethingDifferent(
                                       CacheArg<string>.WithKey(p => p.Substring(1))))
                           ));
 
    var doSomethingMethodInfo = typeof(ITestService).GetMethod("DoSomethingDifferent");
 
    var cacheMethodSettings = CachingConfiguration.GetMethodCacheSettings(typeof(ITestService), doSomethingMethodInfo, new[] { typeof(string) });
    Assert.AreEqual(true, cacheMethodSettings.ShouldCache);
    Assert.IsTrue(cacheMethodSettings.HasCustomCacheKey(0));
}

As you can see from this and the logging configuration I’ve used extension methods on the component registration.  This felt like the right place to do it and has proved easy to use.  You don’t have to use the extension methods, they are just wrappers.  I’m really leaning on Windsor as much as I can, it’s saving me time and I hope giving people an easy to use API.  One thing I don’t like is the number of brackets…

Advertisements

About Tom Peplow

C# .Net developer based in London and the South Coast
This entry was posted in Uncategorized and tagged , , . Bookmark the permalink.

2 Responses to Caching Aspect

  1. Pingback: Aspect Oriented Programming with Castle Windsor | pep => lowdown

  2. Pingback: Caching with Entity Framework | pep => lowdown

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