public void Displayed(ShapeDisplayedContext context)
        {
            // TODO: Configure duration of sliding expiration

            var cacheContext = context.ShapeMetadata.Cache();

            // If the shape is not cached, evaluate the ESIs
            if (cacheContext == null)
            {
                string content;
                using (var sw = new StringWriter())
                {
                    context.ChildContent.WriteTo(sw, HtmlEncoder.Default);
                    content = sw.ToString();
                }

                ProcessESIs(ref content, GetDistributedCache);
                context.ChildContent = new HtmlString(content);
            }
            else if (!_cached.Contains(cacheContext) && context.ChildContent != null)
            {
                var    cacheEntries = GetCacheEntries(cacheContext).ToList();
                string cacheKey     = GetCacheKey(cacheContext.CacheId, cacheEntries);

                using (var sw = new StringWriter())
                {
                    context.ChildContent.WriteTo(sw, HtmlEncoder.Default);
                    var content = sw.ToString();

                    _cached.Add(cacheContext);
                    _cache[cacheKey] = content;
                    var contexts = String.Join(ContextSeparator.ToString(), cacheContext.Contexts.ToArray());
                    context.ChildContent = new HtmlString($"[[cache id='{cacheContext.CacheId}' contexts='{contexts}']]");

                    var bytes = Encoding.UTF8.GetBytes(content);

                    // Default duration is sliding expiration (permanent as long as it's used)
                    DistributedCacheEntryOptions options = new DistributedCacheEntryOptions
                    {
                        SlidingExpiration = new TimeSpan(0, 1, 0)
                    };

                    // If a custom duration is specified, replace the default options
                    if (cacheContext.Duration.HasValue)
                    {
                        options = new DistributedCacheEntryOptions
                        {
                            AbsoluteExpirationRelativeToNow = cacheContext.Duration
                        };
                    }

                    _dynamicCache.SetAsync(cacheKey, bytes, options).Wait();
                    _tagCache.Tag(cacheKey, cacheContext.Tags.ToArray());
                }
            }
        }
        private async Task SetCachedValueAsync(string cacheKey, string value, CacheContext context)
        {
            var failover = _memoryCache.Get <bool>(FailoverKey);

            if (failover)
            {
                return;
            }

            var bytes = Encoding.UTF8.GetBytes(value);

            var options = new DistributedCacheEntryOptions
            {
                AbsoluteExpiration = context.ExpiresOn,
                SlidingExpiration  = context.ExpiresSliding,
                AbsoluteExpirationRelativeToNow = context.ExpiresAfter
            };

            // Default duration is sliding expiration (permanent as long as it's used)
            if (!options.AbsoluteExpiration.HasValue && !options.SlidingExpiration.HasValue && !options.AbsoluteExpirationRelativeToNow.HasValue)
            {
                options.SlidingExpiration = new TimeSpan(0, 1, 0);
            }

            try
            {
                await _dynamicCache.SetAsync(cacheKey, bytes, options);
            }
            catch (Exception e)
            {
                _logger.LogError(e, "Failed to write the '{CacheKey}' to the dynamic cache", cacheKey);

                _memoryCache.Set(FailoverKey, true, new MemoryCacheEntryOptions()
                {
                    AbsoluteExpirationRelativeToNow = _dynamicCacheOptions.FailoverRetryLatency
                });

                return;
            }

            // Lazy load to prevent cyclic dependency
            _tagcache ??= _serviceProvider.GetRequiredService <ITagCache>();
            await _tagcache.TagAsync(cacheKey, context.Tags.ToArray());
        }
        private async Task SetCachedValueAsync(string cacheKey, string value, CacheContext context)
        {
            var bytes = Encoding.UTF8.GetBytes(value);

            var options = new DistributedCacheEntryOptions
            {
                AbsoluteExpiration = context.ExpiresOn,
                SlidingExpiration  = context.ExpiresSliding,
                AbsoluteExpirationRelativeToNow = context.ExpiresAfter
            };

            // Default duration is sliding expiration (permanent as long as it's used)
            if (!options.AbsoluteExpiration.HasValue && !options.SlidingExpiration.HasValue && !options.AbsoluteExpirationRelativeToNow.HasValue)
            {
                options.SlidingExpiration = new TimeSpan(0, 1, 0);
            }

            await _dynamicCache.SetAsync(cacheKey, bytes, options);

            // Lazy load to prevent cyclic dependency
            _tagcache ??= _serviceProvider.GetRequiredService <ITagCache>();
            await _tagcache.TagAsync(cacheKey, context.Tags.ToArray());
        }