SaaS CMS has officially launched! Learn more now.

Daniel Ovaska
Sep 23, 2020
(5 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)
        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:

    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

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.

    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;
                var contentVersionRepository = ServiceLocator.Current.GetInstance<IContentVersionRepository>();
                var versions = contentVersionRepository.List(e.ContentLink);
                    var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
                    var previousPage = contentRepository.Get<PageData>(versions.ToArray()[1].ContentLink);
                        var improvedUrlCache = urlCache as ImprovedContentUrlCache;
                        if (improvedUrlCache != null)
                            _log.Information($"Removing cached urls due to content update");
        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


Vincent Sep 24, 2020 12:30 AM

Daniel, much appreciated for sharing. This is very useful to know.  

Shahram Shahinzadeh
Shahram Shahinzadeh Sep 24, 2020 09:45 AM

Thank you Daniel! Yes it seems it is a bug and we are going to investigate it a.s.a.p. 

Daniel Ovaska
Daniel Ovaska Sep 24, 2020 10:48 AM

Kill it! :)


Daniel Ovaska
Daniel Ovaska Sep 24, 2020 10:56 AM

Giuliano Dore
Giuliano Dore Sep 28, 2020 09:53 AM

Really good !

Matt Smith
Matt Smith Jan 29, 2021 11:30 AM

I noticed your suggestion uses deprecated code.

This issue was resolved by Episerver in bugs CMS-16555 and CMS-16839. Update Episerver.CMS.Core to 11.20.0+ in your projects.

Please login to comment.
Latest blogs
Getting Started with Optimizely SaaS using Next.js Starter App - Extend a component - Part 3

This is the final part of our Optimizely SaaS CMS proof-of-concept (POC) blog series. In this post, we'll dive into extending a component within th...

Raghavendra Murthy | Jul 23, 2024 | Syndicated blog

Optimizely Graph – Faceting with Geta Categories

Overview As Optimizely Graph (and Content Cloud SaaS) makes its global debut, it is known that there are going to be some bugs and quirks. One of t...

Eric Markson | Jul 22, 2024 | Syndicated blog

Integration Bynder (DAM) with Optimizely

Bynder is a comprehensive digital asset management (DAM) platform that enables businesses to efficiently manage, store, organize, and share their...

Sanjay Kumar | Jul 22, 2024

Frontend Hosting for SaaS CMS Solutions

Introduction Now that CMS SaaS Core has gone into general availability, it is a good time to start discussing where to host the head. SaaS Core is...

Minesh Shah (Netcel) | Jul 20, 2024