Caching page output in ASP.NET Core applications

Output caching can significantly improve the performance and scalability of an application by reducing the server-side work required to generate page output. Caching works best with content that changes infrequently and is expensive to generate.

Xperience supports caching for standard page content and the output of page builder widgets.

Hotfix requirement

To use the output caching functionality as described on this page, apply hotfix 13.0.18 or newer. The hotfix removes several caching-related issues and introduces certain API improvements.

Caching page output

You can cache the output of pages using the cache Tag Helper provided by ASP.NET Core. The Tag Helper is highly flexible, allowing for configuration of cache priority, expiration, optional deactivation of the caching functionality, etc.

Cache Tag Helper usage example



@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<cache priority="High" expires-sliding="@TimeSpan.FromSeconds(60)" vary-by-user="true">
    @* Cached content... *@
</cache>


Adding cache dependencies

When cached content contains objects from the Xperience database, you need to ensure the cache is cleared whenever such objects are modified to always display the most up-to-date information. For this purpose, the system allows you to specify cache dependencies on system objects from within cache Tag Helper sections. Cache dependencies instruct the application to automatically clear cached data when referenced objects are modified.

Specify cache dependencies by adding the cache-dependency Tag Helper within sections encapsulated by the cache tag. The helper provides two attributes:

Adding cache dependencies on Xperience objects



@* Adds the Tag Helper to the view *@
@addTagHelper  Kentico.Web.Mvc.Caching.CacheDependencyTagHelper, Kentico.Web.Mvc 

@{ 
    // Creates cache dummy keys targetting all user objects in the system and the /Home page
    var userCacheKeys = new[] { "cms.user|all", "node|mysite|/home" };
}

<cache>
    @* Ensures that contents of this cached section are evicted whenever any user in the system or the /Home page is modified *@
    <cache-dependency cache-keys="@userCacheKeys" />

    Time: @DateTime.Now
</cache>


Controlling cache dependency injection

The cache-dependency Tag Helper can be programmatically enabled or disabled via its enabled attribute. This can be paired together with the enabled attribute of the cache Tag Helper to eliminate unnecessary overhead when caching is disabled:

Example



@{
    // Adds a cache dependency on all pages of the my.article page type that exist on 'mysite'
    var articlesCacheKeys = new[] { "node|mysite|my.article|all" };

    // Checks whether the page is accessed in preview mode (e.g., via the Pages application or a preview link)
    var cacheEnabled = !Context.Kentico().Preview().Enabled;
}

@* Enables or disables the cache and cache dependencies based on the context of the current request (caching in preview mode is disabled) *@
<cache enabled="@cacheEnabled">
    <cache-dependency cache-keys="@articlesCacheKeys" enabled="@cacheEnabled" />
    @* Custom logic (e.g., render all articles) *@
</cache>


Caching different versions of page output

In some cases, pages serve different content based on various conditions. Typical scenarios are multilingual websites or pages that leverage content personalization. Another case is caching within page templates, which are then used for different pages that have their own specific content.

To ensure that output caching works correctly in these scenarios, you need to take additional steps and adjust your application to cache separate output versions. Configure output cache varying by setting the vary-by attributes of the cache Tag Helper.

The following examples demonstrate how you can vary the output cache:

Page template content

Page templates allow different pages to use the same general layout or share fixed sections of content. However, parts of the template typically load data from the actual page, so the final content differs for each page.

If you use the cache Tag Helper for the page-specific sections of a page template, you need to ensure that every page stores its own version of the cache.

We recommend using the vary-by-route attribute of the cache Tag Helper. Pages have their own unique route, regardless of the shared page template.

The route parameter values depend on your site’s routing mode:

  • Content tree-based routing – set the vary-by-route attribute’s value to the CacheVaryByConstants.ROUTE_URL_SLUG constant. The constant stores the name of the route parameter that holds the URL slug value for page routes.
  • Custom routing – set the vary-by-route attribute’s value according to the parameters that you use in your custom page routes.
Caching within a page template



@using Kentico.PageBuilder.Web.Mvc;

...

<cache expires-after="@TimeSpan.FromMinutes(5)" vary-by-culture="true" vary-by-route="@CacheVaryByConstants.ROUTE_URL_SLUG">
    @* Cached content... *@
</cache>


Xperience classifies cookies into several levels according to their purpose via cookie levels. If you display different content to visitors based on their preferred cookie level, you can vary the cache accordingly.

Use the ICurrentCookieLevelProvider service to obtain the cookie level for the current request and pass it to the view via a model class:

Controller class



using CMS.Helpers;

public async Task<ActionResult> Index([FromServices] ICurrentCookieLevelProvider cookieLevelProvider)
{
    // Gets the cookie level for the current request
    string varyByCookieLevel = $"CookieLevel={cookieLevelProvider.GetCurrentCookieLevel()}";

    var viewModel = new HomeIndexViewModel
    {
        VaryByCookieLevel = varyByCookieLevel
    };

    return View(viewModel);
}


In the view, assign the cookie level to the vary-by attribute of the cache Tag Helper:

View template



@* Ensures a separate cache entry for every cookie level detected in requests *@
<cache vary-by="@Model.VaryByCookieLevel">
    ...
</cache>


If you wish to vary the cache according to multiple variables, you can pass a string containing the concatenated name-value pairs of all variables. The framework creates a separate entry for each unique string.

Personas

Personas allow you to segment your visitors according to their behavior on the site. If you wish to cache content that differs based on visitor personas, you can use the vary-by attribute provided by the cache Tag Helper

Obtain the identifier of the current contact’s persona via ContactManagementContext and pass it to the view via a model class: 

Cache personalization based on a contact's persona



using CMS.ContactManagement;

public async Task<ActionResult> Index()
{
    // Gets the contact's matching persona from the context of the current request
    // The 'createAnonymous' flag ensures the system does not create a new anonymous contact for new visitors
    int? persona = ContactManagementContext.GetCurrentContact(createAnonymous: false)?.ContactPersonaID;
    string varyByPersona = $"Persona={persona}";

    var viewModel = new HomeIndexViewModel
    {
        VaryByPersona = varyByPersona
    };

    return View(viewModel);
}


In the view, assign the identifier of the persona to the vary-by attribute of the cache Tag Helper:

Corresponding view template



@* Ensures a separate cache entry for every persona that views the output of this cached section *@
<cache vary-by="@Model.VaryByPersona">
    ...
</cache>


If you wish to vary the cache according to multiple variables, you can pass a string containing the concatenated name-value pairs of all variables. The framework creates a separate entry for each unique string.

A/B testing

To cache different output versions based on the page A/B variant selected for the current request, use the ICurrentABTestVariantProvider service.

Obtain the variant’s identifier via the service’s Get method and pass it to the view via a model class:

Cache personalization based on an A/B variant



using Kentico.OnlineMarketing.Web.Mvc;

public async Task<ActionResult> Index([FromServices] ICurrentABTestVariantProvider currentABTestVariantProvider)
{
    // Gets the identifier of the A/B test variant selected for the current request
    var varyByVariant = $"ABVariant={currentABTestVariantProvider.Get()?.Guid}";

    var viewModel = new HomeIndexViewModel
    {
        VaryByVariant = varyByVariant
    };

    return View(viewModel);
}


In the view, assign the variant identifier to the vary-by attribute of the cache Tag Helper:

Corresponding view template



@* Ensures a separate cache entry for every A/B test variant defined for the page *@
<cache vary-by="@Model.VaryByVariant">
    ...
</cache>


If you wish to vary the cache according to multiple variables, you can pass a string containing the concatenated name-value pairs of all variables. The framework creates a separate entry for each unique string.

Caching the output of page builder widgets

Page builder widgets can be configured to automatically cache their output. The bulk of the caching configuration is performed in editable areas. This allows developers to create flexible caching strategies for individual pages.

To enable and configure caching for a widget:

  1. Set the AllowCache property of the RegisterWidget attribute to true for the selected widget. This marks the widget as cacheable for individual editable areas.

    
    
    
     [assembly: RegisterWidget("CompanyName.CustomWidget", typeof(CustomWidgetViewComponent), "Custom widget", AllowCache = true)]
    
    
     
  2. Enable and configure caching within individual editable areas in your project.

    1. Enable caching for the editable area via the AllowWidgetOutputCache property of the EditableAreaOptions object. The property indicates whether the output of individual widgets placed into the area can be cached. This value is combined with the AllowCache property of contained widgets to determine whether caching should be enabled. The default value is false.

    2. Select a caching strategy for the editable area via the following properties of the EditableAreaOptions object:
      WidgetOutputCacheExpiresOn – a DateTimeOffset value that sets the absolute expiration date for cached content.
      WidgetOutputCacheExpiresAfter – a TimeSpan value that sets the duration from the time of the first request when the content was cached.
      WidgetOutputCacheExpiresSliding – a TimeSpan value that defines a sliding window of expiration for the cached content. Content not accessed within the specified time frame gets evicted.

      These configuration options can also be set via the editable-area Tag Helper.

      
      
      
       @using Kentico.PageBuilder.Web.Mvc
       @using Kentico.Web.Mvc
      
       @{ 
           // Sets the sliding expiration for cached widgets to two minutes
           var options = new EditableAreaOptions() { AllowWidgetOutputCache = true,
                                                     WidgetOutputCacheExpiresSliding = TimeSpan.FromMinutes(2) };
       }
      
       @await Html.Kentico().EditableAreaAsync("area1", options)
      
      
       
  3. The system automatically adds a cache dependency tied to pages where the widget is rendered – when the content of the page is modified, the cached output of individual widgets is evicted.

You have configured caching for your widget. To further customize the caching behavior, you can:

Adding custom cache dependencies

The system automatically adds a cache dependency on the page where the widget is rendered. In addition, widget developers can provide additional cache dependencies when developing a widget. However, this approach requires access to the component view model, and is therefore only supported for widgets based on view components.

Cache dependencies can be added within the view component’s Invoke (InvokeAsync) method via the CacheDependencies property of the ComponentViewModel parameter. Pass a string collection of desired cache dependency keys into the object’s CacheKeys property:




using Kentico.PageBuilder.Web.Mvc;

public async Task<IViewComponentResult> InvokeAsync(ComponentViewModel<AttachmentListWidgetProperties> viewModel)
{
    // Adds a cache dependency on all page attachments
    viewModel.CacheDependencies.CacheKeys = new [] { "cms.attachment|all" };
    ...
}


Caching personalized widget output

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

For this purpose, the system provides the CacheVaryBy attribute. The attribute allows you to specify which request variables affect the contents of the cache. From the specified variables, the system constructs a cache key under which it caches the widget output corresponding to the particular configuration. Should any of the request values change, the output is cached as a separate entry in the cache. The lifetime and eviction strategy of individual entries is controlled by the caching configuration of the corresponding editable area. 

The CacheVaryBy attribute must decorate the view component class of the corresponding widget. As such, only widgets based on view components support this approach.

The attribute supports the following personalization variables:

Property

Type

Description

VaryByUser

bool

Varies the cache based on the current user.

VaryByCulture

bool

Varies the cache based on the current request culture. Enabled by default.

VaryByHost

bool

Varies the cache based on the current host (the domain and port number of the server to which the request is being sent).

VaryByCookieLevel

bool

Varies the cache based on the cookie level of the current request.

VaryByQuery

string

Varies the cache based on parameters in the URL query string.

Specify a comma-delimited set of query string parameters used to vary the cached content.

VaryByCookie

string

Varies the cache based on cookie values.

Specify a comma-delimited set of cookie names used to vary the cached content. 

VaryByHeader

string

Varies the cache based on request headers.

Specify a comma-delimited set of request headers used to vary the cached content.

VaryByRoute

string

Varies the cache based on route data parameters.

Specify a comma-delimited set of application route data parameters used to vary the cached content.

Example

Startup.cs



endpoints.MapControllerRoute( name: "default",
                              pattern: "{controller=Home}/{action=Index}/{Make?}/{Model?}");


WidgetViewComponent.cs



[CacheVaryBy(VaryByRoute="Make;Model")]
public class WidgetViewComponent : ViewComponent


Note: For sites that use content tree-based routing, you can access the name of the route parameter that holds the URL slug value for page routes in the CacheVaryByConstants.ROUTE_URL_SLUG constant.

VaryByOptionTypes

Type[]

An array of System.Type values referencing custom vary by implementations.

You can implement custom vary by conditions that suit your particular scenarios. See Implementing custom personalization options.

CacheVaryBy usage example



using Kentico.PageBuilder.Web.Mvc;

[CacheVaryBy(VaryByUser = true, VaryByCookie = "cookie_1,cookie_2", VaryByCookieLevel = true, VaryByCulture = false, VaryByHeader = "header_1,header_2",
    VaryByHost = true, VaryByQuery = "query_param1,query_param2", VaryByRoute = "param_1,param_2")]
public class MyWidgetViewComponent : ViewComponent


The example above configures caching for a broad range of variables contained within each request. This is unnecessary for most cases. You should cache according to personalization conditions used by your widgets.

Always balance website performance, memory usage on the server, and the risk of displaying incorrect cached content.

Automatic widget cache varying

By default, the system stores separate output cache versions of widgets for every widget personalization variant and A/B test page variant. This ensures that cached widgets automatically display the correct content when using the native personalization and A/B testing features, and no handling is required from developers.

Implementing custom personalization options

In addition to the variables supported by the CacheVaryBy attribute, you can implement cache personalization based on other request variables. Such custom personalization options need to be developed as separate classes implementing the ICacheVaryByOption interface. The interface describes a GetKey method which must return a string containing a key-value pair identifying the personalization variable and its value for the current request.




using Kentico.PageBuilder.Web.Mvc

public class VaryByOption : ICacheVaryByOption
{
        // Returns a key identifying the cache variant
        public string GetKey()
        {
            return $"MyKey={ComputeVariable()}";
        };

        private string ComputeVariable()
        {
            // custom logic...
            return myVariable;
        };
}


Example

The following example implements a personalization option that partitions the cache based on the currently authenticated user’s first name. This approach leads to fewer redundant cache entries in scenarios where the output does not vary per object, but rather according to properties whose values are shared among multiple objects. As a result, it creates a more economical caching strategy that consumes less application memory. Another example could be user gender or assigned role.

Create a new VaryByUserName class, implement the ICacheVaryByOption interface, and define the GetKey method.

VaryByUserName.cs



using System;

using CMS.Membership;

using Kentico.PageBuilder.Web.Mvc;  

public class VaryByUserName : ICacheVaryByOption
{
    public string GetKey()
    {
        string userFirstName = MembershipContext.AuthenticatedUser?.FirstName ?? String.Empty;

        return $"UserFirstName={userFirstName}";
    }
}


Pass the created VaryByUserName class to the CacheVaryBy attribute in your widget’s view component:

MyWidgetViewComponent.cs



using Kentico.PageBuilder.Web.Mvc;

// Caches a separate output for each distict user name  
[CacheVaryBy(typeof(VaryByUserName))] 
public class MyWidgetViewComponent : ViewComponent


The application now creates a distinct cache entry for each user with a unique first name that views content rendered by your widget.