Caching the output of personalized content

When using ASP.NET output caching to improve the performance of your MVC website, you need to take additional steps to ensure that the output cache correctly stores personalized pages. Because personalized pages serve different content based on various conditions, you need to adjust your application to cache separate output versions according to the personalization criteria.

To cache different versions of content, use the OutputCache attribute together with a custom implementation of the GetVaryByCustomString method.

Configuring output caching using custom strings

Kentico offers a fluent API built around .NET framework’s VaryByCustomString output caching feature that allows you to construct output cache keys according to your project’s caching requirements. The system provides an IOutputCacheKeyOptions interface that allows you to create configurable objects for building output cache keys. The options object is configured by chaining any number of VaryBy methods (representing cache key parts). The complete cache key is then constructed by calling OutputCacheKeyHelper.GetVaryByCustomString with the options object as the parameter.

VaryBy methods

By default, the system allows you to construct the cache key using the following request variables (extension methods for IOutputCacheKeyOptions objects):

Method

Namespace

Cache key

VaryByBrowser

Kentico.Web.Mvc

The name and the major version of the client’s browser.

VaryByHost

Kentico.Web.Mvc

The host name of the server.

VaryBySite

Kentico.Web.Mvc

The code name of the site.

VaryByUser

Kentico.Web.Mvc

The name of the current user.

VaryByCookieLevel

Kentico.Web.Mvc

The cookie level of the current user.

VaryByPersona

Kentico.OnlineMarketing.Web.Mvc

The ID of the persona assigned to the current contact.

VaryByABTestVariant

Kentico.OnlineMarketing.Web.Mvc

The GUID of the A/B test variant assigned for the current request.

Example – Configuring output caching

The following steps describe how to cache multiple versions of personalized output using custom strings:

  1. Edit your project’s Global.asax.cs file and override the GetVaryByCustomString method.

    • The method’s custom parameter contains the string that you set in the VaryByCustom property of OutputCache attributes in your controllers.

    • For each custom string, you need to define the caching requirements – use VaryBy methods on an IOutputCacheKeyOptions object to create a set of request variables according to which the output cache needs to vary.

      
      
      
        using System;
        using System.Web;
      
        using Kentico.Web.Mvc;
        using Kentico.OnlineMarketing.Web.Mvc;
      
      
        
      Example
      
      
      
                public override string GetVaryByCustomString(HttpContext context, string custom)
                {
                    // Creates the options object used to store individual cache key parts
                    IOutputCacheKeyOptions options = OutputCacheKeyHelper.CreateOptions();
      
                    // Selects a caching configuration according to the current custom string
                    switch (custom)
                    {
                        case "Default":
                            // Sets the variables that compose the cache key for the 'Default' VaryByCustom string
                            options
                                .VaryByHost()
                                .VaryByBrowser()
                                .VaryByUser();
                            break;
      
                        case "OnlineMarketing":
                            // Sets the variables that compose the cache key for the 'OnlineMarketing' VaryByCustom string
                            options
                                .VaryByCookieLevel()
                                .VaryByPersona()
                                .VaryByABTestVariant();
                            break;
                    }
      
                    // Combines individual 'VaryBy' key parts into a cache key under which the output is cached
                    string cacheKey = OutputCacheKeyHelper.GetVaryByCustomString(context, custom, options);
      
                    // Returns the constructed cache key
                    if (!String.IsNullOrEmpty(cacheKey))
                    {
                        return cacheKey;
                    }
      
                    // Calls the base implementation if the provided custom string does not match any predefined configurations
                    return base.GetVaryByCustomString(context, custom);
                }
      
      
      
        

      Important

      The cache keys created in the example above ensure caching for a broad range of variables contained within each request. This may be unnecessary for your website. For optimal caching, you need to prepare output cache variables tailored according to personalization conditions that you use on the site or individual pages.

      For example, pages that check for contact personas in their personalization condition only require the persona identifier in the cache variables, and you can share the cached content for other variables, such as contact groups to which the contacts belong. On the other hand, strongly personalized pages with dynamic content may require separate cache versions for every contact.

      You always need to balance website performance, memory usage on the server, and the risk of displaying incorrect cached content.

  2. Edit the controller classes that serve personalized content on your MVC site.

  3. Set the VaryByCustom property of OutputCache attributes to one of the specified configurations according to the action’s caching requirements.

    Example
    
    
    
     using System.Web.Mvc;
    
     using CMS.ContactManagement;
    
     using LearningKit.Models.Personalization;
    
     namespace LearningKit.Controllers
     {
         public class PersonalizationController : Controller
         {
             /// <summary>
             /// Gets the current contact, if contact tracking is enabled for the connected Kentico instance.
             /// </summary>
             private ContactInfo CurrentContact => ContactManagementContext.GetCurrentContact();
    
             /// <summary>
             /// Displays a page with a personalized greeting.
             /// The content depends on whether the current contact belongs to the "YoungCustomers" persona.
             /// Caches the output for 10 minutes, with different cache versions defined by the "OnlineMarketing" custom string.
             /// The "OnlineMarketing" configuration separately caches each combination of persona and AB test variant variables.
             /// </summary>
             [OutputCache(Duration = 600, VaryByCustom = "OnlineMarketing")]
             public ActionResult PersonalizedGreeting()
             {
                 CurrentContactViewModel model;
    
                 // If on-line marketing is disabled, CurrentContact is null
                 if (CurrentContact != null)
                 {
                     model = new CurrentContactViewModel(CurrentContact);
                 }
                 else
                 {
                     model = null;
                 }
    
                 return View(model);
             }
         }
     }
    
    
     

The application now caches separate versions of action output based on your custom variables. This ensures that visitors do not receive incorrect cached content when viewing personalized pages.

Varying the output cache using additional variables

In addition to the cache variables provided by the system, you can vary the cache key based on any accessible request variable.

There are two ways to extend the general support provided by Kentico:

Implementing additional cache key parts

You can extend the provided support by implementing additional cache key parts for IOutputCacheKeyOptions. This approach allows for easy reusability of the implemented methods across multiple projects.

Cache key parts consist of:

  • A class implementing the IOutputCacheKey interface. The interface defines members necessary to implement additional cache key parts.
  • An extension method for IOutputCacheKeyOptions. The method needs to add an instance of the IOutputCacheKeyobject to the cache key part collection of the options object by calling IOutputCacheKeyOptions.AddCacheKey.

The following sample demonstrates the implementation of a cache key that varies cached content based on a contact’s gender.

Implementing a contact gender cache key



using System.Web;

using CMS.ContactManagement;

using Kentico.Web.Mvc;

namespace OutputCacheCustomization
{
    public class ContactGenderOutputCacheKey : IOutputCacheKey
    {
        // Used as a prefix for this cache key part
        public string Name => "KenticoContactGender";

        // Invoked when constructing a cache key from the configured 'IOutputCacheKeyOptions' options object
        public string GetVaryByCustomString(HttpContextBase context, string custom)
        {
            // Gets the current contact, without creating a new anonymous contact for new visitors
            ContactInfo existingContact = ContactManagementContext.GetCurrentContact(createAnonymous: false);
            // Gets the contact's gender
            int? contactGender = existingContact?.ContactGender;
            return $"{Name}={contactGender}";
        }
    }
}


Create a corresponding extension method for IOutputCacheKeyOptions. The method adds an instance of the class to an IOutputCacheKeyOptions object using the AddCacheKeymethod.

Implementing the extension method for IOutputCacheKeyOptions



using Kentico.Web.Mvc;

namespace OutputCacheCustomization
{
    public static class OutputCacheKeyOptionsExtensions
    {
        // Varies the output cache based on the contact's gender
        public static IOutputCacheKeyOptions VaryByContactGender(this IOutputCacheKeyOptions options)
        {
            // Adds the ContactGenderOutputCacheKey to the options object
            options.AddCacheKey(new ContactGenderOutputCacheKey());

            return options;
        }
    }
}


You can now use the ContactGender variable when building cache keys in the site’s GetVaryByCustomString implementation.

Modifying the constructed cache key

An alternative approach to implementing reusable extension methods is constructing the cache key using OutputCacheKeyHelper.GetVaryByCustomString and further customizing it by directly appending additional request variables.

The following example implements a custom GetContactGroupsCacheKey method that returns a semicolon-separated list of all contact groups to which the current contact belongs. The string is appended to the cache key constructed by the GetVaryByCustomString helper method, adding another level of variance to the output cache.




private string GetContactGroupsCacheKey()
{
    // Gets the current contact without creating a new anonymous contact for new visitors
    var existingContact = ContactManagementContext.GetCurrentContact(createAnonymous: false);
    if (existingContact == null)
    {
        return String.Empty;
    }

    // Gets the contact groups assigned to the contact
    var groups = existingContact.ContactGroups?.Select(x => x.ContactGroupID).OrderBy(x => x).ToArray() ?? new int[] { };

    // Creates a string used to vary the resulting cache key
    var groupsKey = String.Join(";", groups);

    return $"ContactGroups={groupsKey}";
}

public override string GetVaryByCustomString(HttpContext context, string custom)
{
    // The configuration of the 'IOutputCacheKeyOptions' object is omitted
    ...

    // Combines individual 'VaryBy' components into a cache key under which the output is cached
    string cacheKey = OutputCacheKeyHelper.GetVaryByCustomString(context, custom, options);

    // Prepares an empty string object for the contact groups key part
    string contactGroupsKey = String.Empty;

    // Checks if the current request requires caching based on contact groups
    // As an example, we use contact group caching for the 'OnlineMarketing' configuration
    if (String.Equals(custom, "OnlineMarketing", StringComparison.InvariantCultureIgnoreCase))
    {
        contactGroupsKey = GetContactGroupsCacheKey();
    }

    // Appends the contact groups string to the end of the constructed cache key
    // Separates the key part using the pipe '|' character - a separator used internally 
    // for all cache key parts by the 'OutputCacheKeyHelper.GetVaryByCustomString' method
    if (!String.IsNullOrEmpty(contactGroupsKey))
    {
        cacheKey = String.Join("|", cacheKey, contactGroupsKey);
    }

    // Returns the constructed cache key        
    if (!String.IsNullOrEmpty(cacheKey))
    {
        return cacheKey;
    }

    // Calls the base implementation if the provided custom string does not match any predefined configuration
    return base.GetVaryByCustomString(context, custom);
}