blog
Controlling Sitecore HTML Caching Variance with the Rules Engine
Part 3 of my 3-Part Series on Sitecore Caching

In my last post, Getting Started with Sitecore's HTML Caching (Part 2), I showed you how to use HTML Caching and went over the various OOTB options that you have for controlling it in your solution. In this third and final post in the series, I will be going over how you can extend Sitecore's HTML caching to support my favorite feature in all of Sitecore: the Rules Engine.

Background

When I started researching Sitecore's HTML Caching for this series, I was surprised that there was no OOTB support for using the Rules Engine to configure HTML Caching options. Those who know how much I love the Rules Engine could have easily predicted what I would do next: plan, code, code, code, test...blog!

Goal

The goal of my little micro-project was to add support for configuring additional "Vary By" HTML Caching options for a rendering, using rules. For this to work, my implementation would need of the following:

  • Custom GenerateCacheKey pipeline processor with support for customizing the Cache Key based on the new Variance Rules
  • New Tree List field, "Caching Variance Rules" on the /sitecore/templates/system/layout/sections/caching template
  • Rule actions that add both a Variant Key and Variant Value to the Cache Key

Implementation

It turns out that this was actually pretty easy. The following steps are all that was required to implement this feature:

  1. Create a new CacheKeyEditingContext
  2. Create RenderingCachingVarianceActionBase<T> class
  3. Create new "Advanced Presentation Caching" template
    • Add new "Caching Variance Rules" field to the new template
    • Add new template to the OOTB Caching template's inheritance
  4. Create new EnhancedGenerateCacheKey processor
  5. Create patch file to replace OOTB processor with new one

Step 1: Create the CacheKeyEditingContext class

The first thing I needed was an editing context that to hold the current value of the Cache Key so that the various rule actions would be able to modify it, as needed.

I started by writing a quick class to hold arguments for my new CacheKeyEditingContext:

    public class CacheKeyEditingContextArgs
    {
        public Item RenderingItem { get; set; }
        public string CacheKey { get; set; }
        public NameValueCollection Parameters { get; set; } = new NameValueCollection();
    }

This next thing I did was write a new class that implements Sitecore's Switcher class, to serve as my context. The Switcher class is the same class that Sitecore's EditingContext inherits.

    public sealed class CacheKeyEditingContext : Switcher<CacheKeyEditingContextArgs, CacheKeyEditingContext> 
    {
        public static CacheKeyEditingContextArgs Args => CurrentValue;
        public static bool IsActive => Args != null;
        
        public CacheKeyEditingContext(CacheKeyEditingContextArgs args) : base(args) 
        {            
        }
    }

Step 2: Create the RenderingCachingVarianceActionBase<T> class

The next thing that I needed was the base rule action for all of my other rule actions to inherit from. The base rule action would be responsible for calling the abstract method that would run all of the logic that manipulates the cache key.

Side-note: Admittedly, there are some improvements that I can do here to make the call to UpdateCacheKey more specific to the purpose of modifying the Cache Key. I left this a little open-ended since this is new functionality and I'm not yet sure of all of the customizations that I will want to make to it.

    public abstract class RenderingCachingVarianceActionBase<TRuleContext> : RuleAction<TRuleContext>
        where TRuleContext : RuleContext
    {
        public virtual string VariantKey { get; }
        public virtual string VariantValue { get; }
    
        public override void Apply(TRuleContext ruleContext)
        {
            Assert.IsNotNull(ruleContext, "ruleContext");
            Assert.IsNotNull(ruleContext.Item, "ruleContext.Item");
            
            if (ruleContext.IsAborted || !CacheKeyEditingContext.IsActive)
            {
                return;
            }

            try
            {
                var msg = "RuleAction " + GetType().Name + " started for " + ruleContext.Item.Name;

                Trace.TraceInformation(msg);
                
                UpdateCacheKey(ruleContext, CacheKeyEditingContext.Args);
            }
            catch (Exception exception)
            {
                var msg = "RuleAction " + GetType().Name + " failed.";
                Log.Error(msg, exception, this);
                Trace.TraceError(msg);
            }

            var message = "RuleAction " + GetType().Name + " ended for " + ruleContext.Item.Name;

            Trace.TraceInformation(message);
        }
        
        protected abstract void UpdateCacheKey(TRuleContext ruleContext, CacheKeyEditingContextArgs args);
    }

Step 3: Create the "Advanced Presentation Caching" template

The next thing that I needed was a new template to hold (references to) my rules. I started by creating the template item and added a new field section that named "Caching" so that it would merge with the OOTB section and show my new field next to the OOTB settings fields.

Notice that I stated that the field for holding my rules, "Caching Variance Rules", would be a Tree List field. I was a bit torn on this part. My options were to either add a Rule field or a Tree List field. In the end, I chose the Tree List field so that I could reuse the same rules on multiple renderings. As such, I pointed the Tree List field for my rules at the /sitecore/system/settings/rules folder and only enabled Rule items for selection.

Finally, I added the new template to the Base Templates of the OOTB /sitecore/templates/System/Layout/Sections/Caching template.

Step 4: Create the new EnhancedGenerateCacheKey processor

The next thing that I needed was the custom processor for generating Cache Keys and executing my rules:

    public class GenerateCacheKey : Sitecore.Mvc.Pipelines.Response.RenderRendering.GenerateCacheKey
    {
        protected override string GenerateKey(Rendering rendering, RenderRenderingArgs args)
        {
            var cacheKey = base.GenerateKey(rendering, args);
            var renderingItem = rendering.RenderingItem.InnerItem;
 
            var cachingVarianceRuleIds = renderingItem["Caching Variance Rules"];
            if (string.IsNullOrEmpty(cachingVarianceRuleIds))
            { 
                return cacheKey;
            }
            
            var cachingVarianceRuleItems = cachingVarianceRuleIds
                .Split('|', StringSplitOptions.RemoveEmptyEntries)
                .Select(id => renderingItem.Database.GetItem(id))
                .Where(item => item != null);

            return cachingVarianceRuleItems.Any() 
                ? ExecuteRules(cachingVarianceRuleItems, cacheKey, renderingItem) 
                : cacheKey;
        }
        
        protected virtual string ExecuteRules(IEnumerable<Item> ruleItems, string cacheKey, Item renderingItem) 
        {                
            var args = new CacheKeyEditingContextArgs() 
            {
                CacheKey = cacheKey,
                RenderingItem = renderingItem
            };
            
            var fallback = cacheKey;

            try 
            {
                using (new CacheKeyEditingContext(args)) 
                {
                    var rules = RuleFactory.GetRules<RuleContext>(ruleItems, "Rule").Rules;
                
                    var ruleContext = new RuleContext 
                    { 
                        Item = args.RenderingItem 
                    };
                
                    rules.Run(ruleContext);
                
                    cacheKey = CacheKeyEditingContext.Args.CacheKey;
   
                    return !string.IsNullOrEmpty(cacheKey) ? cacheKey : fallback;
                }
            } 
            catch (Exception ex) 
            {
                Log.Error(
                    $"An exception occurred while generating cache key for rendering item '{renderingItem.ID}'",
                    ex,
                    this);
               
                return fallback;
            }
        }
    }

Step 5: Create the patch file to replace OOTB processor with new one

Lastly, I needed to create a patch file to replace the OOTB GenerateCacheKey processor with my custom one:

    <?xml version="1.0"?>
    <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
      <sitecore>
        <pipelines>
          <mvc.renderRendering>
             <processor type="MyNamespace.Mvc.Pipelines.Response.RenderRendering.GenerateCacheKey, MyNamespace.Mvc" patch:instead="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.GenerateCacheKey, Sitecore.Mvc']" />
          </mvc.renderRendering>
        </pipelines>
      </sitecore>
    </configuration>

Summing it all up

That's it! You should now have everything you need to start writing custom actions to control HTML Caching variance. Some ideas to start you off are:

  • Vary by Template Rule
  • Vary by Value from User Profile Rule
  • Vary by Date Rule
  • Vary by Location Rule (useful pages with maps)
0 Comments
Post a Comment