November Happy Hour will be moved to Thursday December 5th.

Quan Mai
Dec 5, 2019
  8777
(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?

Quan Mai
Quan Mai 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 SaaS CMS + Coveo Search Page

Short on time but need a listing feature with filters, pagination, and sorting? Create a fully functional Coveo-powered search page driven by data...

Damian Smutek | Nov 21, 2024 | Syndicated blog

Optimizely SaaS CMS DAM Picker (Interim)

Simplify your Optimizely SaaS CMS workflow with the Interim DAM Picker Chrome extension. Seamlessly integrate your DAM system, streamlining asset...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog

I'm running Optimizely CMS on .NET 9!

It works 🎉

Tomas Hensrud Gulla | Nov 12, 2024 | Syndicated blog

Recraft's image generation with AI-Assistant for Optimizely

Recraft V3 model is outperforming all other models in the image generation space and we are happy to share: Recraft's new model is now available fo...

Luc Gosso (MVP) | Nov 8, 2024 | Syndicated blog