Episerver performance–Part 3 Object Cache using AOP and interceptors
In earlier blog posts I’ve described how to implement object caching in Episerver using inline coding in function or as a separate layer using the decorator pattern.
Inline coding directly in your repositories / controllers is the easiest to implement in smaller projects but in a while you can get tired of the fact that your methods contain a lot of logic that isn’t really related to what the class should be concerned with.
Using the decorator pattern for solving a cross cutting concern like caching, you can extract that code to a decorator class which is very (S)OLID. But still not very DRY. If you have many classes that need caching you will still find the same lines of ugly caching code copy / pasted in your solution, only this time you have them in your decorator. Slightly better than messing up your repositories and logic, true; but if you have plenty of repositories, it gets more and more annoying to create the same decorators over and over again.
There must be a better way to solve this?
Caching with interceptors and AOP (Aspect Oriented Programming)
If you are new to the concept of interceptors and AOP I would recommend reading an earlier blog post before proceeding. Now we are diving into some concepts that are not widely familiar to .NET developers (but old news to JAVA developers)
EPiServer architecture, AOP and cross cutting concerns
With caching built as an interceptor, we can enable caching on methods simply by placing an attribute on the method you want to cache. Under the hood it will use the standard cache for Episerver that works well with load balancing.
We will continue to use a simple NewsRepository as demo class to have something to cache. The first parameter below specifies how many seconds to cache, the second the area in cache I want to use (to be able to group some cached objects together to make it easy to remove them later). Cache key is either automatically genereated by class,method and parameters or specified manually in the request parameters. The GetNewsItemRequest implements an interface that allows you to also control the cache in detail if needed.
public interface INewsRepository { [Cache(60, CacheBuckets.News)] GetNewsItemResponse GetAllNews(GetNewsItemRequest request); }
Now how cool is that!? I get caching on a method by simply decorating it with an attribute. Now that beats implementing a custom decorator any day of the week. We do need some infrastructure in the project to make it possible however.
Register your new interceptor for the INewsRepository
To enable your interceptor you also need to register it with structuremap. We will use Castles dynamic proxy to automagically generate a decorator class for our interface. DRY and SOLID this time!
var proxyGenerator = new ProxyGenerator(); container.For() .DecorateAllWith(i => proxyGenerator.CreateInterfaceProxyWithTargetInterface(i, new CacheInterceptor())); container.For ().Use ();
Creating a custom caching attribute
To control the cache interceptor you have a few possiblities. A custom attribute is one, appsettings is another and using a separate interface for the requests is a third. I'll use a custom attribute to control what method should be cached and how long and make it possible to use an interface on requests if you want to fin tune it. The cache bucket is mapped to EPiServers master key concept for cache to make it possible to invalidate parts of the cache. In my case, I want to make it easy to clear everything related to news so I'll add a new cache bucket for that.
[System.AttributeUsage(System.AttributeTargets.Method, AllowMultiple = false)] public class CacheAttribute : Attribute { private readonly string[] _cacheBuckets; private readonly TimeSpan? _duration; private readonly string _cacheKey; public CacheAttribute() { } public CacheAttribute(int durationInSeconds,params string[] cacheBuckets) { _cacheBuckets = cacheBuckets; _duration = new TimeSpan(0,0,0,durationInSeconds); } public CacheAttribute(string cacheKey, int duration, params string[] cacheBuckets):this(duration,cacheBuckets) { _cacheKey = cacheKey; } public string GetCacheKey() { return _cacheKey; } public IEnumerableGetCacheBuckets() { return _cacheBuckets; } public TimeSpan? GetDuration() { return _duration; } }
Creating a custom caching interceptor
It's definitely possible to create a simpler interceptor for caching. But if you want full controll of your cache including possiblilty to set cache keys, durations and dependencies it will boil down to a quite a few lines of code. The good part is that you can reuse this code not only for all your classes but for all your projects since interceptors are really easy to reuse between projects.
public class CacheInterceptor : IInterceptor { private readonly ILogger _log; private readonly IObjectLogger _objectLogger; private readonly ICacheService _cacheService; public CacheInterceptor() { _log = LogManager.GetLogger(typeof(CacheInterceptor)); _objectLogger = new ObjectLogger(_log); var cache = ServiceLocator.Current.GetInstance(); _cacheService = new CacheService(_log, _objectLogger, cache); } public void Intercept(IInvocation invocation) { _log.Information("Entering caching interceptor"); object cachedItem = null; var gotItemFromCache = false; var cacheAttribute = InterceptorUtil.GetMethodAttribute (invocation); var methodCalledIsResolved = false; if (cacheAttribute != null) { var methodCacheSettings = InterceptorUtil.GetArgumentByType (invocation); var cacheSettings = GetMergedCacheSettings(methodCacheSettings, cacheAttribute, invocation); _objectLogger.Dump(cacheSettings); if (cacheSettings.GetFromCache) //Get item from cache... { cachedItem = _cacheService.Get(cacheSettings.CacheKey); if (cachedItem != null) { gotItemFromCache = true; var cachedResponse = cachedItem as ICachedResponse; if (cachedResponse != null) { cachedResponse.GotItemFromCache = true; } invocation.ReturnValue = cachedItem; methodCalledIsResolved = true; if (_log.IsInformationEnabled()) { _log.Information(string.Format("Got item from cache with key {1} for method:{0}", invocation.Method.Name, cacheSettings.CacheKey)); } } } if (!gotItemFromCache) //Get item from data source... { invocation.Proceed(); _log.Information(string.Format("Got item from datasource with key {1} for method:{0}", invocation.Method.Name, cacheSettings.CacheKey)); methodCalledIsResolved = true; cachedItem = invocation.ReturnValue; } else { var cacheResponse = invocation.ReturnValue as ICachedResponse; if (cacheResponse != null) { cacheResponse.GotItemFromCache = true; } } if (cacheSettings.StoreInCache && !gotItemFromCache) //Store in cache... { _objectLogger.Dump(cacheSettings); _cacheService.Add(cacheSettings.CacheKey, cachedItem, _cacheService.GetCachePolicy(cacheSettings.CacheBuckets, cacheSettings.Duration ?? new TimeSpan(0, 0, 10, 0))); if (_log.IsInformationEnabled()) { _log.Information( string.Format("Stored item in cachebuckets: {2}\n with key: {1}\n for method:{0}\n", invocation.Method.Name, cacheSettings.CacheKey, string.Join(",", cacheSettings.CacheBuckets))); } } } else { _log.Information("No cache attribute found. Proceeding without cache"); } if (!methodCalledIsResolved) //If something went wrong...just proceed to the method and run it without cache... { invocation.Proceed(); } } private CacheSettings GetMergedCacheSettings(ICachedRequest methodCacheSettings, CacheAttribute attribute,IInvocation invocation) { var cacheSettings = new CacheSettings(); if (methodCacheSettings != null) { cacheSettings.CacheBuckets = methodCacheSettings.CacheBuckets; cacheSettings.CacheKey = methodCacheSettings.CacheKey; if (methodCacheSettings.CacheDuration != null) { cacheSettings.Duration = methodCacheSettings.CacheDuration.Value; } cacheSettings.GetFromCache = methodCacheSettings.GetFromCache; cacheSettings.StoreInCache = methodCacheSettings.StoreInCache; } if (cacheSettings.CacheBuckets == null || !cacheSettings.CacheBuckets.Any()) { cacheSettings.CacheBuckets = attribute.GetCacheBuckets(); } if (cacheSettings.Duration == null && attribute.GetDuration() != null) { cacheSettings.Duration = attribute.GetDuration(); } if (cacheSettings.Duration == null) { cacheSettings.Duration = new TimeSpan(0,0,10,0); } if (cacheSettings.CacheKey == null && attribute.GetCacheKey() != null) { cacheSettings.CacheKey = attribute.GetCacheKey(); } if (cacheSettings.CacheKey == null) { cacheSettings.CacheKey = _objectLogger.Dump(new { Method = string.Format("{0}.{1}", invocation.TargetType.FullName, invocation.MethodInvocationTarget.Name), Params = invocation.Arguments }); } return cacheSettings; } }
For interceptor classes I prefer to avoid using constructor dependency injection to avoid some complexity. What if an interface to the interceptor constructor is actually intercepted itself? Messy :)
Creating a cache service class to help coordinate cache
public class CacheService : ICacheService { private readonly ILogger _log; private readonly IObjectLogger _objectLogger; private readonly ISynchronizedObjectInstanceCache _cache; public CacheService(ILogger logger, IObjectLogger objectLogger, ISynchronizedObjectInstanceCache cacheRepository) { _log = logger; _objectLogger = objectLogger; _cache = cacheRepository; } public object Get(string key) { return _cache.Get(key); } public virtual void Add(string key, object item, CacheEvictionPolicy cacheItemPolicy) { _cache.Insert(key, item, cacheItemPolicy); } public virtual void EmptyCacheBucket(string name) { _log.Information(string.Format("Emptying cache bucket: {0}", name)); _cache.Remove(name); } public virtual string GenerateCacheKey(object parameters) { return _objectLogger.Dump(parameters); } public virtual CacheEvictionPolicy GetCachePolicy(IEnumerabledependencies, TimeSpan duration) { var cip = new CacheEvictionPolicy(new List (),new List (),dependencies,duration, CacheTimeoutType.Absolute); return cip; } public virtual void RemoveItem(string key) { _log.Information(string.Format("Removing single item from cache with key: {0}", key)); _cache.Remove(key); } }
Creating an interface for requests if you want to control your cache in detail
Perfect for not caching stuff in edit mode for example or building scheduled jobs that automatically populates your cache to avoid the first hit
public interface ICachedRequest { string CacheKey { get; } TimeSpan? CacheDuration { get; } bool GetFromCache { get; } bool StoreInCache { get; } IEnumerableCacheBuckets { get; } }
Let your requests implement interface
//I like to wrap requests and response to external datasources //at least with a request and reponse object. //Makes it more flexible to extend later... public class GetNewsItemRequest:Mogul.Interceptors.Cache.ICachedRequest { public string CacheKey { get; set; } public TimeSpan? CacheDuration{ get; set; } public bool GetFromCache { get; set; } public bool StoreInCache { get; set; } public IEnumerableCacheBuckets { get; set; } //Create some sensible defaults... public GetNewsItemRequest() { CacheKey = "NewsRepository:GetAllNews"; CacheDuration = new TimeSpan(0,10,0); GetFromCache = true; StoreInCache = true; CacheBuckets = new[] { MasterCacheKeys.News }; } }
Summary
Interceptors is a powerful concept in object oriented programming. For cross cutting concerns like caching or logging they can create reusable components in a way that hasn't really been possible before. I would personally recommend using them for logging if nothing else. Getting perfect logging on all your classes including measuring performance on methods is great especially for external datasources. The biggest con is that interceptors is still rather unknown for the majority of developers. This will likely change in the future since interceptors and AOP becomes more common just like IoC has become.
I'll upload the entire working source code for an example caching interceptor later as well as a nuget package for alloy site for those who want to take it for a test drive. I'll leave that for my next blog post because I think I've run out of chars in this one :)
Happy coding everyone!
Very cool, thanx for sharing. But if you are lazy like me, have a look at PostSharp, it contains a lot of default plumbing. I did a caching solution while for AOP Caching with PostSharp. The free license for PostSharp will be enough for these kind of implementations.
Yup, I've checked that out as well including your blog post :)
It's a matter of taste which depends on how likely it is that the next guy that takes over your project has the ability to learn it. That's the biggest con with either route for AOP in my opinion until C# gets support out of the box for AOP. Since Episerver already uses Castle core I went that way this time.
I'm a big fan of PostSharp as well though which will hopefully get a bigger impact in the future. It deserves it. I think however .NET community is stuck right now on trying to stomach IOC containers for a while longer before taking the next step. AOP is really the first major benefit of ioc in my eyes. Sure, unit tests and TDD are nice...but they also include some overhead so for productivity in a generic team in a generic project they don't add any great value IMNSHO. AOP can solve pretty huge chunks of functionality in a project in a really sweet way that is actually reusuable. I'm still surprised after a few years of using it and looking for drawbacks :)
Nice work! How would you register the interceptor(s) for several interfaces when scanning an assembly?
Best way to do it is configure it per interface. Normally you don't have THAT many...
then you do it after the type scanning.
It's also possible to create a separate scanning convention to save you some typing if you have a large number of types you want to intercept. Unfortunately the support for this in Structuremap is not the best for dynamically created decorators so you'll have to go into some deep waters...
Below I configure structuremap to add my custom methodloginterceptor to all instances that has a name that ends with "newsrepository". As you see, the above method is much nicer to work with than this mess below :)
So adding the same interceptor to multiple interfaces can look like this if you are ok with typing each interface you want to add support for (with type scanning):
...also pretty fun to be able to hook into Episerver own interfaces and see what they are getting for input parameters and output. Here I get timing and information about what the SiteConfiguration is actually doing... :)
If they don't have logging on their classes, then np...add an interceptor and log away...
Thanks a lot, this was really helpful! We were able to combine assembly scanning with AOP using Castle Dynamic Proxies based on your code.
We ended up with using the Gateway installer as a registry, and then added a interceptor policy where we set up aspects for logging and caching.
Just another flavour to your solution, really. Could probably have used the DecorateAllWith inside the scan aswell.
The interceptor policy:
Forgot to log in, but that was my follow up post. Thanks again for helping out!
Cool! :) Thx for feedback!