LibrarySites.Banner

Vary Output Caching By Ancestor with Template in the Sitecore ASP.NET CMS

This blog post describes a prototype that allows output caching to vary by an ancestor associated with a data template that is or inherits from a specific data template in the Sitecore ASP.NET web Content Management System (CMS) and Experience Platform (XP). Please see the first blog post linked in the Resources section at the end of this page for background information.

In blog post pages such as this one, what appear to be several renderings (About, RSS, Recent Posts, Tags, and Archive) should be cacheable. The default output caching criteria provided by Sitecore do not allow the desired level of control. We would have to specify a data source or pass parameters to the renderings, which would require something different for each blogger. In most cases, a main blog item will be an ancestor of each blog post item. Therefore, we can implement a more generic solution: we can vary by the blog associated with the blog post item (the context item and default data source item).

Assuming that we only need to support MVC, we can implement an mvc.renderRendering processor to add the ID of the ancestor to the cache key for the rendering. We also need a way to specify the data template associated with the ancestor. Maybe we can avoid complex rules, but we should assume that we might use this for more than one template (blogs, news, and so forth). Because we will register the mvc.renderRendering pipeline in the web.config file, we will use the configuration factory to specify the list of data templates.

First I factored out some potential reusable logic into a new abstract class between above the default RenderRenderingProcessor.

namespace SitecoreJohn.Mvc.Pipelines.Response.RenderRendering
{
  using System.Web;
 
  using Sitecore.Diagnostics;
  using Sitecore.Mvc.Pipelines.Response.RenderRendering;
 
  public abstract class CacheKeyModifyingRenderRenderingProcessor
    : RenderRenderingProcessor
  {
    protected abstract void DoProcess(RenderRenderingArgs args);
 
    public override void Process(RenderRenderingArgs args)
    {
      Assert.ArgumentNotNull(args, "args");
 
      if (args.Rendered
        || HttpContext.Current == null
        || !args.Cacheable)
      {
        return;
      }
 
      this.DoProcess(args);
    }
  }
}

Next I implemented an mvc.renderRendering pipeline processor that accepts a list of template IDs.

namespace SitecoreJohn.Mvc.Pipelines.Response.RenderRendering
{
  using System.Collections.Generic;
 
  using Sitecore.Data.Items;
  using Sitecore.Diagnostics;
  using Sitecore.Mvc.Pipelines.Response.RenderRendering;
 
  using SitecoreJohn.Data.Items;
 
  public class VaryByAncestorBasedOnTemplate : CacheKeyModifyingRenderRenderingProcessor
  {
    private List<string> _templateIDs = new List<string>();
 
    private List<TemplateItem> _templateItems = null;
 
    public void AddTemplateByID(string templateID)
    {
      Assert.ArgumentNotNullOrEmpty(templateID, "templateID");
      this._templateIDs.Add(templateID);
    }
 
    protected override void DoProcess(RenderRenderingArgs args)
    {
      Item rendering = args.PageContext.Database.GetItem(
        args.Rendering.RenderingItem.ID);
      Assert.IsNotNull(rendering, "rendering");
 
      if (rendering == null
        || rendering["VaryByAncestor"] != "1")
      {
        return;
      }
 
      Assert.ArgumentNotNull(args.PageContext, "args.PageContext");
      Assert.ArgumentNotNull(args.PageContext.Database, "args.PageContext.Database");
      Item dataSource = args.PageContext.Item;
 
      if (!string.IsNullOrWhiteSpace(args.Rendering.DataSource))
      {
        dataSource = args.PageContext.Database.GetItem(args.Rendering.DataSource);
        Assert.IsNotNull(dataSource, "dataSource: " + args.Rendering.DataSource);
      }
 
      Assert.IsNotNull(dataSource, "dataSource: args.PageContext.Item");
 
      if (this._templateItems == null)
      {
        Assert.IsTrue(this._templateIDs.Count > 0, "this.TemplateIDs.Count must be more than zero");
        this._templateItems = new List<TemplateItem>(this._templateIDs.Count);
 
        foreach (string templateId in this._templateIDs)
        {
          Item item = args.PageContext.Database.GetItem(templateId);
          Assert.IsNotNull(item, "item: " + templateId);
          TemplateItem template = new TemplateItem(item);
          Assert.IsNotNull(template, "template: " + item.Paths.FullPath);
          this._templateItems.Add(template);
        }
      }
 
      foreach(Item item in dataSource.Axes.GetAncestorsWithTemplates(
        this._templateItems.ToArray(),
        true, /*includeSelf*/
        true /*includeAncestors*/))
      {
        args.CacheKey += "_#ancestor:" + item.ID;
        break;
      }
    }
  }
}

Finally we need a web.config include file.

<configuration xmlns:patch="https://www.sitecore.com/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.renderRendering>
        <processor patch:after="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.GenerateCacheKey, Sitecore.Mvc']" type="SitecoreJohn.Mvc.Pipelines.Response.RenderRendering.VaryByAncestorBasedOnTemplate,Sitecore.Sharedsource">
          <templates hint="list:AddTemplateByID">
            <sampleitem>{76036F5E-CBCE-46D1-AF0A-4143F9B557AA}</sampleitem>
          </templates>
        </processor>
      </mvc.renderRendering>
    </pipelines>
  </sitecore>
</configuration>

For this to work, the definition items for renderings should include a VaryByAncestor checkbox, such as by adding a base template to the System/Layout/Sections/Caching data template.

Resources