Invalidate Page Cache on Configuration change in Sitefinity

by Vesselin Vassilev


Last Updated On Jun 23, 2018


Let's take this hypothetical scenario: you have a widget that is displaying a value stored in the configuration section of Sitefinity. You probably noticed that if you change the value and save it - the page does not immediately show the updated value - that's because the page output has been cached and the page does not know that it has to invalidate its cache on change of the config value. This blog post will show how to do that. 

Let's delve into a more specific use case: I store the bundled javascript file on a CDN. This file rarely changes, but when it does - I need to change the version query string of the file so that CDN will give me the fresh version and not the cached one.
To accomplish this, I have a custom configuration section where I store the version of the js file. It looks like this:

public class SdConfig : ConfigSection
{
    [ObjectInfo(Title = "JS Script Version", Description = "Current version of the js file")]
    [ConfigurationProperty("jsScriptVersion", DefaultValue = "22062018")]
    public string JsScriptVersion
    {
        get
        {
            return (string)this["jsScriptVersion"];
        }
        set
        {
            this["jsScriptVersion"] = value;
        }
    }    
}


In my base page template, I am using the js version from the config section value like this:

@if (!Telerik.Sitefinity.Services.SystemManager.IsDesignMode)
{
    var jsVersion = Config.Get<SdConfig>().JsScriptVersion;
         
    @Scripts.Render("CdnUrl/js/scripts.js?v=" + jsVersion)
}


It works nicely and the output looks like this:

<script src="CdnUrl/js/scripts.js?v=22062018"></script>


Now if I go to Administration > Settings > Advanced > Sd and change the js version to something else, e.g. 23062018 - what do you think would happen - nothing, because the page is already cached and it still shows the previous version of the config value.

Here is how to make the page cache invalidate on change of this particular config value - enter CustomOutputCacheVariationBase goodness. It is an abstract class in the Telerik.Sitefinity.Web namespace and is "the base class to override to register a mechanism to specify different output cache for the page request depending on the current context" (copied from decompiled code).

The most important method of this class is GetValue() which returns a string and Sitefinity will use that string in a VaryByHeader cache header. 
This practically means that if GetValue() returns one particular string - then this would be one page cache variation, if it returns another string - this would be another page cache variation, etc.


using SitefinityWebApp.Custom.Configuration;
using Telerik.Sitefinity.Configuration;
using Telerik.Sitefinity.Web;
 
namespace SitefinityWebApp.Custom.CacheVariations
{
    /// <summary>
    /// Output cache will be based on the value of the JsScriptVersion config value.
    /// Any change in that config will invalidate the page cache (or rather create a new page cache variation)
    /// </summary>
    public class JsVersionCacheVariation : CustomOutputCacheVariationBase
    {
        /// <summary>
        /// This could be any custom string
        /// </summary>
        public override string Key
        {
            get
            {
                return "bundle-js-version";
            }
        }
 
        /// <summary>
        /// This is what tells Sitefinity how to distinguish the different page cache variations.
        /// We will have different page cache variations for different js script versions.
        /// </summary>
        /// <returns></returns>
        public override string GetValue()
        {
            var config = Config.Get<SdConfig>();
 
            return config.JsScriptVersion;
        }
    }
}


Finally, we need to register our custom output cache variation. For that, I will create a simple MVC widget and will drop it on the base page template.

using SitefinityWebApp.Custom.CacheVariations;
using System.Web.Mvc;
using Telerik.Sitefinity.Mvc;
using Telerik.Sitefinity.Web;
using Telerik.Sitefinity.Web.UI;
 
namespace SitefinityWebApp.Mvc.Controllers
{
    /// <summary>
    /// This widget registers custom js version based output cache variation - meaning the same URL will have different
    /// html output depending on the value of the js version in the SdConfig.
    /// This widget will go to the base template.
    /// </summary>
    /// <returns></returns>
    [ControllerToolboxItem(Name = "JsVersionCacheDependencies", Title = "Js Version Based Cache", SectionName = "Admin Widgets", CssClass = "sfMvcIcn")]
    [IndexRenderMode(IndexRenderModes.NoOutput)]
    public class JsVersionCacheDependenciesController : Controller
    {
        public ActionResult Index()
        {
            // register custom cache Variation based on the js version configuration value           
            PageRouteHandler.RegisterCustomOutputCacheVariation(new JsVersionCacheVariation());
 
            // the widget has no html output itself
            return new EmptyResult();
        }
    }
}


With this widget placed on the base page template - when you change the value of the jsVersion configuration and refresh the page - it will show the updated value.

To be precise, the above method does not invalidate the existing page cache, but rather it creates a new variation of the page output cache. This means, that for some time, the server will potentially have two (or more) page cache variations, but that's ok - the old variations will be removed when their expiry time comes.

Of course this is not the only solution for this kind of a problem - one would argue that you can simply go and republish the page template and that would take care of any old page cache output. While this is true, I don't like this approach, because:
- you may forget to republish the page template
- it will create a new version of the page template which is unnecessary

The above solution is automatic and works like clockwork.

Sitefinity uses CustomOutputCacheVariationBase class in several places, including: 
- Navigation widget, via the NavigationOutputCacheVariation where it creates page cache variations based on the Roles of the user
- PersonalizationOutputCacheVariation class where it creates page cache variations based on the user segment of the user.
- A/B Testing via the ABTestingOutputCacheVariation where it creates page cache variations based on the current variation.

I've also used the above method to have page cache variations based on the geo-location of the user. So users in Australia will see different home page banners compared to users from Singapore, while they are browsing the same page, e.g. /en



Copyright © Sitefinity Development