How and when to use Cache Substitution in Sitefinity

Sitefinity uses OutputCache to cache HTML page output. This greatly increases the performance of page rendering because it does not have to compile or generate the HTML. If you are unfamiliar with this process, please take a look at this informational post on page rendering in Sitefinity.

What is it?

Post Cache Substitution is a method of rendering a single control dynamically, while rendering the rest of the page HTML quickly from cache. You are essentially substituting a portion of the cached HTML with dynamically-rendered HTML. Technically, it should be substituting the content at render-time, so that search engines see the dynamic content. If the content that you need to dynamically render is not vital for SEO, then you can use a web services approach (which we will talk about later in the post)

How can I use it?

If you are developing your site using ASP.NET Web Forms, then you can follow the Sitefinity docs: https://docs.sitefinity.com/for-developers-use-a-cache-substitution-widget. But, if you are using Feather (MVC) to develop your widgets, their is no documented approach to dynamically rendering a single widget.

Wait, why can’t I use the OutputCache attribute from MVC? This is because Sitefinity isn’t really using the full MVC stack. It is still using WebForms behind the scenes. So using [OutputCache(NoStore=true)] is not going to work when building MVC widgets.

I have managed to get an MVC widget to use cache substitution, but this is not an officially supported approach (mostly because Sitefinity’s page router handler code keeps changing). It uses a custom controller proxy. The controller proxy is referenced in your ToolboxesConfig.config as the “type” attribute. You can download the code for the controller proxy here: MvcControllerProxyNoCache.cs. To use it, you’ll just need to replace the proxy type in the ToolboxesConfig.config for your widget and re-drag the widget onto your pages.

Here is the code for the proxy at the time of this post:

using System;
using System.Collections.Generic;
using System.Net;
using System.Reflection;
using System.Web;
using System.Web.UI;
using Telerik.Sitefinity.Abstractions;
using Telerik.Sitefinity.Mvc;
using Telerik.Sitefinity.Mvc.Proxy;
using Telerik.Sitefinity.Services;
using Telerik.Sitefinity.Web;
using Telerik.Sitefinity.Web.UI.PublicControls;

namespace Avisra.Samples
{
    public class MvcControllerProxyNoCache : MvcControllerProxy
    {
        private bool IsPostBack
        {
            get { return HttpContext.Current.Request.HttpMethod == WebRequestMethods.Http.Post; }
        }

        protected override void OnPreRender(EventArgs e)
        {
            this.ExecuteController();
        }

        protected override void Render(HtmlTextWriter writer)
        {
            if (!SystemManager.IsDesignMode && !SystemManager.IsInlineEditingMode)
            {
                this.Execute();
            }
            else
            {
                base.Render(writer);
            }
        }

        public string RenderStatic(Dictionary<string, string> parameters)
        {
            if (HttpContext.Current.Handler != null)
            {
                return this.output;
            }

            var context = HttpContext.Current;
            if (context.Request.RequestContext.RouteData.Values.Count == 0)
            {
                context.Request.RequestContext.RouteData = new SitefinityRoute().GetRouteData(new HttpContextWrapper(context));
            }

            var controllerActionInvoker = ObjectFactory.Resolve<IControllerActionInvoker>();
            var proxyControl = new MvcControllerProxy
            {
                ControllerName = parameters["ControllerName"],
                SerializedSettings = parameters["SerializedSettings"]
            };

            if (proxyControl.Page == null)
            {
                var page = HttpContext.Current.Handler as Page;
                if (page == null)
                {
                    page = new Page();
                    page.GetType().InvokeMember("SetIntrinsics", BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.NonPublic, Type.DefaultBinder, page, new[] { HttpContext.Current });
                }

                proxyControl.Page = page;
            }

            string output;
            if (controllerActionInvoker.TryInvokeAction(proxyControl, out output))
                return output;

            return string.Empty;
        }

        private void Execute()
        {
            if (this.executed)
                return;

            var parameters = new Dictionary<string, string> 
                { 
                    { "ControllerName", this.ControllerName },
                    { "SerializedSettings", this.SerializedSettings }
                };
            var renderMarkup = new CacheSubstitutionWrapper.RenderMarkupDelegate(RenderStatic);
            var wrapper = new CacheSubstitutionWrapper(parameters, renderMarkup);

            wrapper.RegisterPostCacheCallBack(HttpContext.Current);

            this.executed = true;
        }

        private void ExecuteController()
        {
            if (this.controllerExecuted)
                return;

            var controllerStrategy = ObjectFactory.Resolve<IControllerActionInvoker>();

            this.controllerExecuted = controllerStrategy.TryInvokeAction(this, out this.output);
        }

        private bool controllerExecuted = false;
        private bool executed;
        private string output;
    }
}

When should we use cache substitution?

Ideally, never. Cache substitution has some annoying compatibility issues. For starters, if you want to dynamically compress HTML before inserting into cache (dynamicCompressionBeforeCache), you will run into errors if you are using substitution in your widgets. By default, Sitefinity has dynamicCompressionBeforeCache set to false, so the issue rarely appears for users. Also, some 3rd party tools have issues with it, like New Relic: https://discuss.newrelic.com/t/post-cache-substitution-is-not-compatible/1539.

What alternatives are there?

If you need your dynamic content present in the HTML for search engines, then you are stuck with Cache Substitution or invalidating cache for the whole page using Cache Dependencies. Keep in mind that using dependencies will invalidate the entire page from cache. So if this content is something that changes very frequently, you probably want to stay away from invalidating the page cache, or your users will start to notice.

If you don’t need this specific content for SEO, then you should seriously consider loading it via web services. You can easily inject some content from web services into the HTML on-the-fly using JavaScript. While this approach doesn’t allow the dynamic content to be indexed by search engines, it is much faster than the cache substitution approach. If you are familiar with jQuery and wiring up Web API controllers into Sitefinity projects, then this concept should be cake for you. If not, then take a look at the LoginStatus widget built into Sitefinity Feather.

The approach used by the LoginStatus widget uses a JsonResult route as the API endpoint instead of a custom Web API controller. Personally, I prefer having my own controller outside of the context of the feather widget. But if you like to keep everything simple and organized, then carry on. You can check out the JsonResult method Sitefinity is using here: https://github.com/Sitefinity/feather-widgets/blob/master/Telerik.Sitefinity.Frontend.Identity/Mvc/Controllers/LoginStatusController.cs

Summary

If you need your dynamic content indexed by search engines, then you are stuck with Cache Substitution or invalidating the page cache. If you don’t, then go with web services.

Happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *