Output cache in Optimizely CMS 12 that vary by visitor group

After I wrote a blog post about Quick and dirty output cache in Optimizely CMS12, someone reminded me that this would not work well with visitor groups. All visitors would get the same cached content, even if they belonged to different visitor groups.

I was not using visitor groups and output caching at the same site, but I was curious on how this could be solved, so I tried. I had a look at this 8-year-old blog post by David Knipe. It pointed me in the right direction, but things did not work exactly the same today.

The solution

In my case, I ended up with a single page type that would be able to use visitor groups, and the rest would not. For the page type that could potentially use visitor groups, I would add the VaryByCustom parameter to the ContentOutputCache-attribute, like this.

[ContentOutputCache(Duration = 604800, VaryByCustom = "visitorgroups")]

And then create my own implementation of IOutputCacheVaryByCustomService like this.

public class OuputCacheVaryByCustomService : IOutputCacheVaryByCustomService
{
    private readonly IPrincipalAccessor _principalAccessor;
    private readonly IVisitorGroupRepository _visitorGroupRepository;
    private readonly IVisitorGroupRoleRepository _visitorGroupRoleRepository;

    public OuputCacheVaryByCustomService(IPrincipalAccessor principalAccessor, 
        IVisitorGroupRepository visitorGroupRepository, 
        IVisitorGroupRoleRepository visitorGroupRoleRepository)
    {
        _principalAccessor = principalAccessor;
        _visitorGroupRepository = visitorGroupRepository;
        _visitorGroupRoleRepository = visitorGroupRoleRepository;
    }

    public string GetVaryByCustomString(HttpContext context, string arg)
    {
        if (arg == "visitorgroups")
        {
            return string.Join('|', GetActiveRolesForContext(context));
        }
            
        return string.Empty;
    }

    private IEnumerable<string> GetActiveRolesForContext(HttpContext context)
    {
        var roleNames = _visitorGroupRepository.List().Select(x => x.Name);

        foreach (var roleName in roleNames)
        {
            if (_visitorGroupRoleRepository.TryGetRole(roleName, out var role))
            {
                if (role.IsMatch(_principalAccessor.Principal, context))
                {
                    yield return roleName;
                }
            }
        }
    }
}

The method GetVaryByCustomString will produce a string that contains a list of all visitor group roles the current user belongs to, making sure that all users will get their content personalized – even with output cache enabled.

Finally, register the above service in the ConfigureServices method in your startup.cs like this.

services.TryAdd<IOutputCacheVaryByCustomService, OuputCacheVaryByCustomService>(ServiceLifetime.Singleton);

That's it! With this addition, the quick and dirty approach to output caching is slightly less dirty. You would still not be able to use Optimizely Forms, because of the AntiForgeryToken being cached. Also, the cache invalidation will not work across multiple instances in a load balanced setup.