Skip to content

Commit a447e60

Browse files
committed
Add some sort of implementation of a CacheTagHelper that's automatically aware of Preview and Debug
1 parent fe8f480 commit a447e60

File tree

5 files changed

+236
-0
lines changed

5 files changed

+236
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace Our.Umbraco.TagHelpers.CacheKeys
8+
{
9+
public static class CacheKeyConstants
10+
{
11+
public const string LastCacheRefreshDateKey = "LastCacheRefreshDate";
12+
}
13+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using Our.Umbraco.TagHelpers.Notifications;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
using Umbraco.Cms.Core.Composing;
8+
using Umbraco.Cms.Core.DependencyInjection;
9+
using Umbraco.Cms.Core.Notifications;
10+
11+
namespace Our.Umbraco.TagHelpers.Composing
12+
{
13+
public class CacheTagHelperComposer : IComposer
14+
{
15+
// handle refreshing of content/media/dictionary cache notification to clear the cache key used for the CacheTagHelper
16+
public void Compose(IUmbracoBuilder builder)
17+
{
18+
builder.AddNotificationHandler<ContentCacheRefresherNotification, HandleContentCacheRefresherNotification>();
19+
builder.AddNotificationHandler<MediaCacheRefresherNotification, HandleMediaCacheRefresherNotification>();
20+
builder.AddNotificationHandler<DictionaryCacheRefresherNotification, HandleDictionaryCacheRefresherNotification>();
21+
}
22+
}
23+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using Our.Umbraco.TagHelpers.CacheKeys;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
using Umbraco.Cms.Core.Cache;
8+
using Umbraco.Cms.Core.Events;
9+
using Umbraco.Cms.Core.Notifications;
10+
11+
namespace Our.Umbraco.TagHelpers.Notifications
12+
{
13+
// For Use with the Our Cache TagHelper - to store a last cache updated datetime to use in the varyby key for the Cache TagHelper, to naively break the cache on publish
14+
public class HandleContentCacheRefresherNotification : INotificationHandler<ContentCacheRefresherNotification>
15+
{
16+
private readonly IAppPolicyCache _runtimeCache;
17+
18+
public HandleContentCacheRefresherNotification(AppCaches appCaches)
19+
{
20+
_runtimeCache = appCaches.RuntimeCache;
21+
22+
}
23+
public void Handle(ContentCacheRefresherNotification notification)
24+
{
25+
// fired when content published
26+
// store DateTime, as the cachekey
27+
28+
var lastCacheRefreshDate = DateTime.UtcNow.ToString("s");
29+
//insert and override existing value in appcache
30+
_runtimeCache.Insert(CacheKeyConstants.LastCacheRefreshDateKey, () => lastCacheRefreshDate, null, false, null);
31+
32+
}
33+
}
34+
public class HandleDictionaryCacheRefresherNotification : INotificationHandler<DictionaryCacheRefresherNotification>
35+
{
36+
private readonly IAppPolicyCache _runtimeCache;
37+
public HandleDictionaryCacheRefresherNotification(AppCaches appCaches)
38+
{
39+
_runtimeCache = appCaches.RuntimeCache;
40+
}
41+
public void Handle(DictionaryCacheRefresherNotification notification)
42+
{
43+
// fired when Dictionary item updated
44+
// store DateTime, as the cachekey
45+
46+
var lastCacheRefreshDate = DateTime.UtcNow.ToString("s");
47+
//insert and override existing value in appcache
48+
_runtimeCache.Insert(CacheKeyConstants.LastCacheRefreshDateKey, () => lastCacheRefreshDate, null, false, null);
49+
50+
}
51+
}
52+
public class HandleMediaCacheRefresherNotification : INotificationHandler<MediaCacheRefresherNotification>
53+
{
54+
private readonly IAppPolicyCache _runtimeCache;
55+
public HandleMediaCacheRefresherNotification(AppCaches appCaches)
56+
{
57+
_runtimeCache = appCaches.RuntimeCache;
58+
}
59+
public void Handle(MediaCacheRefresherNotification notification)
60+
{
61+
// fired when media updated
62+
// store DateTime, as the cachekey
63+
64+
var lastCacheRefreshDate = DateTime.UtcNow.ToString("s");
65+
//insert and override existing value in appcache
66+
_runtimeCache.Insert(CacheKeyConstants.LastCacheRefreshDateKey, () => lastCacheRefreshDate, null, false, null);
67+
68+
}
69+
}
70+
71+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using Microsoft.AspNetCore.Mvc.TagHelpers;
2+
using Microsoft.AspNetCore.Razor.TagHelpers;
3+
using Our.Umbraco.TagHelpers.CacheKeys;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Text.Encodings.Web;
9+
using System.Threading.Tasks;
10+
using Umbraco.Cms.Core;
11+
using Umbraco.Cms.Core.Cache;
12+
using Umbraco.Cms.Core.Web;
13+
using Umbraco.Extensions;
14+
15+
namespace Our.Umbraco.TagHelpers
16+
{
17+
/// <summary>
18+
/// A wrapper around .net core CacheTagHelper so you remember not to cache in preview or Umbraco debug mode
19+
/// Also can create a new variance of the cache when anything is published in Umbraco if that is desirable
20+
/// </summary>
21+
[HtmlTargetElement("our-cache")]
22+
public class UmbracoCacheTagHelper : CacheTagHelper
23+
{
24+
private readonly IUmbracoContextFactory _umbracoContextFactory;
25+
private readonly IAppPolicyCache _runtimeCache;
26+
// default to true, a very 'Umbraco' convention.
27+
private bool _updateCacheKeyOnPublish = true;
28+
29+
/// <summary>
30+
/// Whether to update the cache key when any content, media, dictionary item is published in Umbraco.
31+
/// </summary>
32+
public bool UpdateCacheKeyOnPublish
33+
{
34+
get { return _updateCacheKeyOnPublish; }
35+
set { _updateCacheKeyOnPublish = value; }
36+
}
37+
38+
public UmbracoCacheTagHelper(CacheTagHelperMemoryCacheFactory factory, HtmlEncoder htmlEncoder, AppCaches appCaches, IUmbracoContextFactory umbracoContextFactory) : base(factory, htmlEncoder)
39+
{
40+
_umbracoContextFactory = umbracoContextFactory;
41+
_runtimeCache = appCaches.RuntimeCache;
42+
}
43+
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
44+
{
45+
using (UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext())
46+
{
47+
var umbracoContext = umbracoContextReference.UmbracoContext;
48+
// we don't want to enable the cache tag helper if Umbraco is in Preview, or in Debug mode
49+
if (umbracoContext.InPreviewMode || umbracoContext.IsDebug)
50+
{
51+
this.Enabled = false;
52+
await output.GetChildContentAsync();
53+
}
54+
else
55+
{
56+
// with the CacheTagHelper we are wrapping here it's really difficult to clear the cache of the Tag Helper output 'on demand'
57+
// eg when a page is published, with Umbraco's CachedPartial that's what happens, so if you change the name of a page, and the site navigation
58+
// is cached with a cached partial, the cache is automatically cleared.
59+
// it seems for CacheTagHelper the .net core advice when you want to break the cache is to update the varyby key, the previous key in the cache will be forgotten
60+
// and fall out of the cache naturally... (but I did later on find this article: https://www.umbrajobs.com/blog/posts/2021/june/umbraco-9-net-core-caching-part-1-cashing-shared-partial-views/
61+
// where it talks about being able to clear the actual cache tag using reflection.. - it won't work on loadbalanced servers - using wrong notifications!
62+
// ... so maybe that's the way people will ultimately prefer to go...
63+
// but for this tag helper we just track the last time a peace of content, dictionary item or media was published, and use that datetime in the varyby cachekey
64+
// so everytime something is published in Umbraco, the cache tag helper will have a different cache key and this will produce a new cached result
65+
// this might be a bad thing?
66+
// so we have a setting to turn this off, so the tag helper is still usable as the existing .net core cache tag helper, without caching in preview or umbraco debug
67+
// which is still handyish
68+
if (_updateCacheKeyOnPublish)
69+
{
70+
// ironically read the last cache refresh date from runtime cache, and set it to now if it's not there...
71+
var umbLastCacheRefreshCacheKey = _runtimeCache.GetCacheItem(CacheKeyConstants.LastCacheRefreshDateKey, () => GetFallbackCacheRefreshDate()).ToString();
72+
// append to VaryBy key incase VaryBy key is set to some other parameter too
73+
this.VaryBy = umbLastCacheRefreshCacheKey + "|" + this.VaryBy;
74+
// if an expiry date isn't set when using the CacheTagHelper, let's add one to be 24hrs, so when mulitple publishes occur, the versions of this taghelper don't hang around forever
75+
if (this.ExpiresAfter == null)
76+
{
77+
this.ExpiresAfter = new TimeSpan(24, 0, 0);
78+
}
79+
}
80+
81+
await base.ProcessAsync(context, output);
82+
}
83+
}
84+
85+
}
86+
private string GetFallbackCacheRefreshDate()
87+
{
88+
//this fires if the 'appcache' doesn't have a LastCacheRefreshDate set by a publish
89+
// eg after an app pool recycle
90+
// it doesn't really matter that this isn't the last datetime that something was actually published
91+
// because time tends to always move forwards
92+
// the next publish will set a new future LastCacheRefreshDate...
93+
return DateTime.UtcNow.ToString("s");
94+
95+
}
96+
97+
}
98+
}

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,37 @@ Alternatively if you use the `<our-link>` without child DOM elements then it wil
339339

340340
With this tag helper the child DOM elements inside the `<our-link>` is wrapped with the `<a>` tag
341341

342+
## `<our-cache>`
343+
This tag helper element `<our-cache>` is a wrapper around the [DotNet CacheTagHelper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/cache-tag-helper?view=aspnetcore-6.0) - it operates in exactly the same way, with the same options as the DotNet CacheTagHelper, except, it is automatically 'not enabled', when you are in Umbraco Preview or Umbraco Debug mode.
344+
345+
### Without this tag helper
346+
347+
Essentially this is a convenience for setting
348+
349+
'''cshtml
350+
<cache enabled="!UmbracoContext.IsDebug && !UmbracoContext.InPreviewMode">[Your Long Running Expensive Code Here]</cache>
351+
'''
352+
353+
### With this tag helper
354+
355+
'''cshtml
356+
<our-cache>[Your Long Running Expensive Code Here]</our-cache>
357+
'''
358+
359+
### Clearing the Cache 'on publish'
360+
The DotNet CacheTagHelper really isn't designed to make it easy to clear the cache 'on command' it's designed to vary and then fall out of cache after a period of time, but what if you want your cache instantly refreshed when a publish takes place? This tag helper has the possibility to 'vary' the cache by the last Content/Media/Dictionary Cache Refresh date.
361+
362+
'''cshtml
363+
<our-cache update-cache-key-on-publish="true">[Your Long Running Expensive Code Here]</our-cache>
364+
'''
365+
366+
With this set to true any publish will trigger a new 'cache key' and therefore a fresh version of the TagOutput will be displayed, with this set, and if no ExpiresAfter is set on the Tag, we default to 24hrs as the lifetime of the Tag's cache.
367+
368+
This is set to True by default, how opinionated of us.
369+
370+
(NB if you had a thousand tag helpers on your site, all caching large amounts of content, and new publishes to the site occurring every second - this might be detrimental to performance, so do think of the context of your site before setting this value)
371+
372+
342373
## Video 📺
343374
[![How to create ASP.NET TagHelpers for Umbraco](https://user-images.githubusercontent.com/1389894/138666925-15475216-239f-439d-b989-c67995e5df71.png)](https://www.youtube.com/watch?v=3fkDs0NwIE8)
344375

0 commit comments

Comments
 (0)