Solving Caching Issues in the Sitefinity MVC Navigation Widget

September 18, 2017 Digital Experience, Sitefinity

When pages get cached all the widgets on them are cached too. The problem here is that this caching can cause user confusion. When a widget is cached its nodes are cached and some of them could still be visible even if you log out, or some of the menu nodes could be hidden even if you are authenticated. This case should sound familiar to you if you have read the documentation in detail or used the Navigation controls in some of the described scenarios.

This issue is solved elegantly in the WebForms Navigation by exposing OutputCache settings – VaryByAuthenticationStatus and VaryByUserRoles. When one of those two settings is set, the Navigation's cache is invalidated when a user logs in or there is a change in the user's role. Unfortunately, the MVC Navigation does not offer those settings, so for MVC this case requires different solutions.

How to solve the caching problem of the Sitefinity MVC Navigation widget?

There are three immediate solutions:

  • The first solution is obvious, although I would like to mention it: use the hybrid templates and use the WebForms Navigation widget instead of the MVC implementation. The styling of the widget will be a little bit different, but you will benefit quickly from the OutputCache settings.
  • The second solution is to stop caching the pages that do not require authentication. This way you will be sure that when the user logs out, they will see only the public part of the web site in the navigation.
  • The third solution is to implement a custom MVC Navigation widget and extend it with OutputCache settings.

The third solution will provide you with the most flexibility. It might seem like the hardest, but let’s take a look at how we can put that together with just a few lines of code.

Implementing a custom MVC Navigation with OutputCache settings

We don't have to come up with a complex implementation. To start, just look at the code of the WebForms Navigation control and see how the output cache mechanism works there. Use JustDecompile and open Telerik.Sitefinity.dll assembly and look for the LightNavigationControl control and for the CreateChildControl method. Those four lines of code are doing the magic:

if (this.OutputCache.VaryByAuthenticationStatus || this.OutputCache.VaryByUserRoles)
{
    this.outputCacheVariation = new NavigationOutputCacheVariation(this.OutputCache);
    PageRouteHandler.RegisterCustomOutputCacheVariation(this.outputCacheVariation);
}

 

Here is how the MVC Navigation controller looks if we add the full implementation of those four lines of code and implement the dependencies:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Web.Mvc;
using ServiceStack.Text;
using Telerik.Sitefinity.Frontend.Mvc.Infrastructure.Controllers;
using Telerik.Sitefinity.Frontend.Mvc.Infrastructure.Controllers.Attributes;
using Telerik.Sitefinity.Frontend.Navigation.Mvc.Models;
using Telerik.Sitefinity.Frontend.Navigation.Mvc.StringResources;
using Telerik.Sitefinity.Modules.Pages.Configuration;
using Telerik.Sitefinity.Mvc;
using Telerik.Sitefinity.Services;
using Telerik.Sitefinity.Web;
using Telerik.Sitefinity.Web.UI;
using Telerik.Sitefinity.Web.UI.ContentUI.Contracts;
using Telerik.Sitefinity.Web.UI.NavigationControls.Cache;
using Telerik.Sitefinity.Frontend.Navigation.Mvc.Controllers;
using Telerik.Sitefinity.Security.Claims;
using System.Linq;
 
namespace SitefinityWebApp.Mvc.Controllers
{
    [ControllerToolboxItem(Name = "MyCustomNavigation", Title = "My Custom Navigation", SectionName = "Custom MV CWidgets")]
    [Localization(typeof(NavigationResources))]
    [IndexRenderMode(IndexRenderModes.NoOutput)]
    public class MyNavigationController : NavigationController
    {
 
        protected override ViewResult View(IView view, object model)
        {
 
            if (this.OutputCache.VaryByAuthenticationStatus || this.OutputCache.VaryByUserRoles)
            {
                MyPageRouteHandler.RegisterCustomOutputCacheVariation(new NavigationOutputCacheVariation(this.OutputCache));
            }
 
            return base.View(view, model);
        }
 
        private NavigationOutputCacheVariationSettings outputCacheSettings;
        //private string templateNamePrefix = "NavigationView.";
 
        /// <summary>
        /// Provides UI for setting navigation output cache variations
        /// </summary>
        [TypeConverter(typeof(ExpandableObjectConverter))]
        public NavigationOutputCacheVariationSettings OutputCache
        {
            get
            {
                if (this.outputCacheSettings == null)
                {
                    this.outputCacheSettings = new NavigationOutputCacheVariationSettings();
                }
 
                return this.outputCacheSettings;
            }
 
            set
            {
                this.outputCacheSettings = value;
            }
        }
    }
 
 
    public class MyPageRouteHandler : PageRouteHandler
    {
        internal static void RegisterCustomOutputCacheVariation(ICustomOutputCacheVariation cacheVariation)
        {
            if (string.IsNullOrEmpty(cacheVariation.Key))
                throw new InvalidOperationException("The Key property of the cache variation cannot be empty.");
            var context = SystemManager.CurrentHttpContext;
 
            var cacheVariationsRegistry = context.Items[PageRouteHandler.RegisteredCacheVariations] as CustomOutputCacheVariationsRegistry;
            if (cacheVariationsRegistry == null)
            {
                cacheVariationsRegistry = new CustomOutputCacheVariationsRegistry();
                context.Items[PageRouteHandler.RegisteredCacheVariations] = cacheVariationsRegistry;
            }
            cacheVariationsRegistry.AddVariation(cacheVariation);
            cacheVariationsRegistry.Validated = true;
        }
    }
 
    internal class CustomOutputCacheVariationsRegistry
    {
        public CustomOutputCacheVariationsRegistry()
        {
            this.variations = new List<ICustomOutputCacheVariation>();
            this.changed = true;
        }
 
        public CustomOutputCacheVariationsRegistry(IList<ICustomOutputCacheVariation> variations)
        {
            this.variations = variations;
            this.Validated = false;
        }
 
        public bool Validated
        {
            get;
            set;
        }
 
        public bool Changed
        {
            get
            {
                return this.changed;
            }
        }
 
 
        public IList<ICustomOutputCacheVariation> Variations
        {
            get
            {
                return this.variations;
            }
            //set
            //{
            //    this.variations = value;
            //}
        }
 
        public void AddVariation(ICustomOutputCacheVariation variation)
        {
            var existingVariation = this.variations.FirstOrDefault(v => v.Key == variation.Key);
            if (existingVariation != null)
            {
                if (existingVariation.Equals(variation))
                    return;
 
                this.variations.Remove(existingVariation);
            }
 
            this.variations.Add(variation);
            this.changed = true;
            ignoreCache = null;
        }
 
        public bool IgnoreCache()
        {
            if (!ignoreCache.HasValue)
                ignoreCache = variations.Any(cv => cv.NoCache);
            return this.ignoreCache.Value;
        }
 
        private readonly IList<ICustomOutputCacheVariation> variations;
        private bool? ignoreCache;
        private bool changed;
    }
 
    internal class NavigationOutputCacheVariation : CustomOutputCacheVariationBase
    {
        private NavigationOutputCacheVariationSettings Settings { get; set; }
 
        public NavigationOutputCacheVariation(NavigationOutputCacheVariationSettings settings)
        {
            this.Settings = settings;
        }
 
        /// <inheritdoc />
        public override string Key
        {
            get
            {
                return "sf-navigation-view";
            }
        }
 
        /// <inheritdoc />
        public override bool NoCache
        {
            get
            {
                if (this.Settings.VaryByAuthenticationStatus)
                    return ClaimsManager.GetCurrentIdentity().IsAuthenticated;
                else
                    return false;
            }
        }
 
        /// <inheritdoc />
        public override string GetValue()
        {
            if (this.Settings.VaryByUserRoles)
            {
                var identity = ClaimsManager.GetCurrentIdentity();
                var orderedRoles = identity.Roles.OrderBy(r => r.Id).Select(r => r.Id);
                var value = string.Join(string.Empty, orderedRoles);
                var hashedValue = value.GetHashCode().ToString();
                return hashedValue;
            }
            else
                return string.Empty;
        }
    }
}

 

The last step will be to create the views and the widget designers – but this is effortless since you can simply copy and paste those files from the default implementation of the widget. Keep in mind that the above solution is tested with the 10.1 version. If you want to use a different version, you must copy and paste the views and designers corresponding to the version that you are using.

Will OutputCache settings be implemented in the MVC Navigation widget itself?

With Sitefinity 10.2 the above implementation will come out of the box and you can remove the custom code. Although the solution is coming to the latest version soon, for those of you using an older version this solution will continue to work. Happy coding!

Peter Filipov

Peter Filipov (Pepi) is a Product Builder focused on building the future of Sitefinity, relying on the newest technologies such as .NET 6 (and up), React and Angular. His previous experience as a Developer Advocate and Manager of Engineering helps him to understand the customers’ and market needs of Sitefinity. He also is passionate about being active and healthy.