Try our conversational search powered by Generative AI!

Binh Nguyen
Mar 17, 2023
  2050
(1 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

Thomas Schmidt
Thomas Schmidt Mar 17, 2023 01:49 PM

I hope this doesn't come off too harsh, but I think there are some issues with this example, let me try to explain.

First the article is named "How to apply output cache to Optimizely CMS 12", but what is actually going on here is reponse caching, so using response headers to handle caching on clients. Output caching and Response caching are two very different concepts. Response caching is about manipulating response headers to handle caching in browsers/proxies, while output caching is the art of caching entire response on the server. Response caching has been in .net since 3.1, but Output caching is available from 7.0+  The article linked too also talks about output caching and not response caching.

The use of middlewares to add/remove querystrings also feels really hacky, but not a lot of ways to handle what you are doing here with response caching. This completely changes with Output caching in .net 7+ and caching policies, can be made quite clean with policies relying on versions etc. and I am hoping that Optimizely will deliver this out of the box, we shouldn't have to develop this ourselves, it should be built in and the default on everyhting imho :)

Lets also kill usage of ServiceLocator.Current abomination together, not needed anymore in .net!

Output caching reference: https://learn.microsoft.com/en-us/aspnet/core/performance/caching/output?view=aspnetcore-7.0

Binh Nguyen
Binh Nguyen Mar 20, 2023 08:58 AM

Thanks a lot for your comment.

Sorry if the title is confused a bit to you. But the output cache I mentioned in the title it is caching of HTML responses, remarked result of pages or partial controls. It is aslo same meaning with the output cache concept in .NET Framework. 

The output cache and response cache concept that you mentioned are specific concepts in .NET Core. And of cause there are some differents between output cache and response cache in .NET Core as you said but both of them are used to stored content result of controller actions, Razor pages, api. Because Optimizely CMS 12 is based on .NET 6. That is why I do not mention to output cache in .NET Core here.

The examples here is only quick and dirty solution to work arround with Response Cache but we can consider to use built-in Response Cache or third party package in .NET 6. I think that we must not do that when Optimizely launches new version based on newer .NET Core version in the feature.

I agree that we should avoid to use ServiceLocator.Current. We should get an injected instance by this priority orders: constructor, Injected property, using ServiceLocator. Using ServiceLocator is the last choice. I agree that my examples are quick solutions, not most perfect solution. So you can move this middeware to a separated middleware class to remove ServiceLocator usage :)

Please login to comment.
Latest blogs
Solving the mystery of high memory usage

Sometimes, my work is easy, the problem could be resolved with one look (when I’m lucky enough to look at where it needs to be looked, just like th...

Quan Mai | Apr 22, 2024 | Syndicated blog

Search & Navigation reporting improvements

From version 16.1.0 there are some updates on the statistics pages: Add pagination to search phrase list Allows choosing a custom date range to get...

Phong | Apr 22, 2024

Optimizely and the never-ending story of the missing globe!

I've worked with Optimizely CMS for 14 years, and there are two things I'm obsessed with: Link validation and the globe that keeps disappearing on...

Tomas Hensrud Gulla | Apr 18, 2024 | Syndicated blog

Visitor Groups Usage Report For Optimizely CMS 12

This add-on offers detailed information on how visitor groups are used and how effective they are within Optimizely CMS. Editors can monitor and...

Adnan Zameer | Apr 18, 2024 | Syndicated blog