- Published on
A smarter Sitecore HTML Cache Clearer
- Authors

- Name
- Mark Gibbons
- @markgibbons25
**Update:**Sitecore 10.1+ has finally shipped with a superior cache clearing mechanism called “Clear On Content Update”, so this article is now mostly obsolete and only relevant if you are stuck on an old Sitecore version. However the new “Clear On Content Update” does require some additional configuration. Documentation is here.
Essentially my dream wishlist section at the end of this article got implemented into XM / XP. I’d like to think that this blog post might have prompted the XM product team to revisit this.
Sitecore 9.2 and 9.3 includes a “smart” HTML cache clearer. Let’s take a look at what makes it “smart”.
From the HTML caching documentation:
Sitecore clears caches for all sites you configure with the value of the
cacheHtmlproperty astrue. If you do not want the cache for a site to be cleared when you publish you can add thepreventHtmlCacheClearattribute to the site definition.
So the main change they’ve made here is that we no longer need to patch into the publish:end event for every site we define. Does that make this HTML cache clearer “smart”?
I think a smart cache clearer (of any kind — not just HTML) should at the very least try to detect which site(s) are affected by the publish and only clear affected site caches.
This is not a new concept and is in this blog post from 8 years ago by John West, another post from Mark Stiles 9 years ago, and has also been blogged again nearly 6 years ago. So it is somewhat surprising that this has not made it into the product especially when there has been some recent work done in this area.
Let’s make a better Smart HTML cache clearer
I’m essentially updating that original blog post here by incorporating the changes in Sitecore 9.2 and 9.3.
using System;
using System.Collections.Generic;
using System.Linq;
using Sitecore.Abstractions;
using Sitecore.Caching;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Events;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Events;
using Sitecore.Publishing;
using Sitecore.Sites;
using Sitecore.Web;
namespace
{
public class SmartCacheClearer
{
private const string PreventCacheClear = "preventHtmlCacheClear";
private readonly BaseCacheManager _cacheManager;
private readonly BaseSiteContextFactory _siteContextFactory;
public SmartCacheClearer(
BaseCacheManager cacheManager,
BaseSiteContextFactory siteContextFactory)
{
Assert.ArgumentNotNull(cacheManager, nameof(cacheManager));
Assert.ArgumentNotNull(siteContextFactory, nameof(siteContextFactory));
this._cacheManager = cacheManager;
this._siteContextFactory = siteContextFactory;
}
/// <summary>Clears the cache.</summary>
/// <param name="sender">The sender.</param>
/// <param name="args">The arguments.</param>
public void ClearCache(object sender, EventArgs args)
{
Assert.ArgumentNotNull(sender, nameof(sender));
Assert.ArgumentNotNull(args, nameof(args));
this.NotifyStart();
var allSites = this.GetSites();
Item rootItem = null;
bool deep = false;
var remoteEventArgs = args as PublishEndRemoteEventArgs;
if (remoteEventArgs != null)
{
var rootItemId = remoteEventArgs.RootItemId;
var rootItemDatabase = remoteEventArgs.TargetDatabaseName;
Database database;
if (Factory.GetDatabaseNames().Any(name => name.Equals(rootItemDatabase, StringComparison.OrdinalIgnoreCase)))
database = Factory.GetDatabase(rootItemDatabase);
else
database = Factory.GetDatabase("web");
if (database != null)
{
rootItem = database.GetItem(new ID(rootItemId));
}
deep = remoteEventArgs.Deep;
}
else
{
var publisher = Event.ExtractParameter(args, 0) as Publisher;
if (publisher != null)
{
rootItem = publisher.Options.RootItem;
if (rootItem != null)
{
deep = publisher.Options.Deep;
}
}
}
var sitesToClear = allSites;
if (rootItem != null)
{
var sites = allSites
.Where(x => rootItem.Paths.FullPath.ToLower().Contains(x.RootPath.ToLowerInvariant()))
.Where(x => x.Name != "modules_website")
.ToList();
if (sites.Any())
{
sitesToClear = sites;
}
}
foreach (SiteInfo info in sitesToClear)
{
var htmlCache = GetHtmlCacheForSite(new SiteContext(info));
this.ClearHtmlCacheForSite(htmlCache);
if (info.RenderingParametersCache != null)
{
info.RenderingParametersCache.Clear();
}
}
this.NotifyEnd(sitesToClear);
}
private IReadOnlyCollection<SiteInfo> GetSites()
{
List<SiteInfo> sites = this._siteContextFactory.GetSites();
return sites == null ? Array.Empty<SiteInfo>() : sites.Where<SiteInfo>(s => s.CacheHtml && s.Properties[PreventCacheClear] != "true").ToArray<SiteInfo>();
}
private HtmlCache GetHtmlCacheForSite(SiteContext siteContext)
{
return this._cacheManager.GetHtmlCache(siteContext);
}
private void ClearHtmlCacheForSite(HtmlCache htmlCache)
{
htmlCache?.Clear();
}
private void ClearHtmlCacheForItem(HtmlCache htmlCache, Item item)
{
htmlCache.RemoveKeysContaining(item.Paths.FullPath);
}
private void NotifyStart()
{
Log.Info("SmartCacheClearer: Cache clearing - start.", this);
}
private void NotifyEnd(IReadOnlyCollection<SiteInfo> sites)
{
//if (!Log.IsDebugEnabled)
// return;
Log.Info("SmartCacheClearer: Cache clearing - end. Affected sites: " + string.Join(", ", sites.Select<SiteInfo, string>(s => s.Name)), this);
}
}
}
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"
xmlns:set="http://www.sitecore.net/xmlconfig/set/"
xmlns:env="http://www.sitecore.net/xmlconfig/env/">
<sitecore>
<events>
<event name="publish:end">
<handler type="Sitecore.Publishing.HtmlCacheClearer, Sitecore.Kernel" method="ClearCache">
<patch:attribute name="type">SmartCacheClearer, Foundation.Infrastructure</patch:attribute>
<patch:attribute name="resolve">true</patch:attribute>
<sites hint="list">
<patch:delete />
</sites>
</handler>
<!--SmartCacheClearer handles these cases so patch them out-->
<handler type="Sitecore.XA.Foundation.Multisite.EventHandlers.HtmlCacheClearer, Sitecore.XA.Foundation.Multisite" method="OnPublishEnd">
<patch:delete />
</handler>
<handler type="Sitecore.Publishing.RenderingParametersCacheClearer, Sitecore.Kernel" method="ClearCache">
<patch:delete />
</handler>
</event>
<event name="publish:end:remote">
<handler type="Sitecore.Publishing.HtmlCacheClearer, Sitecore.Kernel" method="ClearCache">
<patch:attribute name="type">SmartCacheClearer, Foundation.Infrastructure</patch:attribute>
<patch:attribute name="resolve">true</patch:attribute>
<sites hint="list">
<patch:delete />
</sites>
</handler>
<!--SmartCacheClearer handles these cases so patch them out-->
<handler type="Sitecore.XA.Foundation.Multisite.EventHandlers.HtmlCacheClearer, Sitecore.XA.Foundation.Multisite" method="OnPublishEndRemote">
<patch:delete />
</handler>
<handler type="Sitecore.Publishing.RenderingParametersCacheClearer, Sitecore.Kernel" method="ClearCache">
<patch:delete />
</handler>
</event>
</events>
</sitecore>
</configuration>
Source: gist
Some notes:
- The config is for Sitecore 9.2, if you are on 9.3 it just needs a tweak to change the reference of
Sitecore.Publishing.HtmlCacheClearertoSitecore.Publishing.SmartHtmlCacheClearer. This is because in 9.2 it is still using the old handler, even though the code for the new one is there. - The code also handles
RenderingParametersCacheCleareras it is trivial to also clear the site specific Rendering Parameters Cache at the same time. - The config also patches out the SXA multisite HTML cache clearer as this supersedes the need for it.
The perfect Sitecore Cache Clearer
If we could dream up what the perfect Sitecore cache clearer would look like, I think it should start from the cache key strategy. Every cache in the system should have a smart cache key strategy that contains everything we need to be able to selectively clear specific cache entries from the cache.
It would be no doubt a non-trivial challenge to find such a cache key strategy. For example we might have a change in page item {abc} for site example. The cache key could have example-{abc} which then allows us to easily clear that entry if that single item gets published. This gets complicated when we take into account datasource items, rendering parameters, composite renderings and so on. So I might really just be dreaming that this could be possible — but where there is a will, there is a way.
What about the Item Cache? This seems to be also currently cleared for all sites on publish. It looks like this logic is deep in the Sitecore kernel so I don’t see anyway to optimize this — but I hope this can be revisited.