A critical vulnerability was discovered in React Server Components (Next.js). Our systems remain protected but we advise to update packages to newest version. Learn More

Binh Nguyen Thi
Mar 17, 2023
  84
(0 votes)

How to apply output cache to Optimizely CMS 12

Introduce

Optimizely CMS 12 is a platform for content management systems. It is written by .NET 6.0. Actually, the output caching concept has not been yet made it in .NET 6.0 but there is a response caching concept in .NET6.0.

You can use response cache to cache output for MVC controllers, MVC action methods, RAZOR pages. Response caching reduces the amount of work the web server performs to generate a response by returning result immediately from cache if it exists instead of running methods again and again. By this way, the performance is improved and server resources are optimized.

Step to apply response cache in Optimizely CMS 12

  • Step 1: Add [ResponseCache] attribute to the controller/action/razor page that you want:
    public class StartPageController : PageControllerBase<StartPage>
    {
        [ResponseCache(Duration =30, Location = ResponseCacheLocation.Any)]
        public IActionResult Index(StartPage currentPage)
        {
            var model = PageViewModel.Create(currentPage);

            // Check if it is the StartPage or just a page of the StartPage type.
            if (SiteDefinition.Current.StartPage.CompareToIgnoreWorkID(currentPage.ContentLink))
            {
                // Connect the view models logotype property to the start page's to make it editable
                var editHints = ViewData.GetEditHints<PageViewModel<StartPage>, StartPage>();
               editHints.AddConnection(m => m.Layout.Logotype, p => p.SiteLogotype);
               editHints.AddConnection(m => m.Layout.ProductPages, p => p.ProductPageLinks);
               editHints.AddConnection(m => m.Layout.CompanyInformationPages, p => p.CompanyInformationPageLinks);
               editHints.AddConnection(m => m.Layout.NewsPages, p => p.NewsPageLinks);
               editHints.AddConnection(m => m.Layout.CustomerZonePages, p => p.CustomerZonePageLinks);
           }

           return View(model);
       }
   }

Duration=30 will cache the page for 30 seconds

Location=ResponseCacheLocation.Any will cache the page in both proxies and client.

If you do not apply response cache for certain situation then you can use Location= ResponseCacheLocation.None and NoStore=true

  • Step 2: Add Response Cache Middleware services to service collection with AddResponseCaching extension method:
    public void ConfigureServices(IServiceCollection services)
    {
        if (_webHostingEnvironment.IsDevelopment())
        {
            AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(_webHostingEnvironment.ContentRootPath, "App_Data"));

            services.Configure<SchedulerOptions>(options => options.Enabled = false);
        }

        services
            .AddCmsAspNetIdentity<ApplicationUser>()
            .AddCms()
            .AddCmsTagHelpers()
            .AddAlloy()
            .AddAdminUserRegistration()
            .AddEmbeddedLocalization<Startup>();

        // Required by Wangkanai.Detection
        services.AddDetection();

        services.AddSession(options =>
        {
            options.IdleTimeout = TimeSpan.FromSeconds(10);
            options.Cookie.HttpOnly = true;
            options.Cookie.IsEssential = true;
        });
        services.AddControllers();
        services.AddResponseCaching();
    }
  • Step 3: Configure the app to use the middleware with the UseResponseCaching extension method:
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        // Required by Wangkanai.Detection
        app.UseDetection();
        app.UseSession();

        app.UseResponseCaching();

        app.UseStaticFiles();
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapContent();
            endpoints.MapControllers();
        });
    }

Please note that if you load a page by pressing F5 key in the browser then the cache for this page is refreshed by adding Cache-Control header to request is “max-age=0”. In order to prevent that, you can add the following middleware before response cache middleware:

   app.Use(async (context, next) =>
   {
        const string cc = "Cache-Control";

        if (context.Request.Headers.ContainsKey(cc) && string.Equals(context.Request.Headers[cc], "max-age=0", StringComparison.InvariantCultureIgnoreCase))
        {
              context.Request.Headers.Remove(cc);
        }
        await next();
   });

How to invalidate cache when the content is changed

Actually, the response cache middleware uses MemoryResponseCache by default and this implementation does not support clearing cache. So you can do quickly and dirty to get a new cache by using the content cache version as a query key to vary cache. The content cache version is increased once any content is changed.

Here are the steps that you can take to do that:

  • Add ContentCacheVersion to vary by query keys attribute
   [ResponseCache(Duration =30, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new string[] { "ContentCacheVersion"})]
   public IActionResult Index(StartPage currentPage)
   {
       var model = PageViewModel.Create(currentPage);
       // Check if it is the StartPage or just a page of the StartPage type.
       if (SiteDefinition.Current.StartPage.CompareToIgnoreWorkID(currentPage.ContentLink))
       {
  • Add a middleware before Response Caching Middleware to add content cache version to the query string and add a middleware after Response Caching Middleware to remove this from the query string.
   app.Use(async (context, next) =>
        {
            var contentCacheVersion = ServiceLocator.Current.GetInstance<IContentCacheVersion>();

            context.Request.QueryString = context.Request.QueryString.Add("ContentCacheVersion", contentCacheVersion.Version.ToString());
           
            await next();
        });

        app.UseResponseCaching();

        app.Use(async (context, next) =>
        {
            var nameValueCollection = System.Web.HttpUtility.ParseQueryString(context.Request.QueryString.ToString());
            nameValueCollection.Remove("ContentCacheVersion");
            context.Request.QueryString = new QueryString($"?{nameValueCollection}");
            
            await next();
        });

How to vary response cache by visitor group

It is the same as in the case of changed content, you can do quickly and dirty to add visitor group as a vary by query key like this:

  • Add VisitorGroup to vary by query keys attribute
    [ResponseCache(Duration =30, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new string[] { "VisitorGroup"})]
    public IActionResult Index(StartPage currentPage)
    {
        var model = PageViewModel.Create(currentPage);

        // Check if it is the StartPage or just a page of the StartPage type.
        if (SiteDefinition.Current.StartPage.CompareToIgnoreWorkID(currentPage.ContentLink))
        {
  • Add a middleware before Response Caching Middleware to add the current visitor group to the query string and add a middleware after Response Caching Middleware to remove it from the query string.
   app.Use(async (context, next) =>
        {
            context.Request.QueryString = context.Request.QueryString.Add("VisitorGroup", GetCurrentVisitorGroups(context));
           
            await next();
        });
        app.UseResponseCaching();
        app.Use(async (context, next) =>
        {
            var nameValueCollection = System.Web.HttpUtility.ParseQueryString(context.Request.QueryString.ToString());
            nameValueCollection.Remove("VisitorGroup");
            context.Request.QueryString = new QueryString($"?{nameValueCollection}");
            await next();
        });
  • Add GetCurrentVisitorGroups method to get visitor groups based on current context
    private string GetCurrentVisitorGroups(HttpContext context)
    {
        var  principalAccessor = ServiceLocator.Current.GetInstance<IPrincipalAccessor>();
        var  visitorGroupRepository = ServiceLocator.Current.GetInstance<IVisitorGroupRepository>();
        var  visitorGroupRoleRepository = ServiceLocator.Current.GetInstance<IVisitorGroupRoleRepository>();

        var roleNames = visitorGroupRepository.List().Select(x => x.Name);

        var currentGroups = new List<string>();
        foreach (var roleName in roleNames)
        {
            if (visitorGroupRoleRepository.TryGetRole(roleName, out var role))
            {
                if (role.IsMatch(principalAccessor.Principal, context))
                {
                    currentGroups.Add(roleName);
                }
            }
        }
        return string.Join("|", currentGroups);
    }

That is it. Using this approach, you can apply built-in response cache quickly into Optimizely CMS 12 without customizing too much or using third-party packages. You can see another topic about using third-party output caching package here https://www.gulla.net/en/blog/quick-and-dirty-output-cache-in-optimizely-cms12/

 

Mar 17, 2023

Comments

Please login to comment.
Latest blogs
A day in the life of an Optimizely OMVP: Learning Optimizely Just Got Easier: Introducing the Optimizely Learning Centre

On the back of my last post about the Opti Graph Learning Centre, I am now happy to announce a revamped interactive learning platform that makes...

Graham Carr | Jan 31, 2026

Scheduled job for deleting content types and all related content

In my previous blog post which was about getting an overview of your sites content https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/sche...

Per Nergård (MVP) | Jan 30, 2026

Working With Applications in Optimizely CMS 13

💡 Note:  The following content has been written based on Optimizely CMS 13 Preview 2 and may not accurately reflect the final release version. As...

Mark Stott | Jan 30, 2026

Experimentation at Speed Using Optimizely Opal and Web Experimentation

If you are working in experimentation, you will know that speed matters. The quicker you can go from idea to implementation, the faster you can...

Minesh Shah (Netcel) | Jan 30, 2026

How to run Optimizely CMS on VS Code Dev Containers

VS Code Dev Containers is an extension that allows you to use a Docker container as a full-featured development environment. Instead of installing...

Daniel Halse | Jan 30, 2026

A day in the life of an Optimizely OMVP: Introducing Optimizely Graph Learning Centre Beta: Master GraphQL for Content Delivery

GraphQL is transforming how developers query and deliver content from Optimizely CMS. But let's be honest—there's a learning curve. Between...

Graham Carr | Jan 30, 2026