LibrarySites.Banner

Custom Cache Criteria with MVC in the Sitecore ASP.NET CMS | John West | Sitecore Blog

This blog post describes on approach to adding custom cache criteria in MVC solutions that use the Sitecore ASP.NET web Content Management System (CMS) and Experience Platform (XP).

Most Sitecore solutions can improve performance and capacity by caching the output of renderings. For each managed site, Sitecore can maintain an output cache, which contains the output of renderings used on that site under different conditions. The output cache works like a large hash table; a key unique to the cache corresponds to a chunk of data generated previously under some processing conditions and stored in the cache.

For optimal performance with minimal memory consumption, you should mark as many renderings cacheable as you can, minimize the caching criteria, and use the least variable VaryBy criteria. For example, with no VaryBy criteria for a rendering, Sitecore will only invoke each cacheable rendering only once for each language in each site that uses the rendering. After adding VaryByDevice, Sitecore will invoke those renderings once for each device used to access each language of each site. After adding VaryByData, Sitecore will invoke those renderings once for each data source item passed to that rendering for each device used to access each language of the site.

Remember that you have to mark each rendering as Cacheable to get Sitecore to cache its output, and that Sitecore will only cache output when running that rendering in the context of a managed site for which the cacheHtml attribute is true.

Output cache keys always contain something to identify the rendering component and a string to identify the context language. In other words, the output of all renderings varys by language: we assume that a rendering generates different output for different languages. Before invoking the rendering, Sitecore calculates the cache key based on current processing conditions. If an entry with that key exists in the cache, Sitecore renders that result; otherwise, it invokes the rendering and adds the entry to the cache.

Varying by managed site, rendering identifier, and language may be sufficient for presentation components that generate the same output for each page, such as a footer rendering. The output of other renderings varies by additional criteria that can appear in the cache key:

  • VaryByData: By far the most common VaryBy option, VaryByData adds the data source for the rendering to the cache key. Use VaryByData for renderings that generate different output for different data source items.
  • VaryByLogin: Use VaryByUser for renderings that generate different output depending on whether or not the user has authenticated (not on who has authenticated). VaryByLogin tracks only two states: anonymous and authenticated.
  • VaryByUser: You can use VaryByUser for renderings that generate different output for different authenticated users (who has authenticated). With very large concurrent user communities, and especially with renderings that can generate a great deal of output, VaryByData can consume significant memory. Consider VaryByUser only for renderings used by a great number of pages with no other caching criteria and do not perform adequately. VaryByUser works only for authenticated users; otherwise, it works like VaryByLogin.
  • VaryByParam: If you pass parameters to renderings, you can use VaryByParm to include those parameters in the cache key.
  • VaryByDevice: You can use the VaryByDevice option to generate different cache entries for a single rendering invoked in the context of different Sitecore devices (not physical devices, although you could add a custom VaryByPhysicalDevice property).
  • VaryByQueryString: You can use the VaryByQueryString option to add all query string keys and values to the cache key. For example, if you use query strings to indicate search parameters, then the output of your search results rendering may VaryByQueryString.
  • ClearOnIndexUpdate: Similar to the VaryBy options, ClearOnIndex indicates entries that Sitecore should remove from output caches after search index rebuilding operations complete. You can use the ClearOnIndex attribute for renderings that depend heavily on search that should always present the latest results.

Collectively, these are the VaryBy options, which you may see under other names (VaryByParm, VaryByParam, Vary By Parameters, and so forth).

You may have cases where the output of your renderings varies by other criteria. For example, a rendering may generate different content based on the perceived location of the client (GeoIP information based on the IP address that submitted the HTTP request). A VaryByRegion or VaryByCountry option may be appropriate, though such is beyond the scope of this blog post. In some cases you might want to VaryByUser when users have not authenticated; in that case, you may be able to use VaryBySession. I will use VaryByIP as an example, although this does not seem very realistic.

There are three places where you can define caching options for renderings:

  1. In the rendering definition item.
  2. Where you invoke the rendering statically.
  3. Where you bind the rendering to a placeholder in layout details.

In the first two cases, designating the VaryByIP parameter should be relatively straightforward. We can add a checkbox to the rendering definition item, and we should be able to pass a flag when we invoke the rendering.

For the third case, to provide a VaryByIP checkbox, we would need to override any user interfaces that can apply layout details. This would require custom data in Sitecore’s layout details XML, which subsequent processing could alter or remove completely. We have at least two options:

  1. Use VaryByParam with a parameter such as IP=Address, and rewrite Address at runtime.
  2. Do not set VaryByIP through these user interfaces; set it in the rendering definition item and use that.

The first solution requires that users who configure caching criteria know the names of keys and to specify values. The second approach applies everywhere, not providing an option to enable or disable VaryByIP for individual uses of a rendering.

Here is an example mvc.renderRendering pipeline that rewrites parameters (the first approach):

namespace Sitecore.Sharedsource.Mvc.Pipelines.Response.RenderRendering
{
  using System.Web;
 
  using Sitecore.Diagnostics;
  using Sitecore.Mvc.Pipelines.Response.RenderRendering;
 
  public class RewriteIPParam : RenderRenderingProcessor
  {
    public override void Process(RenderRenderingArgs args)
    {
      Assert.ArgumentNotNull(args, "args");
 
      if (args.Rendered
        || HttpContext.Current == null
        || (!args.Cacheable)
        || (!string.IsNullOrWhiteSpace(args.CacheKey))
        || (!args.Rendering.Caching.VaryByParameters))
      {
        return;
      }
 
      foreach (string key in new [] { "ip", "iP", "Ip", "IP" })
      {
        if (args.Rendering.Parameters.Contains(key))
        {
          string ip =
            HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
 
          if (string.IsNullOrWhiteSpace(ip))
          {
            ip = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];
          }
 
          args.Rendering.Parameters[key] = ip;
          break;
        }
      }
    }
  }
}

Here is an example that requires a VaryByIP checkbox field in the data templates for renderings. Add a base template to the System/Layout/Sections/Caching data template if you use this approach, and remember that you cannot specify this option through the UI; the checkbox in the rendering definition item always applies:

namespace Sitecore.Sharedsource.Mvc.Pipelines.Response.RenderRendering
{
  using System.Web;
 
  using Sitecore.Data.Items;
  using Sitecore.Diagnostics;
  using Sitecore.Mvc.Pipelines.Response.RenderRendering;
 
  public class ApplyVaryByIP : RenderRenderingProcessor
  {
    public override void Process(RenderRenderingArgs args)
    {
      Assert.ArgumentNotNull(args, "args");
 
      if (args.Rendered
        || HttpContext.Current == null
        || (!args.Cacheable)
        || string.IsNullOrWhiteSpace(args.CacheKey)
        || args.Rendering.RenderingItem == null)
      {
        return;
      }
 
      Item rendering = args.PageContext.Database.GetItem(
        args.Rendering.RenderingItem.ID);
 
      if (rendering == null
        || rendering["VaryByIP"] != "1")
      {
        return;
      }
 
      string ip =
        HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
 
      if (string.IsNullOrWhiteSpace(ip))
      {
        ip = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];
      }
 
      args.CacheKey += "_#ip:" + ip;
    }
  }
}

Here is a web.config include file that enables both of these features:

<configuration xmlns:patch="https://www.sitecore.com/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.renderRendering>
        <processor
          patch:before="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.GenerateCacheKey, Sitecore.Mvc']" type="Sitecore.Sharedsource.Mvc.Pipelines.Response.RenderRendering.RewriteIPParam,Sitecore.Sharedsource" />
        <processor
          patch:after="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.GenerateCacheKey, Sitecore.Mvc']" type="Sitecore.Sharedsource.Mvc.Pipelines.Response.RenderRendering.ApplyVaryByIP,Sitecore.Sharedsource" />
      </mvc.renderRendering>
    </pipelines>
  </sitecore>
</cofiguration>

Alex Shyba wrote a blog that shows one way to use VaryByParam for custom caching criteria with Web Forms.

Resources

  • Hi John. I know this one is old, but i have a question/comment about your sample code.  I am implementing this on a * item page for product details. I am resolving the product from a product service using the url of the page.  I want to have a product details rendering on the page with caching, but the caching needs to be different for each product of cause.  So i was implementing your example with a few modifications.  I got it working, but made the observation that the  'args.Cacheable' always seems to return false, no matter what i do. However the 'args.Rendering.Caching.Cacheable' returns the correct value.  Any reason why 'args.Cacheable' does not work or what is the difference between those two?  Also i am wondering why you are not checking the rendering item also, if that is cacheable? Seems strange that for this to work an editor specifically has to set caching to true each time the rendering is inserted. Is there are reason for that? As a developer i would in most cases like to be able to set on the rendering item itself, that it must use caching and use the custom VarByIp for example.  Seems to me, that if i do that and use your example code, it will never cache the rendering. I have to set it on each added renderings / on all pages where it is used.  Or am i missing something here?  Best Regards Lasse Rasch Sitecore Freelancer, Denmark

  • @Lasse: The first thing to check is that you are not in any interactive mode, such as the Experience Editor.

  • @John: Yes that was the case for the args.cachable beeing false :-) Figured that out yesterday.  As for my second question/observation from my first blog entry, it's just a thought. Maybe it's just because your code was just meant as an example. I'm just wondering why :-)  Best Lasse

  • @Lasse: I believe that I tested and if I use VaryByParam in the rendering definition item, Sitecore adds the parameter to the cache key automatically, so the code would not need to check for that. But it is quite possible that I did not test and am wrong, or that I misunderstand your issue.  I assume that args.Rendering.Caching.Cacheable indicates whether Sitecore could cache the individual rendering, but args.Cacheable overrides that for all renderings when you are in the Experience Editor.  If this does not help, or testing shows it to be wrong, please follow up here.