Try our conversational search powered by Generative AI!

Daniel Ovaska
Apr 1, 2016
  6334
(3 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

Jeroen Stemerdink
Jeroen Stemerdink Apr 7, 2016 01:08 PM

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.

Apr 7, 2016 02:05 PM

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 :)

Hallstein Brøtan
Hallstein Brøtan Sep 5, 2016 02:25 PM

Nice work! How would you register the interceptor(s) for several interfaces when scanning an assembly?

Sep 5, 2016 05:00 PM

Best way to do it is configure it per interface. Normally you don't have THAT many...

container.For().DecorateAllWith(i => proxyGenerator.CreateInterfaceProxyWithTargetInterface(i, interceptor));

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 :)

 private static void ConfigureContainer(ConfigurationExpression container)
        {
           
            container.Scan(x =>
            {
                x.TheCallingAssembly();
                //x.WithDefaultConventions();
                x.Convention();
            });
....
 public class CustomInterceptionConvention : DefaultConventionScanner
    {
        public override void Process(Type type, Registry registry)
        {
            base.Process(type, registry);

            if (type.IsInterface || !type.Name.ToLower().EndsWith("newsrepository")) return;

            var pluginType = FindPluginType(type);

            var delegateType = typeof(Func<,>).MakeGenericType(pluginType, pluginType);

            // Create FuncInterceptor class with generic argument +
            var d1 = typeof(FuncInterceptor<>);

            Type[] typeArgs = { pluginType };

            var interceptorType = d1.MakeGenericType(typeArgs);
            // -

            // Create lambda expression for passing it to the FuncInterceptor constructor +
            var arg = Expression.Parameter(pluginType, "x");

            var method = GetType().GetMethod("GetProxy").MakeGenericMethod(pluginType);

            // Crate method calling expression
            var methodCall = Expression.Call(method, arg);

            // Create the lambda expression
            var lambda = Expression.Lambda(delegateType, methodCall, arg);
            // -

            // Create instance of the FuncInterceptor
            var interceptor = Activator.CreateInstance(interceptorType, lambda, "");

            registry.For(pluginType).Singleton().Use(type).InterceptWith(interceptor as StructureMap.Building.Interception.IInterceptor);
        }

        public static T GetProxy(object service)
        {
            var proxyGeneration = new ProxyGenerator();

            var result = proxyGeneration.CreateInterfaceProxyWithTarget(
               typeof(T),
               service,
               (Castle.DynamicProxy.IInterceptor)(GetInterceptor())
               );
            return (T)result;
        }

        public static Castle.DynamicProxy.IInterceptor GetInterceptor()
        {
            var logger = LogManager.Instance.GetLogger("NewsRepository");
            var interceptor = new MethodLoggerInterceptor(logger, new ObjectLogger(logger));
            return interceptor;
        }
    }

Sep 5, 2016 05:18 PM

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):

        private static void ConfigureContainer(ConfigurationExpression container)
        {
            var logger = LogManager.Instance.GetLogger("NewsRepository");
            var interceptor = new MethodLoggerInterceptor(logger, new ObjectLogger(logger));
            var proxyGenerator = new ProxyGenerator();
            var interceptor2 = new MethodLoggerInterceptor(logger, new ObjectLogger(logger));
            container.Scan(x =>
            {
                x.TheCallingAssembly();
                x.WithDefaultConventions();
                //x.Convention();
            });
           
            container.For().DecorateAllWith(i => proxyGenerator.CreateInterfaceProxyWithTargetInterface(i, interceptor));
            container.For().DecorateAllWith(i => proxyGenerator.CreateInterfaceProxyWithTargetInterface(i, interceptor));

...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...

Sep 8, 2016 04:54 PM

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.

public class GatewayRegistry : Registry
{
    public GatewayRegistry()
    {
        Policies.Interceptors(new GatewayInterceptorPolicy());

        Scan(s =>
        {
            s.AssemblyContainingType(typeof(GatewayRegistry));
            s.Include(type => type.Name.EndsWith("Gateway"));
            s.WithDefaultConventions().OnAddedPluginTypes(addedType => addedType.Singleton());
        });
    }
}

The interceptor policy:

public class GatewayInterceptorPolicy : IInterceptorPolicy
{
    public string Description => "Interceptor policy for gateways";

    public static List DynamicProxyInterceptors;

    public GatewayInterceptorPolicy()
    {
        DynamicProxyInterceptors = new List
        {
            new LogExceptionsAspect(),
            new CachingAspect()
        };
    }

    public IEnumerable DetermineInterceptors(Type pluginType, Instance instance)
    {
        if (pluginType.Name.EndsWith("Gateway"))
            yield return (IInterceptor)CreateFuncInterceptor(pluginType);
    }

    private object CreateFuncInterceptor(Type pluginType)
    {
        Type delegateType = typeof(Func<,>).MakeGenericType(pluginType, pluginType);

        // Create FuncInterceptor class with generic argument +
        Type d1 = typeof(FuncInterceptor<>);

        Type[] typeArgs = { pluginType };

        Type interceptorType = d1.MakeGenericType(typeArgs);

        // Create lambda expression for passing it to the FuncInterceptor constructor +
        ParameterExpression arg = Expression.Parameter(pluginType, "x");

        MethodInfo method = GetType().GetMethod("GetProxy").MakeGenericMethod(pluginType);

        // Crate method calling expression
        MethodCallExpression methodCall = Expression.Call(method, arg);

        // Create the lambda expression
        LambdaExpression lambda = Expression.Lambda(delegateType, methodCall, arg);

        // Create instance of the FuncInterceptor
        return Activator.CreateInstance(interceptorType, lambda, "");
    }

    public static T GetProxy(object service)
    {
        var proxyGeneration = new ProxyGenerator();

        return (T)proxyGeneration.CreateInterfaceProxyWithTarget(
            typeof(T),
            service,
            DynamicProxyInterceptors.ToArray());
    }
}

Hallstein Brøtan
Hallstein Brøtan Sep 8, 2016 04:58 PM

Forgot to log in, but that was my follow up post. Thanks again for helping out!

Sep 8, 2016 05:27 PM

Cool! :) Thx for feedback!

Please login to comment.
Latest blogs
Solving the mystery of high memory usage

Sometimes, my work is easy, the problem could be resolved with one look (when I’m lucky enough to look at where it needs to be looked, just like th...

Quan Mai | Apr 22, 2024 | Syndicated blog

Search & Navigation reporting improvements

From version 16.1.0 there are some updates on the statistics pages: Add pagination to search phrase list Allows choosing a custom date range to get...

Phong | Apr 22, 2024

Optimizely and the never-ending story of the missing globe!

I've worked with Optimizely CMS for 14 years, and there are two things I'm obsessed with: Link validation and the globe that keeps disappearing on...

Tomas Hensrud Gulla | Apr 18, 2024 | Syndicated blog

Visitor Groups Usage Report For Optimizely CMS 12

This add-on offers detailed information on how visitor groups are used and how effective they are within Optimizely CMS. Editors can monitor and...

Adnan Zameer | Apr 18, 2024 | Syndicated blog