Quan Mai
Dec 5, 2019
visibility 10704
star star star star star
(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.

error Please login to comment.
Latest blogs
Composition Over Inheritance: Why Your Content Contract Is Not a Base Class

Inside the structural conformance pattern in SaaS CMS — from server-side inheritance to pipeline governance. A contract in Optimizely SaaS CMS is...

Vipin Banka | Sep 19, 2026

Optimizely Opti ID Integration: Why the API Key Was Missing from the DXP Management Portal

While working on an Optimizely CMS 12 Opti ID integration recently, we ran into an issue that initially appeared to be related to SSO authenticatio...

Madhu | Sep 18, 2026 |

Extending a Contract: What You Can Override

Contracts share fields across content types — but not every field setting can be overridden. Push the same contract onto several components, and CM...

Chirag Khanna | Sep 18, 2026 |

Building a Blazor Server UI Inside the Optimizely CMS 13 Admin Shell

What I learned shipping a Blazor Server UI inside Optimizely CMS 13 — Razor components in a NuGet package, the Blazor hub next to MapContent(),...

Stanisław Szołkowski | Sep 17, 2026 |