Don't miss out Virtual Happy Hour this Friday (April 26).

Try our conversational search powered by Generative AI!

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

Vincent
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
Solving the mystery of high memory usage

Sometimes, my work is easy, the problem could be resolved with one look (when I’m lucky enough to look at where it needs to be looked, just like th...

Quan Mai | Apr 22, 2024 | Syndicated blog

Search & Navigation reporting improvements

From version 16.1.0 there are some updates on the statistics pages: Add pagination to search phrase list Allows choosing a custom date range to get...

Phong | Apr 22, 2024

Optimizely and the never-ending story of the missing globe!

I've worked with Optimizely CMS for 14 years, and there are two things I'm obsessed with: Link validation and the globe that keeps disappearing on...

Tomas Hensrud Gulla | Apr 18, 2024 | Syndicated blog

Visitor Groups Usage Report For Optimizely CMS 12

This add-on offers detailed information on how visitor groups are used and how effective they are within Optimizely CMS. Editors can monitor and...

Adnan Zameer | Apr 18, 2024 | Syndicated blog