Five New Optimizely Certifications are Here! Validate your expertise and advance your career with our latest certification exams. Click here to find out more

Daniel Ovaska
Apr 1, 2016
  2724
(0 votes)

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 IEnumerable GetCacheBuckets()
        {
            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(IEnumerable dependencies, 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; }
        IEnumerable CacheBuckets { 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 IEnumerable CacheBuckets { 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!

Apr 01, 2016

Comments

Please login to comment.
Latest blogs
Optimizely Configured Commerce and Spire CMS - Figuring out Handlers

I recently entered the world of Optimizely Configured Commerce and Spire CMS. Intriguing, interesting and challenging at the same time, especially...

Ritu Madan | Mar 12, 2025

Another console app for calling the Optimizely CMS REST API

Introducing a Spectre.Console.Cli app for exploring an Optimizely SaaS CMS instance and to source code control definitions.

Johan Kronberg | Mar 11, 2025 |

Extending UrlResolver to Generate Lowercase Links in Optimizely CMS 12

When working with Optimizely CMS 12, URL consistency is crucial for SEO and usability. By default, Optimizely does not enforce lowercase URLs, whic...

Santiago Morla | Mar 7, 2025 |

Optimizing Experiences with Optimizely: Custom Audience Criteria for Mobile Visitors

In today’s mobile-first world, delivering personalized experiences to visitors using mobile devices is crucial for maximizing engagement and...

Nenad Nicevski | Mar 5, 2025 |

Unable to view Optimizely Forms submissions when some values are too long

I discovered a form where the form submissions could not be viewed in the Optimizely UI, only downloaded. Learn how to fix the issue.

Tomas Hensrud Gulla | Mar 4, 2025 |

CMS 12 DXP Migrations - Time Zones

When it comes to migrating a project from CMS 11 and .NET Framework on the DXP to CMS 12 and .NET Core one thing you need to be aware of is the...

Scott Reed | Mar 4, 2025