Skip to content

Commit

Permalink
Merge pull request #36 from marcemarc/feature/UmbracoCacheTagHelper
Browse files Browse the repository at this point in the history
Add some sort of implementation of a Preview/Debug aware CacheTagHelper
  • Loading branch information
Warren Buckley authored Sep 6, 2022
2 parents ad9a191 + b2aa1fb commit ad58f90
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 0 deletions.
22 changes: 22 additions & 0 deletions Our.Umbraco.TagHelpers/Composing/CacheTagHelperComposer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.Extensions.DependencyInjection;
using Our.Umbraco.TagHelpers.Notifications;
using Our.Umbraco.TagHelpers.Services;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Notifications;

namespace Our.Umbraco.TagHelpers.Composing
{
public class CacheTagHelperComposer : IComposer
{
// handle refreshing of content/media/dictionary cache notification to clear the cache key used for the CacheTagHelper
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddSingleton<IUmbracoTagHelperCacheKeys, UmbracoTagHelperCacheKeys>();

builder.AddNotificationHandler<ContentCacheRefresherNotification, CacheTagRefresherNotifications>();
builder.AddNotificationHandler<MediaCacheRefresherNotification, CacheTagRefresherNotifications>();
builder.AddNotificationHandler<DictionaryCacheRefresherNotification, CacheTagRefresherNotifications>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.Extensions.Caching.Memory;
using Our.Umbraco.TagHelpers.Services;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;

namespace Our.Umbraco.TagHelpers.Notifications
{
/// <summary>
/// For Use with the Our Cache TagHelper
/// We handle the published cache updating notification for content, media and dictionary
/// And then use our dictionary collection of tracked tag helper caches, created in the tag cache helper
/// loop through each one and clear the tag helpers cache
/// </summary>
public class CacheTagRefresherNotifications :
INotificationHandler<ContentCacheRefresherNotification>,
INotificationHandler<DictionaryCacheRefresherNotification>,
INotificationHandler<MediaCacheRefresherNotification>
{

private IMemoryCache _memoryCache;

private IUmbracoTagHelperCacheKeys _cacheKeys;

public CacheTagRefresherNotifications(CacheTagHelperMemoryCacheFactory cacheFactory, IUmbracoTagHelperCacheKeys cacheKeys)
{
_memoryCache = cacheFactory.Cache;
_cacheKeys = cacheKeys;
}

public void Handle(ContentCacheRefresherNotification notification) => ClearUmbracoTagHelperCache();

public void Handle(DictionaryCacheRefresherNotification notification) => ClearUmbracoTagHelperCache();

public void Handle(MediaCacheRefresherNotification notification) => ClearUmbracoTagHelperCache();

private void ClearUmbracoTagHelperCache()
{
// Loop over items in dictionary
foreach (var item in _cacheKeys.CacheKeys)
{
// The value stores the CacheTagKey object
// Looking at src code from MS TagHelper that use this object itself as the key

// Remove item from IMemoryCache
_memoryCache.Remove(item.Value);
}

// Once all items cleared from IMemoryCache that we are tracking
// Clear the dictionary out
// It will fill back up once an <our-cache> TagHelper is called/used on a page
_cacheKeys.CacheKeys.Clear();
}
}
}
10 changes: 10 additions & 0 deletions Our.Umbraco.TagHelpers/Services/IUmbracoTagHelperCacheKeys.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Mvc.TagHelpers.Cache;
using System.Collections.Generic;

namespace Our.Umbraco.TagHelpers.Services
{
public interface IUmbracoTagHelperCacheKeys
{
Dictionary<string, CacheTagKey> CacheKeys { get; }
}
}
14 changes: 14 additions & 0 deletions Our.Umbraco.TagHelpers/Services/UmbracoTagHelperCacheKeys.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc.TagHelpers.Cache;
using System.Collections.Generic;

namespace Our.Umbraco.TagHelpers.Services
{
/// <summary>
/// used to retain a list of hashed cache tag helper keys that have been created using the our cache tag helper
/// and which need to be cleared when a publish notification takes place.
/// </summary>
public class UmbracoTagHelperCacheKeys : IUmbracoTagHelperCacheKeys
{
public Dictionary<string,CacheTagKey> CacheKeys { get; } = new Dictionary<string,CacheTagKey>();
}
}
74 changes: 74 additions & 0 deletions Our.Umbraco.TagHelpers/UmbracoCacheTagHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.TagHelpers.Cache;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Our.Umbraco.TagHelpers.Services;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Web;

namespace Our.Umbraco.TagHelpers
{
/// <summary>
/// A wrapper around .net core CacheTagHelper, that is Umbraco Aware - so won't cache in Preview or Debug Mode
/// And will automatically clear it's when anything is published (optional)
/// </summary>
[HtmlTargetElement("our-cache")]
public class UmbracoCacheTagHelper : CacheTagHelper
{
private readonly IUmbracoContextFactory _umbracoContextFactory;
private readonly IUmbracoTagHelperCacheKeys _cacheKeys;

/// <summary>
/// Whether to update the cache key when any content, media, dictionary item is published in Umbraco.
/// </summary>
public bool UpdateCacheOnPublish { get; set; } = true;

public UmbracoCacheTagHelper(CacheTagHelperMemoryCacheFactory factory,
HtmlEncoder htmlEncoder,
IUmbracoContextFactory umbracoContextFactory,
IUmbracoTagHelperCacheKeys cacheKeys)
: base(factory, htmlEncoder)
{
_umbracoContextFactory = umbracoContextFactory;
_cacheKeys = cacheKeys;
}

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
using (UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext())
{
var umbracoContext = umbracoContextReference.UmbracoContext;

// we don't want to enable the cache tag helper if Umbraco is in Preview, or in Debug mode
if (umbracoContext.InPreviewMode || umbracoContext.IsDebug)
{
// Set the enabled flag to false & let base class
// of the cache tag helper do the disabling of the cache
this.Enabled = false;
}
else
{
// Now whenever anything is published in Umbraco 'the old Umbraco Cache Helper convention' was to clear out all the view memory caches
// we want to do the same by default for this tag helper
// 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
// which we can loop through when the Umbraco cache is updated to clear the tag helper cache.
// you can opt out of this by setting update-cache-on-publish="false" in the individual tag helper
if (UpdateCacheOnPublish)
{
// The base TagHelper would generate it's own CacheTagKey to create a unique hash
// but if we call it here 'too' it will fortunately be the same hash.
// so we can keep track of it & put into some dictionary or collection
// and clear all items out in that collection with our notifications on publish
var cacheKey = new CacheTagKey(this, context);
var key = cacheKey.GenerateKey();
var hashedKey = cacheKey.GenerateHashedKey();
_cacheKeys.CacheKeys.TryAdd(key, cacheKey);
}
}

await base.ProcessAsync(context, output);
}
}
}
}
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,65 @@ Alternatively if you use the `<our-link>` without child DOM elements then it wil

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

## `<our-cache>`
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.

### Without this tag helper

Essentially this is a convenience for setting

'''cshtml
<cache enabled="!UmbracoContext.IsDebug && !UmbracoContext.InPreviewMode">[Your Long Running Expensive Code Here]</cache>
'''

### With this tag helper

'''cshtml
<our-cache>[Your Long Running Expensive Code Here]</our-cache>
'''

### Clearing the Cache 'on publish'
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".

'''cshtml
<our-cache>[Your Long Running Expensive Code Here]</our-cache>
'''
is the same as:
'''cshtml
<our-cache update-cache-on-publish="true">[Your Long Running Expensive Code Here]</our-cache>
'''
But to turn it off:
'''cshtml
<our-cache update-cache-on-publish="false">[Your Long Running Expensive Code Here]</our-cache>
'''

(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)

### Examples
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.

```cshtml
<our-cache expires-on="new DateTime(2025,1,29,17,02,0)">
<p>@DateTime.Now - A set Date in time</p>
</our-cache>
<our-cache expires-after="TimeSpan.FromSeconds(120)">
<p>@DateTime.Now - Every 120 seconds (2minutes)</p>
</our-cache>
<our-cache>
<!-- A global navigation needs to be updated across entire site when phone number changes on homepage node -->
<partial name="Navigation" />
</our-cache>
```
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:
```cshtml
<our-cache update-cache-on-publish="false" vary-by="@Model.UpdateDate.ToString()">
<p>@DateTime.Now - A set Date in time</p>
</our-cache>
```

## Video 📺
[![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)

Expand Down

0 comments on commit ad58f90

Please sign in to comment.