Magnus Rahl
Nov 25, 2010
  7308
(0 votes)

Customizing output cache variation

The title might sound like something really exciting, but it’s actually something rather mundane in one way and very specific in another. So TLDR warning!

The background is that a client used too many variables with long names in the httpCacheVaryByParams attribute of the siteSettings element in the EPiServer settings (web.config/episerver.config). This attribute takes a comma separated list of querystring parameters to vary the cache by and has a max length of 256 chars, over which the site won’t start.

Background: Output caching

ASP.NET has several different cache options which operate on different levels. For example you may have put some object which is expensive to construct in the cache for reusing between requests. Another part of the cache is the output cache which caches the rendered output of pages (or portions thereof) so that the Page object and its UserControls don’t have to be constructed and rendered on every request.

The cache can be set up to vary based on various factors. What this means is that even though you may have an aspx or ascx which renders differently depending on these factors, you can still have caching enabled and have the same performance gains when you have repeated requests with these factors tuned the same way. Querystring variables are the most common example, and in EPiServer the id and epslanguage parameters are varied on to enable caching of different pages and language versions of the pages even though they render using the same aspx.

Output caching in EPiServer

The id and epslanguage parameters are configured in the mentioned httpCacheVaryByParams attribute in siteSettings. The setting is accessed through EPiServer.Configuration.Settings.Instance by the SetCachePolicy method called by the OnInit method in EPiServer.PageBase from which your page templates inherit. The siteSettings element also contains other options for the cache, like the cache duration (httpCacheExpiration attribute) expressed like an HH:MM:SS timespan, as well as the httpCacheVaryByCustom attribute which is also familiar from the standard @OutputCache directive which can be set up declaratively in aspx/ascx.

Solution attempt 1

As mentioned, it is possible to vary the output cache by other means than querystring parametes. Using httpCacheVaryByCustom in siteSettings it is possible to define parameters that you code will be asked to validate in runtime. This is done by overriding the GetVaryByCustomString in Global.asax.

My first thought was to override this to get a way to handle additional querystring parameters stored in a separate config file or whatever. I’ll show what I mean (finally some code!):

public override string GetVaryByCustomString(HttpContext context, string custom)
{
    // Handle only my expected custom variable, otherwise delegate to base class
    if ("customQuery".Equals(custom))
    {
        // These querystring parameters could have been read from some
        // config file, use constants here just to demonstrate
        var myQueryVars = new[] { "mycoolqueryvar", "myawsomequeryvar" };
        // Intersect the parameters to vary by with the paramters in the request
        var matches = myQueryVars.Intersect(context.Request.QueryString.AllKeys);
        // Break if none of the parameters are used in the request
        if (!matches.Any()) return null;
        // Build a string containing the values to vary by and return this
        var queryItems = matches.Select(s =>
            s + "=" + HttpUtility.UrlEncode(Request.QueryString[s])); 
        return string.Join("&", queryItems.ToArray());
    }
    return base.GetVaryByCustomString(context, custom);
}

What this means is that if the aspx/ascx is set to varyByCustom this method will be called. If the varyByCustom setting includes the string customQuery this method will create a string which varies with the values of the querystring parameters mycoolqueryvar and myawsomequeryvar. So it does exactly what varyByParams would do, but I ran out of space for that one, remember? This way I can just set customQuery in the httpCacheVaryByCustom in siteSettings and then put as many parameters as I like in the code or in some configuration I read.

Solution attempt 2

I then started investigating something I should have checked from the beginning. Where were all these parameters used? It turned out (as one might have suspected) that most of them were only used in one or a few aspx/ascx. The solution then felt much simpler: Just add the @OutputCache directive for those aspx:es and only keep widely used parameters (like id and epslanguage) in siteSettings.

But I then ran into trouble. ASP.NET won’t let you add the @OutputCache directive without setting its Duration property. But then how would that timeout then interact with the one set in siteSettings? It turned out that the @OutputCache setting will take precedence, which is perhaps good in some situations but not here. I want to be able to set the cache timeout in the config without having to go through loads of aspx files to set matching timeouts.

So to solve this I had to override the SetCachePolicy method in my page template. But that meant I had to set the parameters to vary by from code. I didn’t want to do this, but luckily I could find a way around it.

Public properties on a Page class are available to set in the @Page directive. So I added this to my template base class:

/// <summary>
/// Property used to set additional parameters to vary cache by.
/// This can be used from the @Page directive. Using this instead
/// of the @OutputCache directive makes it possible to use the
/// cache timeout set in the EPiServer siteSettings config element.
/// Setting @OutputCache requires setting the Duration which
/// overrides that setting. The parameters set in this property
/// are added the same way as parameters from the EPiServer
/// siteSettings
/// </summary>
public virtual string HttpCacheVaryByParams { get; set; }
/// <summary>
/// Override which adds the extra params in HttpCacheVaryByParams
/// to base.Response.Cache.VaryByParams
/// </summary>
protected override void SetCachePolicy()
{
    base.SetCachePolicy();
    if (String.IsNullOrEmpty(HttpCacheVaryByParams)) return;
    foreach (var param in HttpCacheVaryByParams
        .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
        .Select(s => s.Trim())
        .Where(s => !String.IsNullOrEmpty(s)))
    {
        base.Response.Cache.VaryByParams[param] = true;
    }
}

Now I can at least do this:

<%@ Page Language="C#" ... HttpCacheVaryByParams="mycoolqueryparam,myawsomequeryparam" %>
Nov 25, 2010

Comments

Nov 25, 2010 05:51 PM

Recently I was doing some maintenance on a site that uses a CDN there are many pages within the site and when caching was enabled via the site settings the CDN was not indexing page urls with unique query string variations.

From what I vaguely recall, without having to define all the unique query string variations in the HttpCacheVaryByCustom attribute within site settings you can override the GetVaryByCustomString method within the Global.asax to be like the following so it always returns a unique Url.

public override string GetVaryByCustomString(HttpContext context, string custom)
{
#if DEBUG
return base.GetVaryByCustomString(context, custom);
#endif

EPiServer.Configuration.Settings settings = EPiServer.Configuration.Settings.Instance;

if (settings.HttpCacheExpiration.TotalSeconds > 0)
return context.Request.RawUrl;

return base.GetVaryByCustomString(context, custom);
}

Magnus Rahl
Magnus Rahl Nov 25, 2010 05:56 PM

You're right, even though there are at least some hypothetical situations where this creates multiple cached objects that should be the same (querystring parameters in different order for example).

Please login to comment.
Latest blogs
Creating an Optimizely CMS Addon - Adding an Editor Interface Gadget

In   Part One   of this series, I covered getting started with creating your own AddOn for Optimizely CMS 12. This covered what I consider to be an...

Mark Stott | Aug 30, 2024

Configure your own Search & Navigation timeouts

The main blog Configure your own Search & Navigation timeouts was posted for years but you need copy the code to your application. We now bring tho...

Manh Nguyen | Aug 30, 2024

Joining the Optimizely MVP Program

Andy Blyth has been honoured as an Optimizely MVP, recognising his contributions to the Optimizely community. Learn how this achievement will enhan...

Andy Blyth | Aug 29, 2024 | Syndicated blog

Welcome 2024 Summer OMVPs

Hello, Optimizely community! We are thrilled to announce and welcome the newest members to the Optimizely Most Valuable Professionals (OMVP) progra...

Patrick Lam | Aug 29, 2024

Create custom folder type in Edit Mode

Content Folders, which are located in assets pane gadget or multichannel content gadget, allow you to add any type of block or folders. But...

Grzegorz Wiecheć | Aug 28, 2024 | Syndicated blog

Creating an Optimizely AddOn - Getting Started

When Optimizely CMS 12 was launched in the summer of 2021, I created my first AddOn for Optimizely CMS and talked about some of the lessons I learn...

Mark Stott | Aug 28, 2024