Episerver caching issue with .NET 4.7

Update 1: The bug is fixed in .NET 4.7.1 (thanks to Pascal van der Horst for the information)

Update 2: The related bug is fixed in CMS Core 10.10.2 and 9.12.5. If upgrading to that version is not an option, you can contact Episerver support service for further assistance.

Original post:

If you are using Episerver and update to .NET 4.7 (even involuntarily, such as you are using DXC/Azure to host your websites. Microsoft updated Azure to .NET 4.7 on June 26th) , you might notice some weird performance issues. If your servers are in Europe, Asia or Australia, then you can see a peak in memory usage. If your servers in North America, then you can see the number of database calls increased. In both cases, your website performance is affected, the former can cause your websites to constantly restarts as memory usage reaches a threshold limit, and even more obvious in the latter. Why?

It was a known issue in .NET 4.7, as mentioned here: https://support.microsoft.com/en-us/help/4035412/fix-expiration-time-issue-when-you-insert-items-by-using-the-cache-ins

So TL;DR: when Cache.Insert is called with absolute expiration policy, it inserts a cache entry with a time of expiration. It’s supposedly to be in UTC, but a bug in .NET 4.7 caused the entry to be added with local time. So you can see the problem here: if you are in a GMT+n timezone, the cache entry will stay for much longer than the intended time (for example, 2h15m instead of just 15m). And if you are in a GMT-n timezone, the cache entry will immediately expired as soon as it is added, render the caching useless. The only exception is if your server is in UK – GMT+0, then the cache expiration will work as it should.

This, however, does not affect the cache entries added with sliding expiration policy.

Of course, this is bad. But can you do something about it? Sure.

  • If you are hosting on your own servers, hold off installation of .NET 4.7 until further notice.
  • If you are on Azure, where .NET 4.7 is inevitable, then you have several options:
    • Wait for Microsoft to fix it. Microsoft acknowledged the issue on July 19th, but there has been no updates since.
    • Wait for the next version of CMS Core. There is a fix which mitigates the issue.
    • Or you can try this workaround by overriding the default implementation of IObjectInstanceCache.
public class CustomHttpRuntimeCache : HttpRuntimeCache, IObjectInstanceCache, IDisposable
    {
        public override void Insert(string key, object value, CacheEvictionPolicy evictionPolicy)
        {
            if (evictionPolicy == null)
            {
                HttpRuntime.Cache[key] = value;
                return;
            }
            EnsureMasterKeyDependencies(evictionPolicy.MasterKeys);
            switch (evictionPolicy.TimeoutType)
            {
                case CacheTimeoutType.Undefined:
                    HttpRuntime.Cache.Insert(key, value, CreateCacheDependency(evictionPolicy));
                    return;
                case CacheTimeoutType.Sliding:
                    HttpRuntime.Cache.Insert(key, value, CreateCacheDependency(evictionPolicy), System.Web.Caching.Cache.NoAbsoluteExpiration, evictionPolicy.Expiration);
                    return;
                case CacheTimeoutType.Absolute:
                    HttpRuntime.Cache.Insert(key, value, CreateCacheDependency(evictionPolicy), DateTime.UtcNow.Add(evictionPolicy.Expiration), System.Web.Caching.Cache.NoSlidingExpiration);
                    return;
            }
        }
        private void EnsureMasterKeyDependencies(IEnumerable<string> masterKeys)
        {
            if (masterKeys == null)
            {
                return;
            }
 
            foreach (var masterKey in masterKeys)
            {
                if (HttpRuntime.Cache[masterKey] == null)
                {
                    HttpRuntime.Cache.Insert(masterKey, new Object(), null, System.Web.Caching.Cache.NoAbsoluteExpiration, System.Web.Caching.Cache.NoSlidingExpiration, CacheItemPriority.NotRemovable, null);
                }
            }
        }
 
        private CacheDependency CreateCacheDependency(CacheEvictionPolicy evictionPolicy)
        {
            if ((evictionPolicy.CacheKeys == null) &&
                (evictionPolicy.MasterKeys == null) &&
                (evictionPolicy.Files == null))
            {
                // No cache dependency requested
                return null;
            }
 
            var cacheKeys = evictionPolicy.CacheKeys;
            if (cacheKeys == null)
            {
                cacheKeys = evictionPolicy.MasterKeys;
            }
            else if (evictionPolicy.MasterKeys != null)
            {
                cacheKeys = cacheKeys.Union(evictionPolicy.MasterKeys).ToArray();
            }
 
            return new CacheDependency(evictionPolicy.Files, cacheKeys);
        }
}

And a custom implementation of `ISynchronizedObjectInstanceCache` :

        public class CustomRemoteSynchronizationCache : RemoteCacheSynchronization
        {
            public CustomRemoteSynchronizationCache(IObjectInstanceCache localCache) : base(localCache)
            {
            }

            public CustomRemoteSynchronizationCache(IObjectInstanceCache localCache, IEventRegistry eventService) : base(localCache, eventService)
            {
            }
        }

Now you can see this is an obvious workaround: RemoteCacheSynchronization is a class in Internal namespace, so you should not be using it in your solution. However, this is a circumstance where it’s justified to do so.

And then register this class as the implementation of IObjectInstanceCache and `ISynchronizedObjectInstanceCache` in one of your IConfigurableModule. To the best result, make sure this module has ModuleDependency on `FrameworkInitialization`

            services.RemoveAll(typeof(IObjectInstanceCache));
            services.RemoveAll(typeof(ISynchronizedObjectInstanceCache));
            services.AddSingleton<IObjectInstanceCache, CustomHttpRuntimeCache>();
            services.AddSingleton<ISynchronizedObjectInstanceCache, CustomRemoteSynchronizationCache>();

When Microsoft releases the fix for this issue, or when Episerver releases a fix for CMS Core for this issue, the above workaround should be removed.

3 thoughts on “Episerver caching issue with .NET 4.7

Leave a Reply

Your email address will not be published. Required fields are marked *