Easy way to Invalidate Output Cache in Episerver
Various .NET and Episerver folks has already been covering multiple posts about Output Caching and the challenges we, developers, face when implementing Full Page Caching, Donut Hole Caching and Donut Caching. You may think, so how is this post different? Several people has been avoiding the inevitable question about invalidation of cache.
We’ve been using various libraries for managing Output Caching on Episerver CMS and Episerver Commerce solutions. Episerver does provide out of the box solutions, but we are not considering to use this due to its limitations in terms of Donut Hole Caching and Donut Caching. Others, including the built in caching in ASP.NET MVC, has other severe limitations that are problematic in the world of an enterprise CMS: cache entries are first of all not invalidated in a distributed setup and are, secondly, identified based on a page URL. Consequence of the URL based approach is, e.g. when you have an action with arguments that are part of the route (such as a page number), that it is not possible to remove all cached pages for that particular action.
I will not take any credit for solving the fundamental issue in identifying the cached entries by URL. It has been managed by several open source projects for ASP.NET MVC. DevTrends.MvcDonutCaching is the 3rd party library we tend use when implementing output caching. It, most importantly, gives us a great foundation for invalidation of entries, but are also supporting great features such as Donut and Donut Hole Caching.
Please check out their ASP.NET MVC extensible Donut Caching project available here.
DevTrends.MvcDonutCaching has a simple approach for managing the cache entries. Their CacheOutputManager is the centerpiece in my walkthrough, due to its responsibility of adding, getting and removing entries in the memory driven cache being the foundation in their solution. We will, in order to invalidate the cache, primarily use following signature:
RemoveItems([AspMvcController] string controllerName, [AspMvcAction] string actionName, object routeValues)
As you can see, we need to invalidate the cache based on the name of our ASP.NET Controller including Action and optionally, but highly recommended, the route values. It means, that we, in order to invalidate the output cache for a block, need to request with something similar to GeneralBlockController, Index and route values containing a representation of current content.
Taking Episerver into account
First challenge is that DevTrends.MvcDonutCaching only expects simple types as route values. Episerver is natively passing a route value called currentcontent containing an instance of IContent. To ensure that this route value is taken properly into account, we need to adjust a few minor details. The Output Cache supports VaryByParam, which can tell the cache key builder to include the route value called currentcontent.
<add name="Global" duration="3600" varyByParam="currentContent" />
Next step is to ensure that the currentcontent, which is of a complex type, is managed correctly by the cache key builder. DevTrends.MvcDonutCaching defines an abstraction called IKeyBuilder that can be implemented for that specific purpose. Extending the existing DevTrends.MvcDonutCaching.KeyBuilder with an override of BuildKeyFragment that manages IContent is all we need:
We can ensure that our Episerver oriented KeyBuilder is used by defining a new version of their DonutOutputCacheAttribute
We now know that the Output Cache is taking Episerver into account when cache keys are generated. Next step is to make sure that the previously mentioned method is requested when a cache entry has to be invalidated. In our world, that is when content is changed.
Invalidating when content changes
First goal for us is to get the content instance that were changed. Listening to events in Episerver is the only way of knowing if content is being changed by a content editor. Let me take you through how this can be achieved for local and remote events.
Local events is, due to their nature of being raised on the machine itself, rather simple. These are raised via the DataFactory.Instance.PublishedContent event that is handing us the content instance that were changed.
Getting the content instance that were changed is more complicated when dealing with remote events. These kind of events are raised across a network, e.g. via UDP or TCP, when Episerver is used in a distributed setup. Let me try and guide you through an example on how to listen and react to a content change event raised by a remote machine.
Episerver uses Event IDs for identifying the type of an event. These types are, as far as I know, not documented anywhere. My best buddy, when dealing with these, is my .NET reflection tool.
Events identified by 9484e34b-b419-4e59-8fd5-3277668a7fce (GUID) are remove from cache events and is the type of event we are looking for.
Following code-snippet shows how you can listen and react to local and remote events. It is important to emphasize how IEventRegistry acts as the centerpiece when dealing with notifications from other nodes in your physical setup.
Remote events contains a string value detailing the context of the event. It is accessible via the Params property and contains, in respect of our subject, either EPLanguageData:1, EP:ContentVersion:1 or EPContentVersion:1. Please note that the value after the colon is the Id of the content being changed. It means that we can easily use some logic to match and react when one of these three events are raised.
We are now in possession of the piece of content that were changed, regardless if it was done on the current Episerver instance or on another node nearby. Invalidation of our Output Cache, registered for this piece of content, can be done by utilizing the TemplateModelRepository available in Episerver . It is able to serve us all template models registered for this type of content.
We’ve now finally reached the end and are able to call the StartListen() method in a Episerver InitializationModule in order to register our event-listening. It means, that DevTrends.MvcDonutCaching now are removing cache entries, for the particular MVC Controller and content piece, everytime content is changed.
Please be aware that I’ve altered and adjusted code for the sake of the readability. Feel free to let me know if you meet any issues or are seeing any loose ends.
Lastly, I want to add that Jon D Jones has a blog post detailing how to utilize the Donut Caching features, by using custom Html Helper Extensions and Donut Hole Fillers, in DevTrends.MvcDonutCaching. It should enable us all in tie all the loose ends!