This post assumes knowledge of Dependency Injection and AOP.
AOP or Aspect-Oriented Programming is a very powerful way to add Cross-Cutting concerns to a system without impacting the core code base. Cross-Cutting concerns can cover non-functional requirements and also functional requirements.
There are generally three approaches when implementing AOP.
1. Generate Dynamic Proxies at runtime to intercept method calls
2. Post-Compilation Assembly Transformation
3. Attributes
- Just using Plain Old Attributes
I really really don’t like annotating classes & members with Attributes so for me Dynamic Proxies is the only option and the performance hit is worth the productivity gains.
The very common example of AOP is Logging. Logging is by all accounts a Cross-Cutting concern and so lends itself very well to the example.
However I tend to think of Caching as a Cross-Cutting concern and really hate to see code like the below.
public Product GetProductById(int id) { var cacheKey = string.Format("Product-", id); var cache = HttpRuntime.Cache; var product = cache.Get(cacheKey) as Product; if (product != null) { return product; } product = productRepository.Get(id); cache.Add(cacheKey, product, null, System.Web.Caching.Cache.NoAbsoluteExpiration, System.Web.Caching.Cache.NoSlidingExpiration, CacheItemPriority.High, null); return product; }
IMO the above code block only adds noise to your code base and makes it harder to read. It’s also prone to logic errors if it’s being implemented in many places and so is a prime candidate for AOP.
In this example I will be using Castle Windsor just because I am most familiar with it, but any good Dependency Injection framework should support AOP.
In Castle Windsor there is a concept of an Interceptor which as the name implies intercepts any method calls on a class. Behind the scenes this is implemented using DyanmicProxy.
Let’s first have a look at a fairly contrived example of a Service that we want to add Caching too.
public interface ICatalogQueryService { Product GetProductById(int id); }
Now in order to configure the Service to use our Interceptor we need a way to identify which class we want to apply the interceptor too. You can use Attributes or explicitly set the interceptor when configuring the Container however I prefer is to use an Interface to explicitly mark which Interface should be intercepted. This allows you to easily configure a Facility but that’s a topic for another post.
public interface IMustBeCached { }
So after that I change the ICatalogQueryService to implement the IMustBeCached interface.
public interface ICatalogQueryService : IMustBeCached { Product GetProductById(int id); }
The next and most important part is creating the Interceptor, this has to implement the Castle.Core.Interceptor.IInterceptor interface.
So here it is, in just a few lines of code we have implemented a reusable Cache policy that caches all return values for one minute and our ICatalogQueryService implementation is none the wiser.
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; } 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 intercepted method invocation.Proceed(); if (invocation.ReturnValue != null) { cacheProvider.Put(cacheKey, CacheExpiryMinutes, invocation.ReturnValue); } return; } #endregion private static string BuildCacheKeyFrom(IInvocation invocation) { var methodName = invocation.Method.Name; var arguments = (from a in invocation.Arguments select a.ToString()).ToArray(); var argsString = string.Join(",", arguments); var cacheKey = methodName + "-" + argsString; return cacheKey; } }
Now all that’s left to do is tie it to configure the Windsor Container to use the Interceptor.
public class Bootstrapper { private WindsorContainer container; public WindsorContainer Container { get { return container; } } 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); } }
The other great thing about this is that you can turn caching on or off just by configuring the Container.
Usually I will only enable Interceptors when in the Staging & Production environments and so keep any Cache out of the Debug environment as it can be problematic.
That’s all there is to it. Feel free to comment if you have any questions.
Till next time.