Skip to the content

Umbraco 9 .Net Core Caching Part 1 - Cashing Shared Partial Views

I wrote and article a while back called Umbraco 8 Donut Cache.

Well, similar to that article this is also about caching shared partial views. This time around I looked at how to do this in Umbraco 9 .Net Core.

When using .Net Core this is done very differently to as it was before. In-fact it is a massive improvement and partly very easy to do. This is now done using something called a 'Cache Tag Helper'

Firstly you can find some very useful information about this here.

https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/cache-tag-helper?view=aspnetcore-5.0

Getting up and running with Cache Tag Helper in Umbraco 9

The good news is this is extremely easy and takes just a few seconds. Some common partial views that you might want to cache are the header section, main menu section, or footer section. Since these sections are usually on all pages it makes sense that we might add these to the cache.

For the sake of this tutorial we are going to cache the _MainMenu.cshtml partial view. I chose this since it serves as a good example of an issue I am going to introduce further in the article.

So how to cache the _MainMenu.cshtml partial view?

Simple....

Just add these tags around your partial view code.

<cache enabled="true" expires-after="@TimeSpan.FromMinutes(20)">
<!--your partial view code goes here.-->
</cache>

That's it as simple as that! You partial view will now be cached for 20 minutes. This will mean if you create a breakpoint inside your partial view you will notice it now no longer gets hit since the cached version is been loaded.

Actually you can add this <cache> tag around any bit of code. It does not have to be a partial view. It could be any bit of code in any of your razor views. Very cool!

Cache Tag Helper Configuration

There are a number of attributes that you can use to control how the cache works. I wont get into them too much since all the details on this can be found in the above link and I think the above setup would work great in this case.

I will just give a quick summary on some of the main attributes available.

'expires-on sets' an absolute expiration date for the cached item.

'expires-after' sets the length of time from the first request time to cache the contents.

'expires-sliding' Sets the time that a cache entry should be evicted if its value hasn't been accessed

'vary-by-header' accepts a comma-delimited list of header values that trigger a cache refresh when they change.

'vary-by-query' accepts a comma-delimited list of Keys in a query string (Query) that trigger a cache refresh when the value of any listed key changes.

'vary-by-route' accepts a comma-delimited list of route parameter names that trigger a cache refresh when the route data parameter value changes.

'vary-by-cookie' accepts a comma-delimited list of cookie names that trigger a cache refresh when the cookie values change.

'vary-by-user' specifies whether or not the cache resets when the signed-in user (or Context Principal) changes. The current user is also known as the Request Context Principal and can be viewed in a Razor view by referencing @User.Identity.Name.

'vary-by allows' for customization of what data is cached. When the object referenced by the attribute's string value changes, the content of the Cache Tag Helper is updated. Often, a string-concatenation of model values are assigned to this attribute. Effectively, this results in a scenario where an update to any of the concatenated values invalidates the cache.

For more details on these please visit this link.

https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/cache-tag-helper?view=aspnetcore-5.0

Problem Encountered! - .Net Code Cache Tag Helper How to programmatically clear the cache

So I soon saw a scenario which led into me discovering something that was not so great about the cache tag helper for .Net Core. It seems like there is easy 'out of the box' way to manage the cache in code. I found no recommended way to use my own cache key, and look it up later if I wanted to remove it from the cache.

Ok so you can easily turn the cache off by adjusting the tag helper enabled attribute to false.

<cache enabled="true">

This works fine but it doesn't really cover the scenario I had....

So imagine you are caching the _MainMenu.cshtml (which we are!). Great, but what happens if you user adds a menu item in Umbraco backend? Well that's where the problem lies. There was no great way to do this out of the box.

I was hoping that I could track a key I have set and then when the user saved the doc type related to the main menu items I could trigger some code that uses the Key I had to remove it from the cache. Unfortunately it wasn't as easy as thins. For some reason .Net Core decided to make this difficult.

Lucky for you I spent some time figuring out a work around.

Here it is...

Firstly I created these MemoryCache extension methods

This is a fairly big bit of code and quite a few extension methods so you can download the code here

https://github.com/DavidArmitage/BlogCode/blob/main/MemoryCacheExtensions.cs

I then did the following...

Step 1 - On the Cache Tag Helper populate the vary-by attribute with a unique name.

So with the Cache Tag Helper there is no way to choose what key to use. Which means you then have no idea what the key is to later look it up to remove it. Fortunately I came up with this workaround that does the trick!. I basically populate the vary-by attribute with a unique name which I then use later to look up the get associated with the cached item. A bit of a hack but it works great.

Edit your Cache Tag Helper so it looks something like this.

<cache enabled="true" vary-by="_MainMenu.cshtml" expires-after="@TimeSpan.FromMinutes(20)">

     <!--your partial view code goes here.-->
</cache>

I have used the file name of the partial view. If you had a lot of partial views with the same name then I guess you could use the full path here instead. Anything will do as long as it's unique.

Step 2 - Create a PublishedEvent to track changes to the DocType that might need the cache refreshing

As I said. It is likely that a user might add a new menu item to the doc type associated with the _MainMenu.cshtml partial file. In this case we need to make sure the cache is cleared for this partial so when the user reloads the homepage she/he can see their menu changes.

To do firstly create PublishedEvent to track changes.

If you do not yet know how to create events (aka notifications) in Umbraco 9 then you can read another article where in which I wrote about exactly that.

Creating Events (Notifications) in Umbraco 9

For the case of this tutorial I have uploaded my event code here.

https://github.com/DavidArmitage/BlogCode/blob/main/CacheTagHelperMemoryCacheFactory_PublishedEvent.cs

For this I went one step extra which you will see in my event code.

I created a 'composition' which is basically has a repeatable text string. This is to collect a list of file names of the partial views I want to remove from cache when a specific content node is published. I can then add this composition to any doc types I want and smartly connect this up to by event. 

So here is the scenario...

The user edits my content node which is called 'NavigationSettings'. This content node contains the data for not just my MainMenu.cshtml partial but also for my FooterSection.cshtml partial (which also has menu items). I am caching both partial so when the user re-publishes the 'NavigationSettings' I want to clear the cache on both partials.

This is why I decided to add a repeatable text string to the CacheSettingsComposition which has been added to the 'NavigationSettings' doc type.

My 'NavigationSettings' content node looks like this.

icachesettingscomposition

 

Step 3 - In your event look up the cache key based on the unique very-by attribute value

Take a look in the MemoryCacheExtension methods I added a link to above. You will ass that there are a few methods in there to do example what we need.

GetCacheTagKeyBy_varyBy

This gets a list of cache keys based on a list of string values we have saved on our 'NavigationSettings' content node.

public static List<CacheTagKey> GetCacheTagKeyBy_varyBy(this IMemoryCache memoryCache, IEnumerable<string> varyByValues)
{
if(varyByValues != null && varyByValues.Any())
{
List<CacheTagKey> cacheTagKeys = new List<CacheTagKey>();
var keys = GetKeys(memoryCache);
foreach (var key in keys)
{
FieldInfo varyByField = key.GetType().GetField("_varyBy", BindingFlags.NonPublic | BindingFlags.Instance);
if (varyByField != null && varyByField.GetValue(key) != null && varyByValues.Contains((string)varyByField.GetValue(key)))
cacheTagKeys.Add((CacheTagKey)key);
}
return cacheTagKeys;
}
return null;
}

Step 4 - Remove the cache keys we located in the previous step.

So now we have a list of cache keys we can go and remove them from the memory cache. I actually made a extension method to do all this in one.

public static void RemoveCacheTagKeyBy_varyBy(this IMemoryCache memoryCache, IEnumerable<string> varyByValues)
{
     var cacheTagKeys = GetCacheTagKeyBy_varyBy(memoryCache, varyByValues);
     if(cacheTagKeys != null && cacheTagKeys.Any())
     {
          foreach (var cacheTagKey in cacheTagKeys)
               memoryCache.Remove(cacheTagKey);
     }
}

This will take in your list of partial views you want to remove and then remove, look up the cache keys, and then remove them from the cache.

Problem Solved!

My Event (Notification) code looks something like this.

You will notice that I am checking to see if the content node is inheriting my ICacheSettingsComposition. If so then I get the list of partial view filenames and pushed them into my member cache extension methods.

public void Handle(ContentPublishedNotification notification)
{
foreach (IContent node in notification.PublishedEntities)
{
ClearSharedPartialViewCache(node);
if (node.ContentType.Alias.Equals("contentPage"))
{

}
}
}
public void ClearSharedPartialViewCache(IContent node)
{
IPublishedContent content = _siteService.GetContentById(node.Id);
if (content != null && content is ICacheSettingsComposition cacheSettings)
{
var cache = _cacheFactory.Cache;
cache.RemoveCacheTagKeyBy_varyBy(cacheSettings.ClearCachedSharedPartialViewsOnPublish);
}
}

More to come...

Keep an eye out for my follow up articles.

  • I am going to look at .net core distributed cache using redis
  • I am also going to figure out a great way to introduce some of this caching into our block list partial views. This will be something that is a little more dynamic.
  • I might also look at the possibility of caching the whole page.

About the author

David Armitage

.Net MVC Developer
.Net Core Developer
Umbraco Certified Master
Recruitment Professional

Hey Peeps,

I'm an entrepreneur and web developer with a passion for coding. I absolutely love working with Umbraco CMS and appreciate the Umbraco community even more.

I've got 10 years+ .Net experience and 7 years+ experience working in the recruitment industry, both coding and marketing job websites. I wanted to use my skills to help give something back to this awesome community and so I decided to build UmbraJobs.com.

My vision & fundamentals

I want this website to be a place where Umbraco Professionals and Umbraco Partners can advertise their services but at the same time I want to filter out all the under-qualified freelancers & agencies that take up the biggest part of other job & freelancer websites. We've all worked hard to gain our Umbraco certifications, partnership status, and help build the Umbraco community to what it is today. I want to see everyone get rewarded for their efforts.

Follow me on social media

If you're an Umbraco professional, partner, employer, recruiter or a passionate Umbraco community builder then I'm more than happy to hear from you. Follow me on my social media channels and please reach out if you have any needs, want help or consultation with anything Umbraco related or just want a general chat.

comments powered by Disqus

Blog Filter


How we can help?

Need help with an Umbraco project?

Need help with a project but not too sure who to approach? Send us your project brief and budget. We will provide a free consultation and can help you gather quotes from the best and most suitable freelancers or agencies.

Looking to hire an Umbraco Professional?

Have you got job vacancy and want to hire an Umbraco professional? Post a job on our website for free. Alternatively let us know your requirements and we will send suitable prospects your way.

Claim your free profile!

Are you an Umbraco Freelance Developer or Umbraco partner that wants to advertise on our site? If you work with Umbraco or registered as an Umbraco partner then you can create a profile for free.

Let's build the Umbraco Community

We're big on building the Umbraco community and we think you guys are awesome! If there's anyway at all we can help then please reach out.