Don't miss out Virtual Happy Hour this Friday (April 26).

Try our conversational search powered by Generative AI!

Quan Mai
Dec 5, 2019
  8316
(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
Solving the mystery of high memory usage

Sometimes, my work is easy, the problem could be resolved with one look (when I’m lucky enough to look at where it needs to be looked, just like th...

Quan Mai | Apr 22, 2024 | Syndicated blog

Search & Navigation reporting improvements

From version 16.1.0 there are some updates on the statistics pages: Add pagination to search phrase list Allows choosing a custom date range to get...

Phong | Apr 22, 2024

Optimizely and the never-ending story of the missing globe!

I've worked with Optimizely CMS for 14 years, and there are two things I'm obsessed with: Link validation and the globe that keeps disappearing on...

Tomas Hensrud Gulla | Apr 18, 2024 | Syndicated blog

Visitor Groups Usage Report For Optimizely CMS 12

This add-on offers detailed information on how visitor groups are used and how effective they are within Optimizely CMS. Editors can monitor and...

Adnan Zameer | Apr 18, 2024 | Syndicated blog