Data caching

The Xperience API allows developers to cache data in website code. We recommend caching any frequent API calls that load significant data from the Xperience database (or other external sources). For example, caching is typically a good idea when retrieving content in the code of your sites.

To cache data, the system provides the CMS.Helpers.IProgressiveCache service.

The service supports sliding expiration (cache duration is refreshed upon successive requests for the same item), and progressive caching (parallel caching requests from multiple threads don’t result in redundant database operations; instead, one thread is used to fetch the data which is then shared with other waiting threads). Use the following methods to cache data:

  • Load – loads data using a delegate method and caches the results.
  • LoadAsync – an equivalent of the Load method for loading and caching data in asynchronous code.

An instance of the service can be obtained using dependency injection.

See the following sections for examples of usage:

Cache reusable content items and pages

The following example demonstrates the usage of IProgressiveCache to cache a content item retrieval operation on article pages. Assumes content type model classes generated by the code file generator.

Content item caching example


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

using CMS.ContentEngine;
using CMS.DataEngine;

...

// Instances of services for caching and content retrieval obtained via dependency injection
private readonly IContentQueryExecutor executor;
private readonly IProgressiveCache progressiveCache;

public async Task<IEnumerable<ArticlePage>> GetArticles(int topN = 0, CancellationToken cancellationToken = default)
{
    // Caches the loaded data
    return await progressiveCache.LoadAsync(async (cacheSettings) =>
    {
        // Gets the data to cache
        var articles = (await GetAllArticles(topN, cancellationToken)).ToList();

        // Configures cache dependencies for the data. See 'Set cache dependencies'
        cacheSettings.CacheDependency = CacheHelper.GetCacheDependency(GetDependencyCacheKeys(articles));

        return articles;
    },
    // Configures cache behavior and generates the cache key for the entry
    new CacheSettings(cacheMinutes: 5,
                      useSlidingExpiration: true,
                      cacheItemNameParts: new[] { "MyWebsiteChannel", nameof(GetArticles), "/Articles", topN.ToString() }));
}

// Gets all articles according to the provided parameterization using content item query
private Task<IEnumerable<ArticlePage>> GetAllArticles(int topN, CancellationToken cancellationToken)
{
    return executor
            .GetMappedWebPageResult<ArticlePage>(
                new ContentItemQueryBuilder()
                    .ForContentType($"{nameof(DancingGoat)}.{nameof(ArticlePage)}",
                                config => config
                                    .WithLinkedItems(1)
                                    .TopN(topN)
                                    .OrderBy(OrderByColumn.Desc(nameof(ArticlePage.ArticlePagePublishDate)))
                                    .ForWebsite("MyWebsiteChannel", PathMatch.Children("/Articles"))), cancellationToken: cancellationToken);
}

The example caches asynchronous retrieval of page content items from the database and ensures the following cache behavior:

  • Duration: 5 minutes
  • Cache key: MyWebsiteChannel|GetArticles|/Articles|<topN>
  • Sliding expiration: true

Set cache dependencies

Correctly configuring cache dependencies is a critical part of building effective caching solutions. Cache dependencies inform the system whenever the source data of a cached entity changes and prompt it to revoke the corresponding cache entry. This is especially important in richly-linked content models with many dependencies between various content types, where any change to linked content items must clear the cache as well.

Building upon the caching example above, assume the article object being retrieved is modeled like so:

Example article content model

Then the following snippet shows a sample implementation of a GetDependencyCacheKeys method that creates dependencies on the retrieved articles as well as their linked items. The example uses IWebPageLinkedItemsDependencyAsyncRetriever to generate dependency cache keys for all linked content items of the page up to the specified depth. See Cache dependencies on linked content items.

Setting cache dependencies


using System;
using System.Linq;
using System.Collections.Generic;

using CMS.Websites;
using CMS.Helpers;  

...      

// Instances of services obtained via dependency injection     
private readonly IWebPageLinkedItemsDependencyAsyncRetriever linkedItemsDependencyRetriever;  

public async Task<ISet<string>> GetDependencyCacheKeys(IEnumerable<ArticlePage> articles)
{
    // Builds a cache key for each linked content item associated with the articles,
    // up to the depth of 1 (first-level references). The generated keys ensure 
    // the cache is cleared when linked items are modified.
    var dependencyCacheKeys =
            (await linkedItemsDependencyRetriever
                    .Get(articles.Select(article => article.SystemFields.ContentItemID), maxLevel: 1))
                    // Ensures unique values
                    .ToHashSet(StringComparer.InvariantCultureIgnoreCase);

    // Adds cache dependencies on each page in the collection
    foreach (var article in articles)
    {
        // Builds a cache key "webpageitem|byid|<pageId>" for each article
        dependencyCacheKeys.Add(CacheHelper.BuildCacheItemName(new[] { "webpageitem",
                                                                       "byid",
                                                                       article.SystemFields.WebPageItemID.ToString() },
                                                                       lowerCase: false));
        // Builds a cache key "webpageitem|bychannel|MyWebsiteChannel|bypath|<pagePath>" for each article
        dependencyCacheKeys.Add(CacheHelper.BuildCacheItemName(new[] { "webpageitem",
                                                                       "bychannel",
                                                                       "MyWebsiteChannel",
                                                                       "bypath",
                                                                       article.SystemFields.WebPageItemTreePath },
                                                                       lowerCase: false));
    }

    // Creates the cache dependency object from the generated cache keys
    return dependencyCacheKeys;
}

Cache dependencies on linked content items

For content items composed of multiple linked items, it may be required to trigger a cache refresh when not only the main object but also any of its linked items change. 

To generate cache dependencies on linked items, use:

  • CMS.ContentEngine.ILinkedItemsDependencyAsyncRetriever – for reusable content items.
  • CMS.WebPages.IWebPageLinkedItemsDependencyAsyncRetriever – for web pages.

Both services contain Get methods that generate the dependencies based on content item identifiers and their content type definition. For example:

  • contentitem|byid|<contentItemId>
  • cms.contenttype|byname|<contentTypeCodeName>

See Set cache dependencies for an example of usage.

Preview mode and caching

We strongly suggest disabling caching for preview mode in order to prevent cache bloat. You can check whether the current request is under preview mode via IWebsiteChannelContext.IsPreview. For example, you can create a helper method that you can use together with CacheSettings.Cached.

Caching and preview


using CMS.Helpers;
using CMS.Websites.Routing;

...

private bool IsCacheEnabled()
{
    // An instance of IWebsiteChannelContext can be retrieved using dependency injection
    return !websiteChannelContext.IsPreview;
}

...

await progressiveCache.LoadAsync(async (cacheSettings) =>
{
    // Do not use the cache if under preview (e.g., when previewing pages in a channel)
    cacheSettings.Cached = IsCacheEnabled();

    ...
},
// Configures cache behavior and generates the cache key for the entry
new CacheSettings(cacheMinutes: 10, cacheItemNameParts: "cacheKey");

Cache general objects

The following code example shows how to synchronously load and cache user data from the database using IProgressiveCache.Load.



using CMS.DataEngine;
using CMS.Helpers;
using CMS.Membership;

...

// Instances of services used for data retrieval and caching (e.g., obtained using dependency injection)
private readonly IUserInfoProvider userInfoProvider;
private readonly IProgressiveCache progressiveCache;

// Caches the data for 10 minutes under the cache key "customdatasource|users"
// Automatically checks whether the given key is already in the cache 
ObjectQuery<UserInfo> data = progressiveCache.Load(cs => LoadUsers(cs), new CacheSettings(10, "customdatasource|users"));

// Loads the required data. Called only if the data doesn't already exist in the cache.
private ObjectQuery<UserInfo> LoadUsers(CacheSettings cs)
{
    // Loads all user objects from the database
    ObjectQuery<UserInfo> result = UserInfo.Provider.Get();

    // Sets a cache dependency for the data
    // The data is removed from the cache if the objects represented by the dummy key are modified (all administration user objects in this case)
    cs.CacheDependency = CacheHelper.GetCacheDependency("cms.user|all");    

    return result;
}

The caching logic checks if the key specified by the CacheSettings object is in the cache:

  • If yes, the method directly loads the data from the cache.
  • If not, the code calls the method specified by the delegate parameter (LoadUsers in the example) with the CacheSettings as a parameter. The method loads the data from the database, sets a cache dependency, and saves the key into the cache for the specified number of minutes

You can use the caching API when handling data anywhere in your code.

Asynchronous caching example

The following code example shows how to asynchronously load and cache user data from the database using IProgressiveCache.LoadAsync.



using System;
using System.Threading.Tasks;

using CMS.Helpers;

...

// Instances of services used for data retrieval and caching (e.g., obtained using dependency injection)
private readonly IUserInfoProvider userInfoProvider;
private readonly IProgressiveCache progressiveCache;

// Asynchronously loads data and ensures caching
var data = await progressiveCache.LoadAsync(async cacheSettings =>
{
    // Calls an async method that loads the required data (implementation not included in the example)
    var result = await LoadUsersAsync();

    cacheSettings.CacheDependency = CacheHelper.GetCacheDependency("cms.user|all");

    return result;
}, new CacheSettings(TimeSpan.FromMinutes(10).TotalMinutes, "customdatasource|users"));

CacheSettings

When using IProgressiveCache caching methods, you need to provide CMS.Helpers.CacheSettings as a parameter. The settings configure the cache key that stores the data. If you set the same cache key name for multiple data loading operations, they share the same cached value.

You can work with the following properties of the CacheSettings:

CacheSettings property

Type

Description

CacheMinutes

int

The number of minutes for which the cache stores the loaded data. The default value is 10 minutes.

We recommend using an interval of 1 to 60 minutes.

CacheDependency

CMSCacheDependency

Sets dependencies for the cache key (use the CacheHelper.GetCacheDependency method to get the dependency object).

Make sure you always set cache dependencies, unless the CacheMinutes interval is set to an extremely short time or the cached content is predominantly static.

BoolCondition

bool

A boolean condition that must evaluate to true for the data to be cached.

Cached

bool

Indicates whether the data should be cached (based on CacheMinutes and BoolCondition).

AllowProgressiveCaching

bool

Enables or disables progressive caching, which ensures that multiple threads accessing the same data only load it once and reuse the result.