Issues

A Perspective On Caching In Umbraco

I have been working with Umbraco since 2007, and have seen many types of implementations. Most of the caching strategies and patterns I have seen have caused problems. In this article I will share some perspectives on what I have learned about caching when it comes to Umbraco and C#.

Expensive Tree Crawling

Way back when Umbraco was XML and XSLT oriented, it made sense to traverse the XML cache using XSLT functions such as ancestor-or-self, descendant-or-self, etc. It was actually as efficient as it could be. Fast forward a few years to a time when C# developers were using the C# equivalents in the code-behind's of master pages and user controls, then eventually razor views. These days it is not uncommon to see code similar to the following:

var home = Model
            .AncestorsOrSelf(1)
            .Children()
            .FirstOrDefault(x => x.ContentType.Alias == "Home");

However, tree crawling very often has resulted in cumulative slowness. Many developers sought to mitigate the performance issues via caching.

Html.CachedPartial

The following code snippet is from the Umbraco documentation.

@Html.CachedPartial("MyPartialName", new MyModel(), 3600)

This code does what you think it does. It allows you to render a partial view, and then automatically cache the result of the rendered HTML for 3600 seconds.

There is a huge problem with this...

How do you turn it off?

Being able to turn off cache is as important being able to cache something. Let that sink in for a moment...

It is very common during development for your application to start slowly. Developers often attribute this to issues such as IIS "warmup", debug mode, or the dotnet JIT compiler. After the first page loads, everything seems to run smoothly right?

Consider perhaps the possibility that the cache has covered up a performance problem deep in the bowels of your application. This problem pops up when the cache times out. Unfortunately the performance problem arises when your client or users are active on the live website.

Now you may want to consider the following questions: How do I know if the cache is hiding a performance problem? How would I turn off cache so I can see if the application is actually slow? What is the implication of using CachedPartial in many places? How to I performance profile the code with all these cached partials everywhere?

With these questions in mind, you probably want to get a bit more granular with your caching strategy. So you will perhaps drill deeper into your C# code, and start wrapping potentially problematic code with some caching.

IAppPolicyCache

In this section, I'll talk about service layer caching, and Umbraco's documented caching strategy.

The following code snippets are from the Umbraco documentation.

The code snippets define an ITagService, which is implemented by a concrete class called TagService. We don't need to get into the dependency injection (DI) part just yet, however just note that DI is used (behind the scenes) to automatically pass an instance of the TagService to MVC and WebAPI controllers.

public interface ITagService
{
    IEnumerable<TagModel> GetAll(
        string group,
        string cacheKey,
        TimeSpan? timeout = null);
}
public class TagService : ITagService
{
    private readonly ITagQuery _tagQuery;
    private readonly IAppPolicyCache _runtimeCache;

    public TagService(ITagQuery tagQuery, AppCaches appCaches)
    {
        _tagQuery = tagQuery;
        // Grap RuntimeCache from appCaches
        // and assign to our private field.
        _runtimeCache = appCaches.RuntimeCache;
    }

    public IEnumerable<TagModel> GetAll(
        string group,
        string cacheKey,
        TimeSpan? timeout = null)
    {
        // GetCacheItem will automatically insert the object
        // into cache if it doesn't exist.
        return _runtimeCache.GetCacheItem(cacheKey, () =>
        {
            return _tagQuery.GetAllTags(group);
        }, timeout);
    }
}

The following code shows a WebAPI controller which uses the TagService. It defines a cache key to pass into the service, and also a cache timeout.

public class TagsController : UmbracoApiController
{
    private readonly ITagService _tagService;

    // Dependency injection rocks!
    public TagsController(ITagService tagService)
    {
        _tagService = tagService;
    }

    [HttpGet]
    public IEnumerable<TagModel> GetDefaultTags()
    {
        // As mentioned earlier we want tags from "default"
        // group to be cached for a minute.
        return _tagService.GetAll("default", "defaultTags",
            TimeSpan.FromMinutes(1));
    }

    [HttpGet]
    public IEnumerable<TagModel> GetBlogTags()
    {
        // If you don't specify a TimeSpan the object(s)
        // will be cached until manually removed or
        // if the site restarts.
        return _tagService.GetAll("blog", "blogTags");
    }
}

Some Issues

As I stated earlier, being able to turn off cache is as important as the caching itself.

So here are a few questions which you might ask: How do I turn it off? Doesn't a controller only care that a service has a value? Why does the controller need to know that the value is being cached? Should it really care about the cache key? What if I have multiple services using the IAppPolicyCache, and want to disable cache for just one of them? How can I do some targeted performance profiling? Can the usage of the GetAll() method cause unpredictable results when this TagService is a dependency of multiple classes? After all, the cache time is injected into the service by the calling method. What if I don't want to cache everything?

An Alternative Caching Pattern

I like to call this the Inversion of Control Cached Proxy Pattern. This is just a fancy name for using interfaces and dependency injection for what they are actually meant for.

Yes, in this pattern we are still using interfaces and dependency injection. However the main difference is that our interface is a little more granular, and we are implementing the interface in 2 classes.

Let’s first look at the interface…

Instead of defining a GetAll() method in the interface, we now separate it into GetDefaultTags(), and GetBlogTags(). We'll get to why this is important later.

The following is the new ITagService.

public interface ITagService
{
    IEnumerable<TagModel> GetDefaultTags();
    IEnumerable<TagModel> GetBlogTags();
}

The following is the modified service, TagService. Notice that the tag service no longer accepts the IAppPolicyCache, because it no longer does any caching.

public class TagService: ITagService
{
    public TagService(ITagQuery tagQuery)
    {
        _tagQuery = tagQuery;
    }
    ...
}

The following is the cached proxy, TagServiceCachedProxy. Notice that it takes the concrete class TagService as a constructor parameter, as well as the IAppPolicyCache.

public class TagServiceCachedProxy: ITagService
{
    public TagServiceCachedProxy(TagService tagService, IAppPolicyCache appCaches)
    {
        _tagService = _tagService;
        _runtimeCache = appCaches.RuntimeCache;
    }
    ...
}

At first glance you may think that there is not much difference. You may even think that the extra class and code are strange. Hopefully you will see that the advantages far outweigh the objections of the extra code when you see how powerful this strategy can be.

A SOLID TagService

One great advantage of splitting out the ITagService into 2 implementations is that the TagService class is now truly SOLID. The code is clean, and very easy to understand. If we want to performance profile it, then it is easy. There is no cache in the way.

See how lovely this class looks!

public class TagService : ITagService
{
    private readonly ITagQuery _tagQuery;
    public TagService(ITagQuery tagQuery)
    {
        _tagQuery = tagQuery;
    }

    public IEnumerable<TagModel> GetDefaultTags()
    {
        return _tagQuery.GetAllTags("default");
    }
    
    public IEnumerable<TagModel> GetBlogTags(string group)
    {
        return _tagQuery.GetAllTags("blog");
    }
}

A Swappable Proxy

The cached proxy in this example takes the TagService as constructor parameter, along with the IAppPolicyCache. Notice that the methods in this class only call the corresponding method in the TagService, which in turn does the tag query. The cache occurs in this TagServiceCachedProxy class, leaving the TagService clean.

public class TagServiceCachedProxy : ITagService
{
    private readonly TagService _tagService;
    private readonly IAppPolicyCache _runtimeCache;

    public TagServiceCachedProxy(TagService tagService, IAppPolicyCache appCaches)
    {
        _tagService = _tagService;
        _runtimeCache = appCaches.RuntimeCache;
    }

    public IEnumerable<TagModel> GetDefaultTags()
    {
        var cacheKey = $”{typeof(TagServiceCachedProxy)}_GetDefaultTags”;

        return _runtimeCache.GetCacheItem(cacheKey, () =>
        {
            return _tagService.GetAllTags(group);
        }, TimeSpan.FromMinutes(1)));
    }
    
    public IEnumerable<TagModel> GetBlogTags()
    {
        var cacheKey = $”{typeof(TagServiceCachedProxy)}_GetBlogTags”;

        return _runtimeCache.GetCacheItem(cacheKey, () =>
        {
            return _tagService.GetAllTags(group);
        });
    }
}

You may be thinking that this looks very similar to the original TagService in the Umbraco documentation example. At first glance, it does. However this strategy has a powerful distinction. The cached proxy class can actually be swapped out at app-start with the clean SOLID TagService, and vice versa. Therefore we now have the ability to disable cache entirely for this part of the application. We'll get to this in a moment.

What About Passthrough?

Consider a scenario where some methods in your Services should not be cached. One simple way to accomplish this is for the Cached Proxy to just pass the call through to the Service, without doing any cache. In the following example, we show a method called GetSomethingElse(), which simply calls the corresponding method in the TagService.

    public IEnumerable<TagModel> GetSomethingElse()
    {
        return  _tagService.GetSomethingElse(group);
    }
}

Registration Or Not

The most powerful feature of the IoC Cached Proxy is that you don't have to register it.

Consider a scenario (eg. during development) where you want to disable cache in your service(s). The IoC Cached Proxy allows you to simply add an application setting which you can check at the startup of your application. It is as simple as an if statement (see below).

public void Compose(Composition composition)
{
    if (ConfigurationManager.AppSettings["TagServiceCache:Enabled"] == "true")
    {
        composition.Register<TagService, TagService>();
        composition.Register<ITagService, TagServiceCachedProxy>();
    }
    else
    {
        composition.Register<ITagService, TagService>();
    }
    
    composition.Components().Append<Component>();
}

As you can see in the simple registration (above), you now have granular control over your cache. Enabling and disabling a service cache is as trivial as setting a flag in your web.config. This pattern allows you to enable caching for individual services.

Clearing Cache

You will notice that the CachedProxy was using cache keys such as:

var cacheKey = $”{typeof(TagServiceCachedProxy)}_GetBlogTags”;

The prefix is always the class name of the Cached Proxy. This is very useful because it allows us to isolate cache clearing to a single service. We only need to know the class name (the cache prefix), to allow us to clear the cache for a given service.

In your cache clearing Component, you would bind to an event such as ContentService.Published, then clear via regular expressions such as the following:

_runtimeCache.ClearByRegex($"{typeof(TagServiceCachedProxy)}_(.*)");

For the original Umbraco example, check out the Umbraco documentation.

Final Thoughts

I hope this article has given you some food for thought when it comes to caching implementations. My recommendation is to always aim to write good fast code first, then only use caching as a last resort. Most importantly, if you are going to use cache, ensure that you can turn it off, and have easy control over it.

Cache me on Twitter: @anthonydotnet Website: anthonydotnet.blogspot.com

Anthony Dang

Anthony is the Technical Director at Radley Yeldar in London. He has worked in web development since 2007, is active in the open source community (in particular Umbraco CMS), writes tech articles, and is a regular organiser & presenter at conferences and tech meetups. He lives and breathes automation and development processes, and is a vocal proponent of Agile methodologies and Behaviour/Test Driven Development. He has experience in creating high performing remote production teams spanning multiple countries, an Agile/Scum/Kanban enthusiast, and is always looking for ways to make development more efficient and enjoyable. Originally from Sydney, Australia, he is now based in London.

comments powered by Disqus