Skip to content

Commit ad58f90

Browse files
author
Warren Buckley
authored
Merge pull request #36 from marcemarc/feature/UmbracoCacheTagHelper
Add some sort of implementation of a Preview/Debug aware CacheTagHelper
2 parents ad9a191 + b2aa1fb commit ad58f90

File tree

6 files changed

+234
-0
lines changed

6 files changed

+234
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Our.Umbraco.TagHelpers.Notifications;
3+
using Our.Umbraco.TagHelpers.Services;
4+
using Umbraco.Cms.Core.Composing;
5+
using Umbraco.Cms.Core.DependencyInjection;
6+
using Umbraco.Cms.Core.Notifications;
7+
8+
namespace Our.Umbraco.TagHelpers.Composing
9+
{
10+
public class CacheTagHelperComposer : IComposer
11+
{
12+
// handle refreshing of content/media/dictionary cache notification to clear the cache key used for the CacheTagHelper
13+
public void Compose(IUmbracoBuilder builder)
14+
{
15+
builder.Services.AddSingleton<IUmbracoTagHelperCacheKeys, UmbracoTagHelperCacheKeys>();
16+
17+
builder.AddNotificationHandler<ContentCacheRefresherNotification, CacheTagRefresherNotifications>();
18+
builder.AddNotificationHandler<MediaCacheRefresherNotification, CacheTagRefresherNotifications>();
19+
builder.AddNotificationHandler<DictionaryCacheRefresherNotification, CacheTagRefresherNotifications>();
20+
}
21+
}
22+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using Microsoft.AspNetCore.Mvc.TagHelpers;
2+
using Microsoft.Extensions.Caching.Memory;
3+
using Our.Umbraco.TagHelpers.Services;
4+
using Umbraco.Cms.Core.Events;
5+
using Umbraco.Cms.Core.Notifications;
6+
7+
namespace Our.Umbraco.TagHelpers.Notifications
8+
{
9+
/// <summary>
10+
/// For Use with the Our Cache TagHelper
11+
/// We handle the published cache updating notification for content, media and dictionary
12+
/// And then use our dictionary collection of tracked tag helper caches, created in the tag cache helper
13+
/// loop through each one and clear the tag helpers cache
14+
/// </summary>
15+
public class CacheTagRefresherNotifications :
16+
INotificationHandler<ContentCacheRefresherNotification>,
17+
INotificationHandler<DictionaryCacheRefresherNotification>,
18+
INotificationHandler<MediaCacheRefresherNotification>
19+
{
20+
21+
private IMemoryCache _memoryCache;
22+
23+
private IUmbracoTagHelperCacheKeys _cacheKeys;
24+
25+
public CacheTagRefresherNotifications(CacheTagHelperMemoryCacheFactory cacheFactory, IUmbracoTagHelperCacheKeys cacheKeys)
26+
{
27+
_memoryCache = cacheFactory.Cache;
28+
_cacheKeys = cacheKeys;
29+
}
30+
31+
public void Handle(ContentCacheRefresherNotification notification) => ClearUmbracoTagHelperCache();
32+
33+
public void Handle(DictionaryCacheRefresherNotification notification) => ClearUmbracoTagHelperCache();
34+
35+
public void Handle(MediaCacheRefresherNotification notification) => ClearUmbracoTagHelperCache();
36+
37+
private void ClearUmbracoTagHelperCache()
38+
{
39+
// Loop over items in dictionary
40+
foreach (var item in _cacheKeys.CacheKeys)
41+
{
42+
// The value stores the CacheTagKey object
43+
// Looking at src code from MS TagHelper that use this object itself as the key
44+
45+
// Remove item from IMemoryCache
46+
_memoryCache.Remove(item.Value);
47+
}
48+
49+
// Once all items cleared from IMemoryCache that we are tracking
50+
// Clear the dictionary out
51+
// It will fill back up once an <our-cache> TagHelper is called/used on a page
52+
_cacheKeys.CacheKeys.Clear();
53+
}
54+
}
55+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Microsoft.AspNetCore.Mvc.TagHelpers.Cache;
2+
using System.Collections.Generic;
3+
4+
namespace Our.Umbraco.TagHelpers.Services
5+
{
6+
public interface IUmbracoTagHelperCacheKeys
7+
{
8+
Dictionary<string, CacheTagKey> CacheKeys { get; }
9+
}
10+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Microsoft.AspNetCore.Mvc.TagHelpers.Cache;
2+
using System.Collections.Generic;
3+
4+
namespace Our.Umbraco.TagHelpers.Services
5+
{
6+
/// <summary>
7+
/// used to retain a list of hashed cache tag helper keys that have been created using the our cache tag helper
8+
/// and which need to be cleared when a publish notification takes place.
9+
/// </summary>
10+
public class UmbracoTagHelperCacheKeys : IUmbracoTagHelperCacheKeys
11+
{
12+
public Dictionary<string,CacheTagKey> CacheKeys { get; } = new Dictionary<string,CacheTagKey>();
13+
}
14+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using Microsoft.AspNetCore.Mvc.TagHelpers;
2+
using Microsoft.AspNetCore.Mvc.TagHelpers.Cache;
3+
using Microsoft.AspNetCore.Razor.TagHelpers;
4+
using Our.Umbraco.TagHelpers.Services;
5+
using System.Text.Encodings.Web;
6+
using System.Threading.Tasks;
7+
using Umbraco.Cms.Core;
8+
using Umbraco.Cms.Core.Web;
9+
10+
namespace Our.Umbraco.TagHelpers
11+
{
12+
/// <summary>
13+
/// A wrapper around .net core CacheTagHelper, that is Umbraco Aware - so won't cache in Preview or Debug Mode
14+
/// And will automatically clear it's when anything is published (optional)
15+
/// </summary>
16+
[HtmlTargetElement("our-cache")]
17+
public class UmbracoCacheTagHelper : CacheTagHelper
18+
{
19+
private readonly IUmbracoContextFactory _umbracoContextFactory;
20+
private readonly IUmbracoTagHelperCacheKeys _cacheKeys;
21+
22+
/// <summary>
23+
/// Whether to update the cache key when any content, media, dictionary item is published in Umbraco.
24+
/// </summary>
25+
public bool UpdateCacheOnPublish { get; set; } = true;
26+
27+
public UmbracoCacheTagHelper(CacheTagHelperMemoryCacheFactory factory,
28+
HtmlEncoder htmlEncoder,
29+
IUmbracoContextFactory umbracoContextFactory,
30+
IUmbracoTagHelperCacheKeys cacheKeys)
31+
: base(factory, htmlEncoder)
32+
{
33+
_umbracoContextFactory = umbracoContextFactory;
34+
_cacheKeys = cacheKeys;
35+
}
36+
37+
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
38+
{
39+
using (UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext())
40+
{
41+
var umbracoContext = umbracoContextReference.UmbracoContext;
42+
43+
// we don't want to enable the cache tag helper if Umbraco is in Preview, or in Debug mode
44+
if (umbracoContext.InPreviewMode || umbracoContext.IsDebug)
45+
{
46+
// Set the enabled flag to false & let base class
47+
// of the cache tag helper do the disabling of the cache
48+
this.Enabled = false;
49+
}
50+
else
51+
{
52+
// Now whenever anything is published in Umbraco 'the old Umbraco Cache Helper convention' was to clear out all the view memory caches
53+
// we want to do the same by default for this tag helper
54+
// we can't track by convention as dot net tag helper cache key is hashed - but we can generte the hash key here, and add it to a dictionary
55+
// which we can loop through when the Umbraco cache is updated to clear the tag helper cache.
56+
// you can opt out of this by setting update-cache-on-publish="false" in the individual tag helper
57+
if (UpdateCacheOnPublish)
58+
{
59+
// The base TagHelper would generate it's own CacheTagKey to create a unique hash
60+
// but if we call it here 'too' it will fortunately be the same hash.
61+
// so we can keep track of it & put into some dictionary or collection
62+
// and clear all items out in that collection with our notifications on publish
63+
var cacheKey = new CacheTagKey(this, context);
64+
var key = cacheKey.GenerateKey();
65+
var hashedKey = cacheKey.GenerateHashedKey();
66+
_cacheKeys.CacheKeys.TryAdd(key, cacheKey);
67+
}
68+
}
69+
70+
await base.ProcessAsync(context, output);
71+
}
72+
}
73+
}
74+
}

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,65 @@ 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 Umbraco convention with other Cache Helpers, eg Html.CachedPartial is to clear all memory caches whenever any item is published in the Umbraco Backoffice. By default the our-cache tag helper will do the same, however you can turn this part off on an individual TagHelper basis by setting update-cache-key-on-publish="false".
361+
362+
'''cshtml
363+
<our-cache>[Your Long Running Expensive Code Here]</our-cache>
364+
'''
365+
is the same as:
366+
'''cshtml
367+
<our-cache update-cache-on-publish="true">[Your Long Running Expensive Code Here]</our-cache>
368+
'''
369+
But to turn it off:
370+
'''cshtml
371+
<our-cache update-cache-on-publish="false">[Your Long Running Expensive Code Here]</our-cache>
372+
'''
373+
374+
(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 allowing the cache to be cleared on each publish)
375+
376+
### Examples
377+
All examples will skip the cache for Umbraco preview mode and debug mode, and evict cache items anytime Umbraco publishes content, media or dictionary items.
378+
379+
```cshtml
380+
<our-cache expires-on="new DateTime(2025,1,29,17,02,0)">
381+
<p>@DateTime.Now - A set Date in time</p>
382+
</our-cache>
383+
384+
<our-cache expires-after="TimeSpan.FromSeconds(120)">
385+
<p>@DateTime.Now - Every 120 seconds (2minutes)</p>
386+
</our-cache>
387+
388+
<our-cache>
389+
<!-- A global navigation needs to be updated across entire site when phone number changes on homepage node -->
390+
<partial name="Navigation" />
391+
</our-cache>
392+
393+
```
394+
This example will turn off the automatic clearing of the tag helper cache if 'any page' is published, but still clear the cache if the individual page is published:
395+
```cshtml
396+
<our-cache update-cache-on-publish="false" vary-by="@Model.UpdateDate.ToString()">
397+
<p>@DateTime.Now - A set Date in time</p>
398+
</our-cache>
399+
```
400+
342401
## Video 📺
343402
[![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)
344403

0 commit comments

Comments
 (0)