Output caching can significantly improve the performance of an EPiServer site. However since EPiServer Visitor Groups were introduced it meant it was difficult to switch output caching on as each page could be unique for each user depending on the visitor groups matched. Also if a user returns to a page it may change depending on their actions on the rest of the site.

EPiServer Visitor Groups are a powerful feature and output caching can have real benefits in the right scenarios so I wanted the best of both worlds. So I've been thinking about a solution that would allow us to switch on output caching but still benefit from the use of visitor groups. 

Proposed approach

My proposed approach is very simple. It is to implement a unique hash depending on the visitor groups matched for each request. By implementing the hash it makes it possible to vary the cache depending on this hash. This in turn allows multiple versions of the page in the output cache varied by visitor group(s) matched. 

The code

OK so let's get into some code! First of all we need to tell EPiServer that we are happy to output cache the results by using the [ContentOutputCache] attribute. This is achieved as follows:

public class StartPageController : PageControllerBase
{
    [ContentOutputCache]
    public ActionResult Index(StartPage currentPage)
    {
        //Do your work here
    }
}

The [ContentOutputCache] output caches results using the output cache by with a dependency on the EPiServer version key. The cache settings for the attribute are configured in episerver.config. In order to ensure we take visitor groups into account we need to configure episerver.config as follows (other sections removed for brevity):


  ...
  
  ...

There are a couple of key parts of the configuration above. The httpCacheability="Server" section tells the server to cache the output on the server only. The reason for this is to stop the client browser caching the page and to make a request to the server each time in order to assess GetVaryByCustomString() for each request. This ensures visitor groups are assessed for each request and could mean a cached version could be served up if other users have matched the exact same visitor groups. The reason we need make a request to the server each time is because a user's journey on the site may mean that the visitor groups they have matched have changed.

Next up is httpCacheVaryByCustom="VisitorGroupHash". This tells ASP.net to vary the cached versions of the output by the custom parameter called "VisitorGroupHash". When ASP.net attempts to get a version of the page, it will execute GetVaryByCustomString and we can vary the result depending on the matched visitor groups.

Next and most importantly GetVaryByCustomString needs to be overriden in Global.asax.cs with a custom implementation to return a unique string if the custom parameter == VisitorGroupHash:

public override string GetVaryByCustomString(HttpContext context, string custom)
{
    try
    {
        if ("VisitorGroupHash".Equals(custom))
        {
            var helper = new VisitorGroupHelper();
            StringBuilder sb = new StringBuilder();
            foreach (var role in GetRolesForContext(context))
            {
                sb.Append(role);
                sb.Append(helper.IsPrincipalInGroup(context.User, role));
                sb.Append("|");
            }
            return sb.ToString();
        }
    }
    catch
    {
    }
    return base.GetVaryByCustomString(context, custom);
}

Potential implementations of GetRolesForContext()

There are a couple of ways this GetVisitorGroupsForRequest() could implemented. The most simple implementation is to simply assess all visitor groups defined in the system as shown below:

private IEnumerable GetRolesForContext(HttpContext context)
{
    var repo = VisitorGroupRole.GetRepository();
    return repo.GetAllRoles();
}

This will work if your site has a small number of visitor groups and/or you are only going to use output caching on some key pages on the site (e.g. homepage and key landing pages). However it will not scale as the number of versions of a page will be number of visitor groups to the power of 2.

A more generic approach would be to inspect the request, load up the content item, see if any visitor groups are used on a page and only return these visitor groups. I haven't coded this up yet!

Impacts to consider

The overall impact of this approach could be far faster page response times, particularly if some parts of the page are expensive to build. Memory usage could increase dramatically across the site if you have several visitor groups and are caching 1000's of pages in the output cache.

Disclaimer

This was only a quick proof of concept. This is released as is, you use it at your own risk.


Comments

comments powered by Disqus