Jeremy Davis
Jeremy Davis
Sitecore, C# and web development
Article printed from: https://blog.jermdavis.dev/posts/2016/edge-cases-of-caching

Edge cases of caching

Published 30 May 2016
Updated 31 May 2016
Sitecore C# ~1 min. read

Most of the time when you need to cache things in Sitecore, it's handled for you with the built in frameworks for data and UI layer caches. Usually your problems are solved by tuning the sizes of the caches and configuring the right caching settings on your UI components. But what happens when you've got bits of data which you want to cache that are neither components or items?

Recently I was doing some performance tuning for a site, and noticed that there were some fairly high-effort calls required in layout code that couldn't be moved out to a component. For example, how can you optimise code to calculate attributes that go into the <html> element? I was looking at code where extension methods were injecting this data:

<!DOCTYPE html>
<html lang="@Html.CurrentLanguage()" @Html.TextDirection()>
  <head>
    <!-- metadata components etc. go here -->
  </head>
  <body>
    <!-- placeholders and body markup -->
  </body>
</html> 

					

Now layouts are never cached by Sitecore, so these extension method calls will always run. What can be done to speed them up?

The pattern I decided to experiment with was to add these bits of data into the HTML cache with some custom code. That means the expensive bit of these calls only gets made once, but the cached values will still get cleared out by publishing operations - ensuring that pages will be updated to match any content changes.

There were a number of these extension methods in the code which could benefit from being cached, so to try and write one bit of code which could be reused for each of these methods I added helper function which each of the extensions could use:

private static string htmlCacheable(string key, Func<string> dataFn)
{
    string value = Sitecore.Context.Site.SiteInfo.HtmlCache.GetHtml(key);

    if (string.IsNullOrWhiteSpace(value))
    {
        value = dataFn();
        Sitecore.Context.Site.SiteInfo.HtmlCache.SetHtml(key, value);
    }

    return value;
}

					

The method checks whether the current <site/>‘s HTML cache contains the specified key. If it does this value is returned. But if the cache returns nothing then the value which needs to be cached is computed by calling the data generating function, before adding the result to the cache. Since the cache is found based on the context site, you don't have to worry about which site is running this code.

That means that the code for individual extension methods can go from their old structure:

public static string CurrentLanguage(this HtmlHelper html)
{
    // code to calculate the right value
    return value;
}

					

To this structure:

public static string CurrentLanguage(this HtmlHelper html)
{
    string key = "layout::language";
    Func<string> dataFn = () =>
    {
        // code to calculate the right value
        return value;
    };

    return htmlCacheable(key, dataFn);
}

					

This works great for simple helpers, as long as you think of a unique value for your cache key. However, some of the extension methods I was looking at could take parameters. In that situation, it's important to ensure you use all the parameter values when you're creating your cache key. That means you'll get a separate cache entry for each distinct call. For example:

public static string CalculateCorrectSiteLogoMarkup(this HtmlHelper html, int width, int height)
{
    string key = string.Format("layout::logo|width={0}|height={1}", width, height);
    Func<string> dataFn = () =>
    {
        // code to calculate the right value
        return value;
    };

    return htmlCacheable(key, dataFn);
}

					

And with that, you can cache pretty much any of the long running functions that might be hiding behind a layout...

↑ Back to top