SaaS CMS has officially launched! Learn more now.

Ha Bui
May 7, 2020
  3757
(0 votes)

Another Fun and Profit with IMemory caching

Hi all,

As you might know Quan Mai already had a great post: https://world.episerver.com/blogs/Quan-Mai/Dates/2019/12/use-memorycache-in-your-project---for-fun-and-profit/

Today I want to introduce other way base on 

Microsoft.Extensions.Caching.Memory

Some references for you:

1. https://www.nuget.org/packages/Microsoft.Extensions.Caching.Memory/

2. https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycache?view=dotnet-plat-ext-3.1

If you make a search with "IMemory" then quite a lot of tutorial but almost is in .Net Core but doesn't blocking us to apply on .NET and EPiServer as well!

Okay, lets go step by step!

  1. Create your interceptor class: MemoryCacheInterceptor
  2. Intercep via ConfigurationModule and StructureMap interceptors pattern: MemoryCacheConfigurationModule
[InitializableModule]
[ModuleDependency(typeof(FrameworkInitialization))]
public class MemoryCacheConfigurationModule : IConfigurableModule, IInitializableModule
{
    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        context.ConfigurationComplete += (o, e) =>
        {
            e.Services.Intercept<IObjectInstanceCache>((locator, httpRuntimeCache) =>
            {
                return new MemoryCacheInterceptor();
            });
        };
    }

    public void Initialize(InitializationEngine context)
    {
    }

    public void Uninitialize(InitializationEngine context)
    {
    }
}

/// <summary>
/// Implement IObjectInstanceCache with IMemoryCache
/// <see cref="Microsoft.Extensions.Caching.Memory.IMemoryCache"/>
/// </summary>
public class MemoryCacheInterceptor : IObjectInstanceCache, IDisposable
{
    // Interesting things will come here
}

Our main focus will be MemoryCacheInterceptor class, as you see its inherited from IObjectInstanceCache and IDisposable then we should implement those methods below:

0. Properties and Constructor

private static readonly ILog _log = LogManager.GetLogger(typeof(MemoryCacheInterceptor));

private readonly IMemoryCache _memoryCache;
private readonly object _dependencyObject;
private CancellationTokenSource _rootTokenSource;

private readonly ConcurrentDictionary<string, CancellationTokenSource> _tokenSources;

public MemoryCacheInterceptor()
{
    _memoryCache = new MemoryCache(new MemoryCacheOptions());
    _dependencyObject = new object();
    _rootTokenSource = new CancellationTokenSource();
    _tokenSources = new ConcurrentDictionary<string, CancellationTokenSource>();
    _log.Info("Started NitecoMemeoryCacheInterceptor");
}

As you see:

_memoryCache : Create new instance of MemoryCache with posibility of customize option on MemoryCacheOptions like how much memory will be used ... See more in: https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheoptions?view=dotnet-plat-ext-3.1

_dependencyObject : Dummy object cache of all master keys.

_rootTokenSource that is IMemory techinique based on CancellationToken that will help us to to invalidate a set of cache all in one go. You will see relations betweeen a CacheEntry and the token later on Insert method! 

_tokenSources : Tokens provider for each cache key

1. public void Clear()

public void Clear()
{
    if (_rootTokenSource != null && !_rootTokenSource.IsCancellationRequested && _rootTokenSource.Token.CanBeCanceled)
    {
        _rootTokenSource.Cancel();
        _rootTokenSource.Dispose();
    }

    _rootTokenSource = new CancellationTokenSource();
}

2. public object Get(string key)

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

3. public void Remove(string key)

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

4. public void Dispose()

public void Dispose()
{
    _memoryCache.Dispose();
}

5. public void Insert(string key, object value, CacheEvictionPolicy evictionPolicy)

That is main part and we should take a coffee then focusing on ;)

...

public void Insert(string key, object value, CacheEvictionPolicy evictionPolicy)
{
    if (evictionPolicy == null)
    {
        // depend on root token only
        _memoryCache.Set(key, value, new CancellationChangeToken(_rootTokenSource.Token));
        return;
    }

    // Try setup dummy master object cache.
    EnsureMasterKeys(evictionPolicy.MasterKeys);

    using (var cacheEntry = _memoryCache.CreateEntry(key))
    {
        // Propagate tokens to current cache entry. 
        AddDependencyTokens(evictionPolicy);

        var cacheEntryOption = GetCachEntryOption(key, evictionPolicy);
        cacheEntry.SetOptions(cacheEntryOption.AddExpirationToken(new CancellationChangeToken(_rootTokenSource.Token)));
        cacheEntry.SetValue(value);
    }
}

Okay are you ready?

Simple part in the function is: Check null of evictionPolicy then insert to memory but remember to tight this cache key on our root token source to eviction all cache on Clear method! Done!

if (evictionPolicy == null)
{
    // depend on root token only
    _memoryCache.Set(key, value, new CancellationChangeToken(_rootTokenSource.Token));
    return;
}

Next we ensure master keys are ready to use (have cached object) with EnsureMasterKeys(evictionPolicy.MasterKeys) method:

private void EnsureMasterKeys(string[] masterKeys)
{
    if (masterKeys != null)
    {
        foreach (string key in masterKeys)
        {
            object cached;
            if (!_memoryCache.TryGetValue(key, out cached))
            {
                var token = _tokenSources.GetOrAdd(key, new CancellationTokenSource());
                _memoryCache.Set(key, _dependencyObject,
                    new MemoryCacheEntryOptions()
                    .SetAbsoluteExpiration(DateTimeOffset.MaxValue)
                    .SetPriority(CacheItemPriority.NeverRemove)
                    .AddExpirationToken(new CancellationChangeToken(_rootTokenSource.Token))
                    .AddExpirationToken(new CancellationChangeToken(token.Token))
                    .RegisterPostEvictionCallback(PostEvictionCallback));
            }
        }
    }
}

Our idea here is: Loop through master kesy, if it's not existed then insert with an absoluted maximum time and highest cache priority so in theory it will never been automactically removed by Garbage Collection (GC). Then create  token source for it, add the token and root token source together to the cache entry option.

Oh but what and why do we need PostEvictionCallback ? and when it will be trigerred? 

What and When:The given callback will be fired after the cache entry is evicted from the cache.  Read more

Why : Whenever the master cache is evicted then we should remove all children depend on the master token!

private void PostEvictionCallback(object key, object value, EvictionReason evictionReason, object state)
{
    CancellationTokenSource token;
    if (_tokenSources.TryRemove((string)key, out token)
        && token != null && !token.IsCancellationRequested && token.Token.CanBeCanceled)
    {
        token.Cancel();
        token.Dispose();
    }
}

Last part: Create new cache entry, add dependency tokens and of course with root token as well and then set to memory!

using (var cacheEntry = _memoryCache.CreateEntry(key))
{
    // Propagate tokens to current cache entry. 
    AddDependencyTokens(evictionPolicy);

    var cacheEntryOption = GetCachEntryOption(key, evictionPolicy);
    cacheEntry.SetOptions(cacheEntryOption.AddExpirationToken(new CancellationChangeToken(_rootTokenSource.Token)));
    cacheEntry.SetValue(value);
}

The magic part AddDependencyTokens(evictionPolicy)

private void AddDependencyTokens(CacheEvictionPolicy evictionPolicy)
{
    var dependencies = evictionPolicy.CacheKeys;
    if (dependencies == null)
    {
        dependencies = evictionPolicy.MasterKeys;
    }
    else if (evictionPolicy.MasterKeys != null)
    {
        dependencies = dependencies.Concat(evictionPolicy.MasterKeys).ToArray();
    }

    if (dependencies == null) return;

    foreach (var k in dependencies)
    {
        object v;
        _memoryCache.TryGetValue(k, out v);
    }

}

Hmm, quite strange right? Firstly, combination cache keys with master keys (we're considering cache keys and master keys are dependencies) and then just do: TryGetValue ? Is that enough? Yes, because of when you use:

using (var cacheEntry = _memoryCache.CreateEntry(key))

then you put this entry on the top of scope, see more:  Microsoft.Extensions.Caching.Memory.CacheEntry.CacheEntry

internal CacheEntry(object key, Action<CacheEntry> notifyCacheEntryDisposed, Action<CacheEntry> notifyCacheOfExpiration)
{
	...
	_scope = CacheEntryHelper.EnterScope(this);
}

and  Microsoft.Extensions.Caching.Memory.MemoryCache.TryGetValue

public bool TryGetValue(object key, out object result)
{
    ...
    value.PropagateOptions(CacheEntryHelper.Current);
    ...
}

Next is simple but important function GetCachEntryOption 

private MemoryCacheEntryOptions GetCachEntryOption(string key, CacheEvictionPolicy evictionPolicy)
{
    var cacheEntryOption = new MemoryCacheEntryOptions();

    switch (evictionPolicy.TimeoutType)
    {
        case CacheTimeoutType.Undefined:
            break;
        case CacheTimeoutType.Sliding:
            cacheEntryOption = cacheEntryOption.SetSlidingExpiration(evictionPolicy.Expiration);
            break;
        case CacheTimeoutType.Absolute:
            cacheEntryOption = cacheEntryOption.SetAbsoluteExpiration(evictionPolicy.Expiration);
            break;
    }

    var tokenSource = _tokenSources.GetOrAdd(key, new CancellationTokenSource());
    return cacheEntryOption
            .AddExpirationToken(new CancellationChangeToken(tokenSource.Token))
            .RegisterPostEvictionCallback(PostEvictionCallback);
}

Once againe you see PostEvictionCallback here with the same logic: When cache is evicted then we will cancel the token so that it will evict all others dependent cache entries!

Viola! Happy Coding!

May 07, 2020

Comments

Please login to comment.
Latest blogs
Optimizely SaaS CMS Concepts and Terminologies

Whether you're a new user of Optimizely CMS or a veteran who have been through the evolution of it, the SaaS CMS is bringing some new concepts and...

Patrick Lam | Jul 15, 2024

How to have a link plugin with extra link id attribute in TinyMce

Introduce Optimizely CMS Editing is using TinyMce for editing rich-text content. We need to use this control a lot in CMS site for kind of WYSWYG...

Binh Nguyen Thi | Jul 13, 2024

Create your first demo site with Optimizely SaaS/Visual Builder

Hello everyone, We are very excited about the launch of our SaaS CMS and the new Visual Builder that comes with it. Since it is the first time you'...

Patrick Lam | Jul 11, 2024

Integrate a CMP workflow step with CMS

As you might know Optimizely has an integration where you can create and edit pages in the CMS directly from the CMP. One of the benefits of this i...

Marcus Hoffmann | Jul 10, 2024

GetNextSegment with empty Remaining causing fuzzes

Optimizely CMS offers you to create partial routers. This concept allows you display content differently depending on the routed content in the URL...

David Drouin-Prince | Jul 8, 2024 | Syndicated blog

Product Listing Page - using Graph

Optimizely Graph makes it possible to query your data in an advanced way, by using GraphQL. Querying data, using facets and search phrases, is very...

Jonas Bergqvist | Jul 5, 2024