Calling all developers! We invite you to provide your input on Feature Experimentation by completing this brief survey.

 

Andreas J
Dec 2, 2022
  1775
(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
Level Up with Optimizely's Newly Relaunched Certifications!

We're thrilled to announce the relaunch of our Optimizely Certifications—designed to help partners, customers, and developers redefine what it mean...

Satata Satez | Jan 14, 2025

Introducing AI Assistance for DBLocalizationProvider

The LocalizationProvider for Optimizely has long been a powerful tool for enhancing the localization capabilities of Optimizely CMS. Designed to ma...

Luc Gosso (MVP) | Jan 14, 2025 | Syndicated blog

Order tabs with drag and drop - Blazor

I have started to play around a little with Blazor and the best way to learn is to reimplement some old stuff for CMS12. So I took a look at my old...

Per Nergård | Jan 14, 2025

Product Recommendations - Common Pitfalls

With the added freedom and flexibility that the release of the self-service widgets feature for Product Recommendations provides you as...

Dylan Walker | Jan 14, 2025