Andreas J
Dec 2, 2022
(1 votes)

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:

    @Html.PropertyFor(m => m.ContentArea)

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)

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)

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:

Dec 02, 2022


Mark Stott
Mark Stott Dec 8, 2022 09:56 AM

Great post, I'm definitely going to go give this a try.  I do however have one question about this line:

var requiredRoles = string.Join(",", contentArea.Items.Select(x => x.ContentGroup));

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?

Andreas J
Andreas J Dec 8, 2022 02:27 PM

Finally a response, thank you! 

You must be correct. As FilteredItems is already filtered by the current user, we could probably change from

var requiredRoles = string.Join(",", contentArea.Items.Select(x => x.ContentGroup));


var ids = string.Join(",", contentArea.FilteredItems.Select(x => x.ContentLink.ID.ToString()));

... and include ids in the cache key instead. Do you agree?

Scott Reed
Scott Reed Dec 8, 2022 05:06 PM

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.

Mark Stott
Mark Stott Dec 8, 2022 05:07 PM

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.

Please login to comment.
Latest blogs
Optimizely Web Experimentation Metrics

Within the domain of Optimizely Web Experimentation Metrics, the emphasis is on objective key performance indicators (KPIs) selected to assess an...

Matthew Dunn | Sep 23, 2023 | Syndicated blog

Optimizely For you Intranet

Having been at Optimizely and in the CMS industry for nearly 16 years I have seen clients’ intranet requirements go from a simple site just to hous...

Robert Folan | Sep 22, 2023

Vulnerability in EPiServer.GoogleAnalytics v3 and v4

Introduction A potential security vulnerability was detected for Optimizely Google Analytics addon (including EPiServer.GoogleAnalytics and...

Bien Nguyen | Sep 20, 2023

Overriding Optimizely’s Content Recommendations Block to Implement Custom Recommendations

Introduction The Content Recommendations add-on for Optimizely CMS dynamically recommends content from your site tailored to the interests of each...

abritt | Sep 13, 2023 | Syndicated blog