Episerver Caching – Implementing a Custom Output Cache Provider in MVC

This is the fourth post in a series of posts covering Episerver output caching; I would recommend reading Episerver Caching – Output Caching Strategies Explained…. What Is a Donut Cache? to understand some of the needs as to why you might want to create a custom output cache provider.

In the previous articles, I’ve covered how to enable the output cache on the pages and blocks using a full page caching strategy, as well as setting custom cache keys/params to deal with custom content that might be causing the cache engine to return the wrong HTML, in certain circumstances.

If you have decided, however, that a ‘full page’ caching strategy isn’t the right approach for your project, then the next part of the puzzle that you will have to deal with, is how to implement donut caching in Episerver.

What makes donut caching more complex and confusing to implement than full page caching is because.NET manages full page output cache and partial page caching differently.  In a ‘full page’ cache scenario, the traditional runtime output cache attribute is used.  In ‘partial page’ caching, .NET uses a key/value pair strategy using the MemoryCache-object so it’s not as easy as just enabling the [OutputCacheAttribute] on your blocks.

To further complicate the matter in Episerver caching, it is easy to get confused between block caching and partial block caching. Under the hood. NET will use ‘partial’ caching when a block doesn’t have its own controller.  In today’s guide, we’re going to cover the basics of setting up a custom cache provider.  In theory, there are two ways to implement this, although in practice I’ve only found one works a lot better.

Creating A Custom Cache Provider

In a normal MVC website, a common scenario when you need a more complex caching approach, like a donut cache, is to create your own caching provider.  In your code, you inherit from ‘OutputCacheProvider’ and implement the overrides and you should end up with a skeleton class that looks like this:

public class CustomOutputCacheProvider: OutputCacheProvider
{
public override object Add(string key, object entry, DateTime utcExpiry)
{
}
public override object Get(string key)
{
}
public override void Set(string key, object entry, DateTime utcExpiry)
{
}
public override void Remove(string key)
{
}
}

In your caching section in your web.config you set your default provider:

<caching>
<outputCache enableOutputCache="true" defaultProvider="CustomOutputCacheProvider">
<providers>
<add name="CustomOutputCacheProvider" type="Namespace.CustomOutputCacheProvider, AssemblyName" />
</providers>
</outputCache>
</caching>

If you try and implement this out, as is, you’ll find two issues.  One, your Get and Set will never be hit by the debugger in Visual Studio (it didn’t for me at least!!!).  Also, when you try and load your website you may see:

‘When using a custom output cache provider like ‘EPiServerOutputCacheProvider’, only the following expiration policies and cache features are supported: file dependencies, absolute expirations, static validation callbacks and static substitution callbacks.’

IObjectInstanceCache

As most of the documentation I’ve found on the web doesn’t clearly cover how to fix these two issues in MVC, I’ll cover the basics. In Episerver 8 (I think) a new way/interface was introduced to enable us to cache. The IObjectInstanceCache, (see Object caching).  With this interface and a little bit of configuration, we can still create intercept all the requests to the cache without having to implement a custom OutputCacheProvider.  This benefits us in two ways.. the debugger will now work and we can do all our configuration in code, rather than the web.config.

The skeleton code for the custom provider now looks like this:

public class GmgObjectInstanceCache : IObjectInstanceCache
{
public void Clear()
{
}
public object Get(string key)
{
}
public void Insert(string key, object value, CacheEvictionPolicy evictionPolicy)
{
}
public T ReadThrough<T>(string key, Func<T> readValue, Func<CacheEvictionPolicy> evictionPolicy) where T : class
{
}
public void Remove(string key)
{
}
private string GetOutputCacheKey(string key)
{
}
}

In our web.config we don’t need any code to set the custom provider. InStead we create an Initialization module.

[ModuleDependency(typeof(EPiServer.Commerce.Initialization.InitializationModule))]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class WebApiInitializationModule : IConfigurableModule
{
public void Initialize(InitializationEngine context)
{
}
public void ConfigureContainer(ServiceConfigurationContext context)
{
context.Container.Configure(x => { x.For<IObjectInstanceCache>().Use<GmgObjectInstanceCache>(); });
}
public void Preload(string[] parameters)
{
}
public void Uninitialize(InitializationEngine context)
{
}
}

Now if you tried to run this code, you should see the Get and Add being hit in your debugger. The object cacher works for CMS and commerce.

EPiServerOutputCacheProvider

The next issue we will encounter is the OutputCacheProvider not supported error. This one is more dependent on your implementation. The reason for this error is that you need to set specific Episerver cache dependencies when you create a custom implementation.  If you haven’t set these cache item dependencies, the site will fail.

If you ever tried to implement a custom output cache provider in a web forms solution and wanted to create custom cache provider policies, we would override the SetCachePolicy() method on the GMG Base Page to set these missing properties.

When we work with MVC we don’t have a concept of a base page.  Instead, if you want to implement a more custom per-page caching policy, you can create a custom OutputCacheAttribute and add any custom logic that you want to in there.

This means that if you want to cache some content, you will need to create a custom OutputCacheAttribute to decorate your pages/blocks to set these essential cache items. The code below that should stop your website was mainly ripped from another blog, but the code should look similar to this:

public class GmgOutputCacheAttribute : OutputCacheAttribute
{        public override void OnActionExecuting(ActionExecutingContext filterContext)
{
try
{
string cacheVaryByCustom = EPiServer.Configuration.Settings.Instance.HttpCacheVaryByCustom;
int cacheVaryByCustomLength = cacheVaryByCustom.Length;
HttpContext.Current.Response.Cache.SetVaryByCustom(cacheVaryByCustom);
string[] cacheVaryByParams = EPiServer.Configuration.Settings.Instance.HttpCacheVaryByParams;
foreach (string varyBy in cacheVaryByParams)
{
HttpContext.Current.Response.Cache.VaryByParams[varyBy] = true;
}
HttpContext.Current.Response.Cache.SetValidUntilExpires(true);
// Cannot use cache item dependency when using OutputCacheProvider
// Response.AddCacheItemDependency(DataFactoryCache.VersionKey);
HttpContext.Current.Response.Cache.AddValidationCallback(new HttpCacheValidateHandler(ValidateOutputCache), null);
var cacheExpiration = DateTime.Now + EPiServer.Configuration.Settings.Instance.HttpCacheExpiration;
var pageRouteHelper = EPiServer.ServiceLocation.ServiceLocator.Current.GetInstance<EPiServer.Web.Routing.PageRouteHelper>();
var currentPage = pageRouteHelper.Page;
var stopPublish = currentPage != null ? currentPage.StopPublish : DateTime.MaxValue;
HttpContext.Current.Response.Cache.SetExpires(cacheExpiration < stopPublish ? cacheExpiration : stopPublish);
HttpContext.Current.Response.Cache.SetCacheability(EPiServer.Configuration.Settings.Instance.HttpCacheability);
}
catch(Exception ex)
{ }
base.OnActionExecuting(filterContext);
}
private static string GetUrlToRemove(ActionExecutingContext filterContext)
{
var routeValues = new RouteValueDictionary(filterContext.ActionParameters);
var urlHelper = new UrlHelper(filterContext.RequestContext);
string action = filterContext.ActionDescriptor.ActionName;
string controller = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
return urlHelper.Action(action, controller, routeValues);
}
private static void ValidateOutputCache(HttpContext context, object data, ref HttpValidationStatus validationStatus)
{
if (!UseOutputCache(context))
{
validationStatus = HttpValidationStatus.IgnoreThisRequest;
}
}
private static bool UseOutputCache(HttpContext context)
{
return !PrincipalInfo.CurrentPrincipal.Identity.IsAuthenticated
&& EPiServer.Configuration.Settings.Instance.HttpCacheExpiration != TimeSpan.Zero
&& String.Compare(context.Request.HttpMethod, "GET", true) == 0;
}

Conclusion

In today’s guide, we’ve covered quite a lot. When you need to create a custom output cache provider to implement something like donut caching within Episerver, you generally have to jump through a few more hoops than you would in a standard MVC build.  When implementing a custom output cache provider, Episerver recommends using Object Caching and the IObjectInstanceCache interface.  Doing this will also make your debugging issue a lot easier.

As EpiSErver is expecting certain dependencies to be set to perform it’s caching, you won’t be able to use the standard OutputCache attribute in your project.  Instead, you will need to create a custom attribute that sets these dependencies whenever you want to cache. If you don’t, Episerver will throw a server error.

Jon D Jones

Software Architect, Programmer and Technologist Jon Jones is founder and CEO of London-based tech firm Digital Prompt. He has been working in the field for nearly a decade, specializing in new technologies and technical solution research in the web business. A passionate blogger by heart , speaker & consultant from England.. always on the hunt for the next challenge

More Posts

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *