Quan Mai
Dec 5, 2019
  10276
(3 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

Johan Kronberg
Johan Kronberg Dec 5, 2019 11:11 AM

There's no race condition prevention in place here. Does the default implementation have that and if not, why not?

Dec 5, 2019 12:02 PM

@Johan: the cache should work as last write win, so I don't think any lock is necessary. I know that Cache has locks internally, not quite sure about MemoryCache but I'd guess it'll do the same.

Stefan Holm Olsen
Stefan Holm Olsen Dec 8, 2019 07:08 AM

Nice and clean solution, Quan.

Always fun to explore and try out new ways. Once tried something similar on .Net Core, which was a little different on dependencies.

Please login to comment.
Latest blogs
Optimizely CMS 13: Why Search & Navigation Now Means Graph Migration

Optimizely CMS 13 makes Graph a required part of the platform. Here is what that means in practice for teams moving from Search & Navigation, with ...

Wojciech Seweryn | Apr 8, 2026 |

Jhoose Security Module V3.0.0 – Site-Level Security Configuration for Optimizely

Jhoose Security Module updated for Optimizely CMS 13, introducing separate packages for CMS 12 and 13 with ongoing support and enhancements.

Andrew Markham | Apr 6, 2026 |

Searchable settings page

In my current project which has been actively developed for quite some time we have a big classic settings page. Unfortunately the placement and...

Per Nergård (MVP) | Apr 6, 2026

Using Azure Devops Pipelines in Optimizely SAAS (Configured) Commerce

Introduction When working with SAAS Commerce build service v2 your currently need to use a github repo with configured branches to start deployment...

Mark Hall | Apr 4, 2026 |

Forcing Lowercase URLs in Optimizely CMS During Auto-Translation

Learn how to fix uppercase and punctuation issues in Optimizely CMS 12 URL segments caused by LanguageManager auto-translation using a custom...

Stuart | Apr 2, 2026 |

Stott Robots Handler v7 for Optimizely CMS 13

Stott Robots Handler version 7 is now available for  Optimizely PaaS CMS 13 . This is includes updates to support the switch from a Site based...

Mark Stott | Apr 2, 2026