In my previous post I showed how you can very easily add Caching to your application by using Castle Windsor.
The example used was a very basic implementation and whilst it can be useful for a large number of cases it didn’t cover the all important cache invalidation.
In this post I hope to explain how with a bit of convention over configuration how you can invalidate your cache when an object changes.
In the last post I was using the Method name combined with the arguments to build the cache key. Now to allow us to build a cache key that can be used when invalidating the cache the key needs to be related to the Result type.
For this I’m going to create a simple interface called IAmCacheable to identify the object we want to cache.
public interface IAmCacheable { object Key { get; } }
And make sure the object we want to cache implements this interface.
public class Product : IAmCacheable { public object Key { get; } public string Name { get; set; } }
Now we have to change the CacheInterceptor to build the Cache Key based on the combination of the Type and the Id.
public class CacheInterceptor : IInterceptor { private readonly ICacheProvider cacheProvider; private const int CacheExpiryMinutes = 1; public CacheInterceptor(ICacheProvider cacheProvider) { this.cacheProvider = cacheProvider; } #region IInterceptor Members public void Intercept(IInvocation invocation) { //check if the method has a return value if (invocation.Method.ReturnType == typeof(void)) { invocation.Proceed(); return; } //check to see if the return type implements IAmCacheable var returnTypeIsCacheable = typeof (IAmCacheable) .IsAssignableFrom(invocation.Method.ReturnType); if (!returnTypeIsCacheable) { invocation.Proceed(); return; } var cacheKey = BuildCacheKeyFrom(invocation); //try get the return value from the cache provider var item = cacheProvider.Get(cacheKey); if (item != null) { invocation.ReturnValue = item; return; } //call the interecepted method invocation.Proceed(); if (invocation.ReturnValue != null) { cacheProvider.Put(cacheKey, CacheExpiryMinutes, invocation.ReturnValue); } return; } #endregion private static string BuildCacheKeyFrom(IInvocation invocation) { var cacheableTypeName = invocation.Method.ReturnType.Name; var cacheKey = "{0}-{1}"; var keyArgName = "id"; var prms = invocation.Method.GetParameters(); var keyIndex = prms.TakeWhile(prm => prm.Name != keyArgName).Count(); var keyValue = invocation.Arguments[keyIndex]; return string.Format(cacheKey, cacheableTypeName, keyValue); } }
The main change here is in the BuildCacheKeyFrom method. What I’m doing here is looking for the value of the Argument named “id” and using this combined with the Type name to build the key which would end up like “Product-1”.
Now that we’ve changed the logic to build the Cache Key we need to be remove the object from the Cache when it changes.
Similar to the IMustBeCached interface in the first article we create an interface called IInvalidateCache to identity which call to apply the invalidation interceptor on.
public interface IInvalidateCache { }
And we’ll make the IProductRepository implement this like so.
public interface IProductRepository : IInvalidateCache { Product Get(int id); void Update(Product product); }
Now we need a new type of Interceptor called CacheInvalidationInterceptor which removes any IAmCacheable objects from the cache when they are passed as arguments to any method.
public class CacheInvalidationInterceptor : IInterceptor { private readonly ICacheProvider cacheProvider; public CacheInvalidationInterceptor(ICacheProvider cacheProvider) { this.cacheProvider = cacheProvider; } #region IInterceptor Members public void Intercept(IInvocation invocation) { //check to see if any argument implements IAmCacheable var hasCacheableArgument = invocation.Arguments.Any(a => a is IAmCacheable); if (!hasCacheableArgument) { invocation.Proceed(); return; } //call the intercepted method invocation.Proceed(); var cacheKey = BuildCacheKeyFrom(invocation); //remove the item from the cache cacheProvider.Remove(cacheKey); return; } #endregion private static string BuildCacheKeyFrom(IInvocation invocation) { var cacheable = invocation.Arguments.First(a => a is IAmCacheable) as IAmCacheable; var cacheableTypeName = cacheable.GetType().Name; var cacheKey = "{0}-{1}"; return string.Format(cacheKey, cacheableTypeName, cacheable.Key); } }
All we do here is look for any Arguments which implement IAmCacheable and then use a combination of the Type name and the IAmCacheable.Key to build the cache key.
We also make sure the method is called first by calling Invocation.Proceed before removing the item from the Cache.
Finally we need to wire up this new Interceptor in the Bootstrapper.
public void Configure() { container = new WindsorContainer(); container.Register( Component.For<CacheInterceptor>(), Component.For<ICacheProvider>() .ImplementedBy<WebCacheProvider>().LifeStyle.Singleton, Component.For<ICatalogQueryService>() .ImplementedBy<CatalogQueryService>() .LifeStyle.Transient .Interceptors(new InterceptorReference(typeof(CacheInterceptor))).Anywhere); container.Register( Component.For<CacheInvalidationInterceptor>(), Component.For<IProductRepository>() .ImplementedBy<ProductRepository>() .LifeStyle.Transient .Interceptors(new InterceptorReference( typeof(CacheInvalidationInterceptor))) .Anywhere); }
This is my final post on the subject. Hope this gives you some ideas on how to implement Caching or other Cross-Cutting concerns using AOP.
Till next time.