Monday, 15 August 2016

Personalisation and caching with Umbraco

A little while ago I put together a package for Umbraco called Personalisation Groups as a means of providing personalisation features to the Umbraco CMS. One addition I've been considering is how to add better support for use of caching for a web application utilising personalisation features.

In any context, not just Umbraco, this isn't straightforward. Normally caching, at least on a page level, is going to be varied by the request URL. But of course with personalisation in place, for a given page, we are displaying different content to different users and so we don't want the cached version of a page customised to particular user being displayed to the next.

There are a few ways to potentially tackle this. Some personalisation engines would look to do this client-side, using JavaScript to make the page modifications. Donut caching might be another approach if you are able to construct the templates to match it's requirements. I've looked though to try to find a method using the standard way of applying page level caching with ASP.Net MVC, via output caching.

Output caching is available to Umbraco websites, at least those using their own controllers and hijacked routes. It's used then just as it is in any ASP.Net MVC application, though there's a need to ensure that, given a single controller may be responsible for a number of pages, that the cache is varied across the URL. The way to do this is nicely described in this blog post and should be handled in addition to that I describe below.

When setting up an output cache as well as the time to cache for it's possible to configure various things to vary the cached page by - querystring paramenters, HTTP headers or custom values such as the URL as described above. It's this latter support for custom values we can make use of here for caching personalised versions of pages.

With the personalisation groups package you create one or more group definitions - things like "Weekday morning visitors", "Overseas visitors" etc. and then associate them with pieces of content. To help with caching invalidation I've not set up a helper method as an extension to the Umbraco helper that generates a hash string for all the available groups for the current visitor. It checks each group, determines if the current visitor matches the group and stores the result. This is available from version 0.1.6.

The value of this string can itself be cached per user, which you'd likely want to do for a period of time as, though not expensive, it's not necessary to calculate on every page request. However it equally shouldn't be cached for too long as visitor's status in each personalisation group may change as they use the website. For example a group targeting morning visitors would no longer match if the same visitor is still there in the afternoon.

With that method in available, it's possible to use it with output caching to ensure the cache varies by this set of matched personalisation groups, for example with a controller like this:

    public class TestPageController : RenderMvcController
    {
        [OutputCache(Duration = 600, VaryByParam = "*", VaryByCustom = "PersonalisationGroupsVisitorHash")]
        public override ActionResult Index(RenderModel model)
        {
            ...
            return CurrentTemplate(vm);
        }

    }

And code in global.asax.cs as follows (credit here for code to determine the ASP.Net session cookie):

    public class Global : UmbracoApplication
    {
        private static readonly SessionStateSection SessionStateSection =   
           (SessionStateSection)ConfigurationManager.GetSection("system.web/sessionState");

        public void Session_OnStart()
        {
            // Just set something to ensure a session is created
            Session[AppConstants.SessionKeys.PersonalisationGroupsEnsureSession] = 1;
        }

        public override string GetVaryByCustomString(HttpContext context, string custom)
        {
            if (custom.Equals("PersonalisationGroupsVisitorHash", StringComparison.OrdinalIgnoreCase))
            {
                var cookieName = SessionStateSection.CookieName;
                var sessionIdCookie = context.Request.Cookies[cookieName];
                if (sessionIdCookie != null)
                {
                    var umbracoHelper = new UmbracoHelper(UmbracoContext.Current);
                    var hash = umbracoHelper.GetPersonalisationGroupsHashForVisitor(1093,   // Would normally get the node Id from config
                        sessionIdCookie.Value, 
                        20);
                    return hash;
                }
            }

            return base.GetVaryByCustomString(context, custom);
        }
    }

More details at the GitHub repository here. Comments and feedback welcome.

No comments:

Post a Comment