Try our conversational search powered by Generative AI!

Andreas J
Dec 2, 2022
  1441
(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:

<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/

Dec 02, 2022

Comments

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

to

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 Search and Navigation - Part 2 - Filter Tips

Introduction Continuing from Part 1 – Search Tips , today I will share the next part – filter tips. The platform versions used for this article are...

Binh Nguyen Thi | Jul 1, 2024

Integrating HubSpot CRM without the MA Connector

Have HubSpot CRM? Want to push user data into it from Optimizely? Don’t have any personalisation requirements with that data? Don’t want to pay $80...

Matt Pallatt | Jun 27, 2024

Keeping the website secure by updating external packages

Did you see the latest warning from Optimizely to update this package with a critical security warning? https://world.optimizely.com/documentation/...

Daniel Ovaska | Jun 27, 2024

Optimizely CMS image anonymization now available for Linux!

The famous image anonymization add-on for Optimizely CMS, with at least 5 downloads, is now finally available for use on Linux. Supports simultaneo...

Tomas Hensrud Gulla | Jun 25, 2024 | Syndicated blog