World is now on Opti ID! Learn more

Quan Mai
Dec 5, 2019
  33
(0 votes)

Use MemoryCache in your project - for fun and profit

Well, mostly fun.

You probably already know that cache is essential for your website and performance, and if you are using cache, you should probably be using ISynchronizationObjectInstanceCache, as it will take care of the remote cache invalidation for you, so if you scale out your site to multiple instances, everything will work.

You probably also know that the default implementation of ISynchronizationObjectInstanceCache - or more precisely, the default implementation of IObjectInstanceCache, which ISynchronizationObjectInstanceCache extends, use HttpRuntime.Cache if you are using a website. While it has been working well, HttpRuntime.Cache has been there since ASP.NET 1.1. In .NET 4.0, Microsoft introduced a new cache, ObjectCache as the abstraction of how a cache should be implemented, and MemoryCache as the default implementation. There is no apparent benefit of ObjectCache/MemoryCache over Cache except it can be used everywhere, not limited to just website. However it's newer, and probably shinnier, so let's try out for fun.

Like HttpRuntime.Cache, MemoryCache is a "local" cache. So we will just need to implement IObjectInstanceCache. If you want some sample code, here it is

    public class MemoryObjectCache : IObjectInstanceCache
    {
        private readonly MemoryCache _objectCache;
        private readonly CacheItemPolicy _masterKeyPolicy;
        private readonly object _masterObject;

        public MemoryObjectCache()
        {
            _objectCache = MemoryCache.Default;
            _masterKeyPolicy = new CacheItemPolicy();
            _masterKeyPolicy.AbsoluteExpiration = new DateTimeOffset(DateTime.MaxValue);
            _masterKeyPolicy.Priority = CacheItemPriority.NotRemovable;
            _masterObject = new object();
        }
        public void Clear()
        {
            foreach (var pair in _objectCache.ToList())
            {
                _objectCache.Remove(pair.Key);
            }
        }

        public object Get(string key)
        {
            return _objectCache.Get(key);
        }

        public void Insert(string key, object value, CacheEvictionPolicy evictionPolicy)
        {
            if (evictionPolicy == null)
            {
                HttpRuntime.Cache[key] = value;
                return;
            }
            EnsureMasterKeyDependencies(evictionPolicy.MasterKeys);

            _objectCache.Add(key, value, CreateCacheDependency(evictionPolicy));
        }

        private void EnsureMasterKeyDependencies(IEnumerable<string> masterKeys)
        {
            if (masterKeys == null)
            {
                return;
            }

            foreach (var masterKey in masterKeys)
            {
                if (_objectCache.Get(masterKey) == null)
                {
                    _objectCache.Add(masterKey, _masterObject , _masterKeyPolicy);
                }
            }
        }

        private CacheItemPolicy CreateCacheDependency(CacheEvictionPolicy evictionPolicy)
        {
            if ((evictionPolicy.CacheKeys == null) &&
                (evictionPolicy.MasterKeys == 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();
            }

            var cacheItemPolicy = new CacheItemPolicy();
            if (evictionPolicy.TimeoutType == CacheTimeoutType.Sliding)
            {
                cacheItemPolicy.SlidingExpiration = evictionPolicy.Expiration;
            }
            else if (evictionPolicy.TimeoutType == CacheTimeoutType.Absolute)
            {
                cacheItemPolicy.AbsoluteExpiration = new DateTimeOffset(DateTime.UtcNow.Add(evictionPolicy.Expiration));
            }
            cacheItemPolicy.ChangeMonitors.Add(_objectCache.CreateCacheEntryChangeMonitor(cacheKeys));
            return cacheItemPolicy;
        }

        public void Remove(string key)
        {
            _objectCache.Remove(key);
        }
    }

The implementation is fairly straightforward. The only "tricky" part is to create CacheItemPolicy which is corresponding to the CacheDependency in HttpRuntime.Cache. CacheDependency can takes the dependency keys as the constructor parameter, but CacheItemPolicy has a property named "ChangeMonitors" to do the same thing, but you would need to create an instance first.

To get this code to build, you'll need to manually add the reference to System.Runtime.Caching, as Visual Studio can't automatically resolve MemoryCache for you.

The final piece is to register our implementation to replace to the default one. As the default is registered with ServiceConfiguration attribute, it's easy to override it by doing so in an IConfigurationModule.ConfigureContainers

var services = context.Services;
services.AddSingleton<IObjectInstanceCache, MemoryObjectCache>();

But there is a catch here: the default implementation of ISynchronizationObjectInstanceCache is also registered by attribute, as a singleton. So if the timing is wrong and any code tries to get an instance of it before our code, an instance with use the default implementation of IObjectInstanceCache is created and used. The sure way, even though a bit "dirty", is to ensure that we re-register ISynchronizationObjectInstanceCache after our implementation (it's dirty because the default implementation, RemoteCacheSynchronization is in an internal namespace, and can be changed without notice. as a rule of thumb, we should try to avoid using such classes)

services.AddSingleton<IObjectInstanceCache, MemoryObjectCache>();

services.RemoveAll(typeof(ISynchronizedObjectInstanceCache));

services.AddSingleton<ISynchronizedObjectInstanceCache, RemoteCacheSynchronization>();

And that's it.

The real question is should we use this in production? Mostly, no. The default implementation is well tested and time proven, and the improvement of MemoryCache over HttpRuntime.Cache, if any, is not big enough to justify the risk. But I hope this gives you some idea of what can be done (and how to do it), and probably having some fun replacing default implementations of the framework!

Dec 05, 2019

Comments

Please login to comment.
Latest blogs
Make Global Assets Site- and Language-Aware at Indexing Time

I had a support case the other day with a question around search on global assets on a multisite. This is the result of that investigation. This co...

dada | Jun 26, 2025

The remote server returned an error: (400) Bad Request – when configuring Azure Storage for an older Optimizely CMS site

How to fix a strange issue that occurred when I moved editor-uploaded files for some old Optimizely CMS 11 solutions to Azure Storage.

Tomas Hensrud Gulla | Jun 26, 2025 |

Enable Opal AI for your Optimizely products

Learn how to enable Opal AI, and meet your infinite workforce.

Tomas Hensrud Gulla | Jun 25, 2025 |

Deploying to Optimizely Frontend Hosting: A Practical Guide

Optimizely Frontend Hosting is a cloud-based solution for deploying headless frontend applications - currently supporting only Next.js projects. It...

Szymon Uryga | Jun 25, 2025

World on Opti ID

We're excited to announce that world.optimizely.com is now integrated with Opti ID! What does this mean for you? New Users:  You can now log in wit...

Patrick Lam | Jun 22, 2025

Avoid Scandinavian Letters in File Names in Optimizely CMS

Discover how Scandinavian letters in file names can break media in Optimizely CMS—and learn a simple code fix to automatically sanitize uploads for...

Henning Sjørbotten | Jun 19, 2025 |