Take the community feedback survey now.

Daniel Ovaska
Sep 23, 2020
  90
(0 votes)

Improved url caching in Episerver

If you try to get a url from the UrlResolver Episerver CMS will try to cache it for you which is great! It works well until you change the Url segment on the page. Then it fails to update that cached url and you might end up with 404s for the links to that page and its children for a couple of minutes until the cache clears. I've tried it in both versions 11.12 and 11.19 and the bug is easily reproducable in an Alloy site.
Note: This bug was later fixed in 11.20 which makes this work around unneccessary.

  1. Just change url segement (Name in URL) field on alloy track page to alloy-track-2 or similar. Publish. 
  2. Go to start page and refresh it (CTRL F5). Try clicking on alloy track in top navigation
  3. 404

Restarting the site will of course solve it. Waiting a couple of minutes will too. If you don't like any of these options or waiting for a bug fix that takes care of this issue you can use my little workaround below. It's also a nice example of how you can use standard .NET object oriented programming to extend and tweak Episerver behaviour. Find the interface you are interested in, tweak it, register it in ioc, use it.

  1. New url cache handler
    Lets make a new url cache! It will be fun! The one Episerver uses under the hood uses the IContentUrlCache interface, so let's make a new one that supports clearing the cached urls when badly needed e.g. when someone decides to change that "Name in URL" field. I'm adding a RemoveAll() method and a new master key that is common for all urls to make it easy to clear them all.
using EPiServer;
using EPiServer.Core;
using EPiServer.Framework;
using EPiServer.Framework.Cache;
using EPiServer.Framework.Web;
using EPiServer.Globalization;
using EPiServer.Logging.Compatibility;
using EPiServer.Web;
using EPiServer.Web.Internal;
using EPiServer.Web.Routing;
using EPiServer.Web.Routing.Internal;
using EPiServer.Web.Routing.Segments;
using EPiServer.Web.Routing.Segments.Internal;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Web;
using System.Web.Routing;

namespace DanielOvaska
{
    
    public class ImprovedContentUrlCache : IContentUrlCache
    {
        private const string UrlPrefix = "ep:url:";
        private const string DependecyPrefix = "ep:url:d:";
        private readonly IObjectInstanceCache _cache;
        private readonly AncestorReferencesLoader _ancestorLoader;
        private readonly TimeSpan _cacheExpirationTime;

        public ImprovedContentUrlCache(
          IObjectInstanceCache cache,
          AncestorReferencesLoader ancestorLoader,
          RoutingOptions contentOptions)
        {
            this._cache = cache;
            this._ancestorLoader = ancestorLoader;
            this._cacheExpirationTime = contentOptions.UrlCacheExpirationTime;
            if (this._cacheExpirationTime <= TimeSpan.Zero)
                throw new ArgumentException("The cache expiration time should be greater than zero");
        }

        public string Get(ContentUrlCacheContext context)
        {
            var url = this._cache.Get(this.GetCacheKey(context)) as string;
            return url;
        }

        public void Remove(ContentUrlCacheContext context)
        {
            this._cache.Remove(this.GetDependencyKey(context.ContentLink));
        }
        public void RemoveAll()
        {
            this._cache.Insert(_masterKeyForAllUrls, "cleared at " + DateTime.Now.ToLongTimeString(), null);
        }
        public void Insert(string url, ContentUrlCacheContext context)
        {
            ContentReference contentLink = context.ContentLink;
            IEnumerable<ContentReference> ancestors = this._ancestorLoader.GetAncestors(contentLink, AncestorLoaderRule.ContentAssetAware);
            TimeSpan cacheExpirationTime = this._cacheExpirationTime;
            this._cache.Insert(this.GetCacheKey(context), (object)url, new CacheEvictionPolicy(cacheExpirationTime, CacheTimeoutType.Sliding, Enumerable.Empty<string>(), this.CreateDependencyKeys(contentLink, ancestors)));
        }
        private string _masterKeyForAllUrls = "ImprovedContentUrlCache";
        internal IEnumerable<string> CreateDependencyKeys(
          ContentReference contentLink,
          IEnumerable<ContentReference> ancestors)
        {
            yield return _masterKeyForAllUrls;
            yield return this.GetDependencyKey(contentLink);
            foreach (ContentReference ancestor in ancestors)
                yield return this.GetDependencyKey(ancestor);
        }

        internal string GetCacheKey(ContentUrlCacheContext context)
        {
            return "ep:url:" + context.GetHashCode().ToString();
        }

        internal string GetDependencyKey(ContentReference contentLink)
        {
            return "ep:url:d:" + contentLink.ToReferenceWithoutVersion().GetHashCode().ToString();
        }
    }
}

2. Dependency injection of new class.
Now we must tell Episerver to use our improved url cache handler instead. You can do that easily by a few lines when configuring the IoC container:

 [InitializableModule]
    public class DependencyResolverInitialization : IConfigurableModule
    {
        public void ConfigureContainer(ServiceConfigurationContext context)
        {
            //Implementations for custom interfaces can be registered here.

            context.ConfigurationComplete += (o, e) =>
            {
                //Register custom implementations that should be used in favour of the default implementations
                context.Services.AddSingleton<IContentUrlCache,ImprovedContentUrlCache>();
...

3. Reacting to published event
Let's hook into the published event to clear url cache if and only if any editor has been tampering with url segments to avoid getting those pesky 404s. I'm going to clear them all to avoid messing with tricky cache dependencies since it also affects their children...and language versions etc. Just clear it. Changing urls on an existing page should be a rare event so it really shouldn't have any real performance issues. Let's add some code so that cache will only be cleared if the url segment has been changed. We don't want to kill the cache every time an editor publishes a typo fix to a random page. That would be bad for performance.

[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class ChangeEventInitialization : IInitializableModule
    {
        private ILogger _log = LogManager.GetLogger(typeof(ChangeEventInitialization));
        public void Initialize(InitializationEngine context)
        {
            var events = ServiceLocator.Current.GetInstance<IContentEvents>();
            events.PublishedContent += Events_PublishedContent;
        }
        private void Events_PublishedContent(object sender, EPiServer.ContentEventArgs e)
        {
            _log.Information($"Published content fired for content {e.ContentLink.ID}");
            var urlCache = ServiceLocator.Current.GetInstance<IContentUrlCache>();
            var page = e.Content as PageData;
            if(page!=null)
            {
                var contentVersionRepository = ServiceLocator.Current.GetInstance<IContentVersionRepository>();
                var versions = contentVersionRepository.List(e.ContentLink);
                if(versions.Count()>1)
                {
                    var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
                    var previousPage = contentRepository.Get<PageData>(versions.ToArray()[1].ContentLink);
                    if(previousPage.URLSegment!=page.URLSegment)
                    {
                        var improvedUrlCache = urlCache as ImprovedContentUrlCache;
                        if (improvedUrlCache != null)
                        {
                            _log.Information($"Removing cached urls due to content update");
                            improvedUrlCache.RemoveAll();
                        }
                    }
                }
            }
           
        }
        public void Uninitialize(InitializationEngine context)
        {
            var events = ServiceLocator.Current.GetInstance<IContentEvents>();
            events.PublishedContent -= Events_PublishedContent;
        }
    }

Hopefully that helps someone until Episerver fixes that cache invalidation of urls themselves. Then feel free to remove this little work around! Also remember that changing urls on a page is usually a bad idea due to SEO and incoming links anyway but that's another discussion. 

Happy coding!

Sep 23, 2020

Comments

Please login to comment.
Latest blogs
A day in the life of an Optimizely OMVP - Opticon London 2025

This installment of a day in the life of an Optimizely OMVP gives an in-depth coverage of my trip down to London to attend Opticon London 2025 held...

Graham Carr | Oct 2, 2025

Optimizely Web Experimentation Using Real-Time Segments: A Step-by-Step Guide

  Introduction Personalization has become de facto standard for any digital channel to improve the user's engagement KPI’s.  Personalization uses...

Ratish | Oct 1, 2025 |

Trigger DXP Warmup Locally to Catch Bugs & Performance Issues Early

Here’s our documentation on warmup in DXP : 🔗 https://docs.developers.optimizely.com/digital-experience-platform/docs/warming-up-sites What I didn...

dada | Sep 29, 2025

Creating Opal Tools for Stott Robots Handler

This summer, the Netcel Development team and I took part in Optimizely’s Opal Hackathon. The challenge from Optimizely was to extend Opal’s abiliti...

Mark Stott | Sep 28, 2025

Integrating Commerce Search v3 (Vertex AI) with Optimizely Configured Commerce

Introduction This blog provides a technical guide for integrating Commerce Search v3, which leverages Google Cloud's Vertex AI Search, into an...

Vaibhav | Sep 27, 2025

A day in the life of an Optimizely MVP - Opti Graph Extensions add-on v1.0.0 released

I am pleased to announce that the official v1.0.0 of the Opti Graph Extensions add-on has now been released and is generally available. Refer to my...

Graham Carr | Sep 25, 2025