LibrarySites.Banner

Output Caching by Variable Parameter Values with the Sitecore ASP.NET CMS

This blog post suggests a technique to vary output caching by custom parameters that vary at runtime with the Sitecore ASP.NET web Content Management System (CMS). For background information, please read at least the first blog post linked in the Resources section at the end of this page.

Sitecore lets us vary output caching by a number of criteria, including arbitrary parameters that we can pass to a presentation component. But what if we need to determine the values of those parameters at runtime rather than passing static values? For example, what if the output of a sublayout needs to vary depending on the country associated with the geographic location of the client?

Sitecore uses the Sitecore.Web.UI.WebControls.Sublayout web control to invoke sublayouts. We need to override this class to manipulate the cache key after ASP.NET creates this web control, but before that web control adds the user control to itself.

The best way I could think to do this involves overriding the GetChildControls() method of the Sublayout web control. That method invokes GetCacheKey() to determine whether output generated by the sublayout under equivalent processing conditions exists in the cache. If it does not, the Sublayout control adds the user control to the control hierarchy; otherwise it adds what is basically a LiteralControl that retrieves the cached output. In the GetChildControls() method, the first thing we need to rewrite the value of the Parameters property to replace tokens that represent the dynamic values with the runtime values for those tokens.

For example, maybe we want a sublayout to generate new output every minute. We cannot use the Sitecore UI to pass the current minute to the sublayout because we need to calculate that timestamp at runtime. To achieve the objective, we can pass parameters such as minute=true to the sublayout, and rewrite the value of the minute parameter at runtime.

To meet such requirements, you can implement a solution based on the following untested prototype:

namespace Sitecore.Sharedsource.Web.UI.WebControls
{
  using System.Linq;
 
  using Assert = Sitecore.Diagnostics.Assert;
 
  using SC = Sitecore;
 
  public class Sublayout : SC.Web.UI.WebControls.Sublayout
  {
    protected override void CreateChildControls()
    {
      if (!string.IsNullOrEmpty(this.Parameters))
      {
        SC.Collections.SafeDictionary<string> values = SC.Web.WebUtil.ParseQueryString(this.Parameters);
        Assert.IsNotNull(values, "values");
 
        if (values.Keys.Contains("minute"))
        {
          this.Parameters = SC.Web.WebUtil.ReplaceUrlParameter(
            '?' + this.Parameters,
            "minute",
            SC.DateUtil.IsoNow.Substring(0, 13), // strip the seconds
            false /*append*/ ).TrimStart('?');
        }
      }
 
      base.CreateChildControls();
    }
  }
}

Now when our class invokes GetCacheKey(), the value will include the rewritten value for the minute parameter. Remember that you will need to replace any <sc:sublayout> controls in your layouts and sublayouts with your own prefix that corresponds to your own Sublayout class.

We could add logic to the GetCacheKey() method to determine the current minute and add it to the cache key, but there is a slight chance that the minute could change between the invocation of GetCacheKey() that determines whether to bind the user control and the invocation of GetCacheKey() that retrieves that cached output. Anyway, I wanted to provide this example as a general pattern, not as a specific solution that you should actually implement (this would leave cache entries from previous minutes in the cache, which would increase memory consumption until the next publication that clears the output cache). The blog post linked in the Resources section at the end of this page provides a better solution for scheduling expiration of specific entries from the output cache.

To make this work when binding Sublayouts to placeholders, we need to override the class that Sitecore uses to create the Sublayout web controls to return an instance of our custom class rather than the default.

namespace Sitecore.Sharedsource.Web.UI
{
  using System.Collections.Specialized;
  using System.Web.UI;
 
  using SC = Sitecore;
 
  public class SublayoutRenderingType : Sitecore.Web.UI.SublayoutRenderingType
  {
    public override Control GetControl(NameValueCollection parameters, bool assert)
    {
      SC.Sharedsource.Web.UI.WebControls.Sublayout sublayout =
        new SC.Sharedsource.Web.UI.WebControls.Sublayout();
 
      foreach (string name in parameters.Keys)
      {
        string str = parameters[name];
        Sitecore.Reflection.ReflectionUtil.SetProperty((object)sublayout, name, (object)str);
      }
 
      return (Control)sublayout;
    }
  }
}

You can use a Web.config include file (Sitecore.Sharedsource.SublayoutRenderingType.config in my case) to cause Sitecore to use this class:

<configuration xmlns:patch="https://www.sitecore.com/xmlconfig/">
  <sitecore>
    <renderingControls>
      <control template="sublayout">
        <patch:attribute name="type">Sitecore.Sharedsource.Web.UI.SublayoutRenderingType, Sitecore.Sharedsource</patch:attribute>
      </control>
    </renderingControls>
  </sitecore>
</configuration>

To achieve the same result for XSL controls, we could take a similar approach with the Sitecore.Web.UI.WebControls.XslFile web control and the Sitecore.Web.UI.XslControlRenderingType class used to instantiate that type when binding XSL renderings to placeholders. To achieve the same result for web controls, we could override the GetCachingKey() method to include the Parameters parsing logic. So any specific logic for rewriting the Parameters property probably belongs in a helper class.

What we do not want to do is add properties such as VaryByMinute. If we did that, we would need to hack into various UIs to expose those specific properties and save them in layout details, and something down the line might lose those values.

Resources