Banner

On Sitefinity Custom Widget Caching

Jun 20, 2017

Sitefinity is Caching too Hard!

If you’ve done software development for any length of time, you’ve likely run into a caching issue. Caching is difficult to get right, but it is so beneficial that we work with it in nearly everything we do. In the case of Sitefinity, it performs caching all over the place. As a result, many aspects of the Sitefinity experience are greatly improved. If you disabled caching for a Sitefinity site altogether, the site would slow down immensely. Sitefinity, by its nature of being a Content Management System, stores the bulk of its content in the site’s backing data store (usually a database), so it would get very chatty if it couldn’t cache. However, like with all caching, we have the issue of cache invalidation to tackle. For built-in content widgets, this is already taken care of. But what if you have a custom content widget? You’ll quickly discover that a Sitefinity page, after rendered the first time, will not re-render even if said content is updated in the backend in some form. In this post I’ll show how you can enable automatic cache busting, so that custom widget caching is implemented correctly and your pages will always update when your content does.

Example: Dynamic Content Widget Implementing Custom Widget Caching

Here we have a controller working with a dynamic content item. This kind of controller implementation for custom widget caching can apply to any dynamic content type. The only thing that has to change is the type that you pass to the caching method. Let’s have a look and break it down.

using System;
using System.Collections.Generic;
using System.Web.Mvc;
using Telerik.Sitefinity.Data;
using Telerik.Sitefinity.DynamicModules.Model;
using Telerik.Sitefinity.Services;
using Telerik.Sitefinity.Utilities.TypeConverters;
using Telerik.Sitefinity.Web;
using Telerik.Sitefinity.Web.UI.ContentUI.Contracts;
 
namespace SitefinityWebApp.Mvc.Controllers
{
    public class ExampleDynamicContentWidgetController : Controller, IHasCacheDependency
    {
        private Type DynamicContentType => TypeResolutionService.ResolveType("Telerik.Sitefinity.DynamicTypes.Model.CustomModule.CustomType");
 
        public IList<CacheDependencyKey> GetCacheDependencyObjects()
        {
            return new List<CacheDependencyKey> { new CacheDependencyKey { Key = DynamicContentType.FullName, Type = typeof(DynamicContent) } };
        }
 
        protected void SubscribeToCacheDependency()
        {
            if (SystemManager.IsDesignMode || SystemManager.IsPreviewMode)
            {
                return;
            }
            IList<CacheDependencyKey> keys = GetCacheDependencyObjects();
 
            if (!SystemManager.CurrentHttpContext.Items.Contains(PageCacheDependencyKeys.PageData))
            {
                SystemManager.CurrentHttpContext.Items.Add(PageCacheDependencyKeys.PageData, new List<CacheDependencyKey>());
            }
            ((List<CacheDependencyKey>)SystemManager.CurrentHttpContext.Items[PageCacheDependencyKeys.PageData]).AddRange(keys);
        }
 
        public ActionResult Index()
        {
            SubscribeToCacheDependency();
 
            // Access your dynamic content here.
 
            return View("Default");
        }
    }
}

The first thing you should note is that you have to make your controller implement the “IHasCacheDependency” interface and implement the “GetCacheDependencyObjects” method. This is the cornerstone of custom widget caching. Inside you see we are returning a single CacheDependencyKey, and tying it to our custom content type.

“SubscribeToCacheDependency” is the method you use to wire in any of your MVC actions to the caching mechanism. Note how in our Index action the first thing we do is call it.

That’s all there is to it! Now our widget will tell any pages it is on to re-run and get fresh data, instead of being stuck with stale cached date. Custom widget caching is really that simple!

Example: News Content Widget Implementing Custom Widget Caching

This will look much like the above example, but I wanted to demonstrate how to set up caching for a built-in Sitefinity content type.

using System.Collections.Generic;
using System.Web.Mvc;
using Telerik.Sitefinity.News.Model;
using Telerik.Sitefinity.Services;
using Telerik.Sitefinity.Data;
using Telerik.Sitefinity.Web;
using Telerik.Sitefinity.Web.UI.ContentUI.Contracts;
  
namespace SitefinityWebApp.Mvc.Controllers
{
    public class ExampleNewsWidgetController : Controller, IHasCacheDependency
    {
        public IList<CacheDependencyKey> GetCacheDependencyObjects()
        {
            return new List<CacheDependencyKey> { new CacheDependencyKey { Type = typeof(NewsItem) } };
        }
  
        protected void SubscribeToCacheDependency()
        {
            if (SystemManager.IsDesignMode || SystemManager.IsPreviewMode)
            {
                return;
            }
            IList<CacheDependencyKey> keys = GetCacheDependencyObjects();
  
            if (!SystemManager.CurrentHttpContext.Items.Contains(PageCacheDependencyKeys.PageData))
            {
                SystemManager.CurrentHttpContext.Items.Add(PageCacheDependencyKeys.PageData, new List<CacheDependencyKey>());
            }
            ((List<CacheDependencyKey>)SystemManager.CurrentHttpContext.Items[PageCacheDependencyKeys.PageData]).AddRange(keys);
        }
  
        public ActionResult Index(string category)
        {
            SubscribeToCacheDependency();
  
            // Access News here.
  
            return View();
        }
    }
}

You see here that with the exception of GetCacheDependencyObjects creating a key whose type is “typeof(NewsItem)”, everything else plays out exactly the same. For built-in Sitefinity content types, you pass the type of the Model item (in this case, NewsItem) in order to configure it properly.

Refactoring Custom Widget Caching Widgets to Share Code

Since the two above are so similar, we definitely would want to refactor it. What we can do is create an abstract base class to make custom widget caching easy to implement going forward.

using System;
using System.Collections.Generic;
using System.Web.Mvc;
using Telerik.Sitefinity.Data;
using Telerik.Sitefinity.DynamicModules.Model;
using Telerik.Sitefinity.News.Model;
using Telerik.Sitefinity.Services;
using Telerik.Sitefinity.Utilities.TypeConverters;
using Telerik.Sitefinity.Web;
using Telerik.Sitefinity.Web.UI.ContentUI.Contracts;
  
namespace SitefinityWebApp.Mvc.Controllers
{
    public abstract class CacheDependencyController : Controller, IHasCacheDependency
    {
        protected abstract Type CachedObjectType { get; }
  
        public IList<CacheDependencyKey> GetCacheDependencyObjects()
        {
            var key = CachedObjectType.ToString().StartsWith("Telerik.Sitefinity.DynamicTypes.Model")
                ? new CacheDependencyKey {Key = CachedObjectType.FullName, Type = typeof(DynamicContent)}
                : new CacheDependencyKey {Type = CachedObjectType};
  
            return new List<CacheDependencyKey> {key};
        }
  
        protected void SubscribeToCacheDependency()
        {
            if (SystemManager.IsDesignMode || SystemManager.IsPreviewMode)
            {
                return;
            }
            IList<CacheDependencyKey> keys = GetCacheDependencyObjects();
            if (!SystemManager.CurrentHttpContext.Items.Contains(PageCacheDependencyKeys.PageData))
            {
                SystemManager.CurrentHttpContext.Items.Add(PageCacheDependencyKeys.PageData, new List<CacheDependencyKey>());
            }
            ((List<CacheDependencyKey>)SystemManager.CurrentHttpContext.Items[PageCacheDependencyKeys.PageData]).AddRange(keys);
        }
    }
  
    public class ExampleDynamicContentWidgetController : CacheDependencyController
    {
        protected override Type CachedObjectType => TypeResolutionService.ResolveType("Telerik.Sitefinity.DynamicTypes.Model.CustomModule.CustomType");
  
        public ActionResult Index()
        {
            SubscribeToCacheDependency();
  
            // Access your dynamic content here.
  
            return View("Default");
        }
    }
  
    public class ExampleNewsWidgetController : CacheDependencyController
    {
        protected override Type CachedObjectType => typeof(NewsItem);
  
        public ActionResult Index(string category)
        {
            SubscribeToCacheDependency();
  
            // Access News here.
  
            return View();
        }
    }
}

Our base class now handles the boilerplate code, as well as deciding the correct kind of CacheDependencyKey to create and return. Look how easy it is to slap on to any custom widget! We inherit from the abstract class, implement the CachedObjectType, and call SubscribeToCacheDependency in any action we wish to cache. Custom Widget Caching is now incredibly simple!

If you have multiple types you wish to enable, then this particular refactor won’t work. It could easily be adjusted to accept a list of Types to cache, however. GetCacheDependencyObjects would simply have to be adjusted to return the list of types, instead of the list of a single type.

With this new tool in your arsenal, you can now continue to use the power of caching in your Sitefinity site, while making sure custom widget caching is in place to bust caches and prevent your site from serving stale content.

Load more comments
comment-avatar

Copyright © 2017 Alain "Lino" Tadros