How to use CacheTagHelper with content areas in Optimizely CMS 12
I might be going out on a limb here - if you have a better solution, feel very free to share it!
Upgrading your Optimizely web application from .NET Framework to .NET Core should provide a huge performance win. However, the ContentOutputCacheAttribute did not make the cut and was not migrated by Optimizely. If your web application was reliant on this, chances are you had some bottlenecks that will remain even after the upgrade. Not being able to use the output cache attribute will make those bottlenecks come alive once more.
PS. Don't have any bottlenecks? Continue reading anyway - learning how to use CacheTagHelper can dramatically improve your performance.
To once again (shame on you!) hide those bottlenecks, we can pretty easily make use of the CacheTagHelper that .NET Core provides. An unrealistically simple example of this would be:
<cache>
@Html.PropertyFor(m => m.ContentArea)
</cache>
You've now "solved" one problem (the bottleneck), but gained one new - cache invalidation.
In order to re-evaluate the code block within the cache node - you need to add vary-by-*
conditions. Since we're caching a content area, we need to make sure the cache is invalidated whenever visitor groups are applied or when content is published.
Let's first use vary-by
to depend on visitor group setup of the content area.
public static string GetContentAreaCacheDiscriminator(this IHtmlHelper htmlHelper, ContentArea contentArea)
{
var httpContext = htmlHelper.ViewContext.HttpContext;
if (contentArea == null)
{
return httpContext.Request.GetDisplayUrl();
}
var requiredRoles = string.Join(",", contentArea.Items.Select(x => x.ContentGroup));
return $"{httpContext.Request.GetDisplayUrl()}-{requiredRoles}";
}
... and in the Razor view ...
<cache vary-by="@Html.GetContentAreaCacheDiscriminator(Model.ContentArea)">
@Html.PropertyFor(m => m.ContentArea)
</cache>
The ContentGroup
property of every ContentAreaItem
in the content area seems to keeps track of what users can see the content.
So, now the cache should be invalidated if visitor groups are applied to the content area.
The ability to re-evaluate the content area whenever some nested block is published is more tricky. The only simple-ish way I can come up with is listening to the publish event, increment some counter and include that counter in the cache discriminator method.
It would look something like this:
public static class AtomicState
{
public static int Counter = 0;
public static void Increment() => Interlocked.Increment(ref Counter);
}
[InitializableModule, ModuleDependency(typeof(FrameworkInitialization))]
public class ContentEventsInitializableModule : IInitializableModule
{
private IContentEvents _contentEvents;
public void Initialize(InitializationEngine context)
{
_contentEvents = context.Locate.Advanced.GetRequiredService<IContentEvents>();
_contentEvents.PublishedContent += OnPublished;
}
public void Uninitialize(InitializationEngine context)
{
_contentEvents.PublishedContent -= OnPublished;
}
private static void OnPublished(object sender, ContentEventArgs e)
{
AtomicState.Increment();
}
}
Now we must make use of AtomicState.Counter in our cache discriminator method, so just add it to the return statement:
return $"{httpContext.Request.GetDisplayUrl()}-{AtomicState.Counter}-{requiredRoles}";
Since AtomicState.Counter
will be read zero or more times per request, and can be updated at any point, we must make it thread safe - which in this case is achieved with Interlocked.Increment(...)
.
If you wan't more granular control over what cache to invalidate, you need to check the published event for what content type was published and you need another counter for the specific case.
That's it. Thank you for your time! 😏
I originally wrote this post on my personal blog:
https://andreas.jilvero.se/blog/how-to-use-cachetaghelper-with-content-areas-in-optimizely/
Great post, I'm definitely going to go give this a try. I do however have one question about this line:
As the Items property is unfiltered, it suggests that the cache will not respect visitor groups because the cache key will include the selected content groups for all possible personalisations rather than that which the visitor qualifies for. Should this not instead grab the ContentGroup from contentArea.FilteredItems?
Finally a response, thank you!
You must be correct. As FilteredItems is already filtered by the current user, we could probably change from
to
... and include ids in the cache key instead. Do you agree?
The other option as well that works well with the cach tag helpers and the approach I taught on the CMS 12 & Commerce 14 Masterclass was to use them for Donut Hole Caching. I combine them viewmodel cached through the ISyncronizedObjectRepository with the block work ID and any dependencies then cache this with a unique reference per block.
This then caches every viewmodel based on block changes the same way the common donut hole caching technique worked. I like this approach as blocks are cached across pages rather than the whole page allowing separate block invalidation plus it make building block dependencies in to the validation keys easier.
It would hopefully keep the cache unique for the for user's with matching visitor groups and different for others so would appear viable. I would add that all of the content will have been cached already by optimizely for subsequent loads, so you won't be saving on data loads but really on processing time and any dynamic logic you may have for those blocks. It would be interesting to see some performance analytics on this.