Invalidate Page Cache on Configuration change in Sitefinity
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:
In my base page template, I am using the js version from the config section value like this:
It works nicely and the output looks like this:
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.
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.
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
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