First of all, what is donut caching?

Donut caching is a technique for caching the results of MVC Actions. You use a NuGet package called MvcDonutCaching. Once you have it installed and set up you get the benefit of caching for different sections of your pages (e.g. navigation menu, footer information etc).

Is it difficult to set up?

No it is super simple. I learned how to do it at The Umbraco UK Festival 2018 in a session run by Ismail Mayat from The Cogworks.
You basically install a NuGet package, decorate your controller actions with attributes and put some settings in the config. There is a helpful guide on the readme.

Ok I'm convinced, show me what to do.

First you just need to install the NuGet Package. I like to install it via the Package Manager Console:

Install-Package MvcDonutCaching -Version 1.3.1-rc1

So once you have the NuGet package installed, you just need to add some cache profiles to the web.config file.

<caching>
  <outputCacheSettings>
    <outputCacheProfiles>
      <add name="FiveMins" duration="300" varyByParam="*" />
      <add name="OneHour" duration="3600" varyByParam="*" />
      <add name="OneDay" duration="86400" varyByParam="*" />
      <add name="OneWeek" duration="604800" varyByParam="*" />
      <add name="OneMonth" duration="2629746" varyByParam="*" />
      <add name="SixMonths" duration="15778476" varyByParam="*" />
      <add name="OneYear" duration="31556952" varyByParam="*" />
    </outputCacheProfiles>
  </outputCacheSettings>
</caching>

You don't need all of these, but I thought it would be useful for you if I worked all of them out. The number is the number of seconds, so FiveMins = 5 x 60 = 300 seconds.

Now we can start using it

In the training course we used it for the footer links and content information. You can use it in most places where the content isn't likely to change, and if the content does change, you can invalidate the cache and get it repopulate with the latest.

I think it would be useful on the navigation, because how often does that change, so I'm going to add it to the Eye Love Lashes website, and put it on the navigation menu controller action.

At the moment, in the template I am calling the navigation partial directly like this:
@{ Html.RenderPartial("SiteLayout/_MainNavigation"); }

So to use Donut Caching we need to change this slightly and put it in a controller action and as we are using Umbraco it is on our surface controller. So I'm putting it in my SiteLayoutController which inherits from SurfaceController

using DevTrends.MvcDonutCaching;
using System.Collections.Generic;
using System.Web.Mvc;
using Umbraco.Core.Models;
using Umbraco.Web;
using Umbraco.Web.Mvc;

namespace ELL.Web.Controllers
{
    public class SiteLayoutController : SurfaceController
    {
        [ChildActionOnly]
        [DonutOutputCache(CacheProfile = "OneMonth")]
        public ActionResult RenderMainNavigation()
        {
            IPublishedContent homePage = CurrentPage.AncestorOrSelf("home");
            IEnumerable<IPublishedContent> model = homePage.Children(x => x.IsVisible());

            return PartialView("/Views/Partials/SiteLayout/_MainNavigation.cshtml", model);
        }
    }
}

Notice the ChildActionOnly attribute. This isn't to do with Donut Caching, this is so someone can't call this action from the url like '/umbraco/Surface/SiteLayout/RenderMainNavigation/'

Also notice the DonutOutputCache attribute, we are using the profile OneMonth which we defined in the web.config.

Then I have a partial view which looks like this:

@inherits UmbracoViewPage<IEnumerable<IPublishedContent>>

@using Umbraco.Web.Models

<header id="header">
    <nav>
        <ul>
            <li><a href="#menu">Menu</a></li>
        </ul>
    </nav>
</header>

<nav id="menu">
    <h2>Menu</h2>
    <ul class="links">
        <li><a href="/">Home</a></li>
        @if (Model != null && Model.Count() > 0)
        {
            foreach (IPublishedContent page in Model)
            {
                if (page.DocumentTypeAlias == "prettyLink")
                {
                    RelatedLinks relatedLinks = page.GetPropertyValue<RelatedLinks>("linkTarget");
                    RelatedLink prettyLink = null;
                    if (relatedLinks != null && relatedLinks.Count() > 0)
                    {
                        prettyLink = relatedLinks.FirstOrDefault();
                    }

                    if (prettyLink != null)
                    {
                        <li><a href="@prettyLink.Link" target="@(prettyLink.NewWindow ? "_blank" : null)">@prettyLink.Caption</a></li>
                    }
                }
                else
                {
                    <li><a href="@page.Url">@page.Name</a></li>
                }
            }
        }
    </ul>

    <ul class="actions vertical">
        <li><a href="#footer" class="button fit special">Contact</a></li>
    </ul>
</nav>

And now we change how we are calling this, in the WebBase.cshtml template to this:

@Html.Action("RenderMainNavigation", "SiteLayout")

If we leave it like this then every time the Action of RenderMainNavigation is called, it will get it from the cache, for a month. This is ok but if we change the content we might want to invalidate the cache, so we do that like this:

Invalidating the Cache on Publish

Add a class which inherits from ApplicationEventHandler and in it, get it to add an event to the ContentService_Published event which will invalidate the cache.

using DevTrends.MvcDonutCaching;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;

namespace ELL.Web.EventHandlers
{
    public class ContentEvents : ApplicationEventHandler
    {
        protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
        {
            Umbraco.Core.Services.ContentService.Published += ContentService_Published;
        }

        private void ContentService_Published(Umbraco.Core.Publishing.IPublishingStrategy sender, Umbraco.Core.Events.PublishEventArgs<Umbraco.Core.Models.IContent> e)
        {
            var navigationDocTypeAliases = new List<string> { "content", "prettyLink", "newsList" };

            //if the document type alias of the published content item is in the list above then invalidate the cache. 
            if (e.PublishedEntities.Any(x => navigationDocTypeAliases.Contains(x.ContentType.Alias)))
            {
                var cacheManager = new OutputCacheManager();
                cacheManager.RemoveItem("SiteLayout", "RenderMainNavigation");
            }
        }
    }
}

Notice the list of document type aliases. You just add the aliases of the doc types you are interested in and it will invalidate the cache if they are published.

How to test it is working

You can add this line to the partial:

<p>@DateTime.Now</p>

Without caching, this value will change every time you refresh the page, keeping up with the current date and time. But with caching you can see that it stays the same until the cache is invalidated. To invalidate the cache, just save and publish a page which has a doc type that is in the list of document type aliases in the ContentService_Published method.

What about Preview Mode?

Dave Woestenborghs gave me some pointers, the first one was about preview mode. If you want to skip the caching when you are in preview mode, you need to create a custom attribute like this which you will use instead of the default DonutOutputCache attribute. Here is Dave's code for this:

public class UmbracoDonutOutputCacheAttribute : DonutOutputCacheAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        // overrides donut output cache attribute to not cache when in umbraco preview mode or Doctype grid editor preview
        this.Duration = -1;
        var previewMode = UmbracoContext.Current.InPreviewMode;
        if (!previewMode)
        {
            if (UmbracoContext.Current.HttpContext.Request.QueryString["dtgePreview"] == "1")
            {
                previewMode = true;
            }
        }
        if (previewMode)
        {
            this.Duration = 0;
        }
        base.OnActionExecuting(filterContext);
    }
}

What about load balanced sites?

Dave also made a good point about invalidating the cache in a Load Balanced site. So here is Dave's code for how to invalidate the cache if you have a load balanced website:

public class UmbracoStartup : ApplicationEventHandler
{
    protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication,
        ApplicationContext applicationContext)
    {
        PageCacheRefresher.CacheUpdated += (sender, args) => this.ClearCache();
    }

    private void ClearCache()
    {
        var cacheManager = new OutputCacheManager();
        cacheManager.RemoveItems();
    }
}

I personally prefer the way Ismail showed me, by using the ContentService_Published event because it only clears the cache if the document type alias matches, but I can see that if you are in a load balanced website, you need to used this way to invalidate the cache because I am told the ContentService_Published event only fires on the server where the change happened.

That's it

I hope you found this article useful, thanks to Ismail Mayat and The Cogworks for teaching me how to do this, and Dave for his pointers.

I made a video

If you learn better from watching a video then watch this YouTube video I made for you.

Want to thank me?

If I've helped you out and you want to thank me, why not buy me a coffee?

About the author

Paul Seal

Umbraco MVP and .NET Web Developer from Derby (UK) who specialises in building Content Management System (CMS) websites using MVC with Umbraco as a framework. Paul is passionate about web development and programming as a whole. Apart from when he's with his wife and son, if he's not writing code, he's thinking about it or listening to a podcast about it.

Related Posts

How I use source control for my Umbraco website source code and media

In this post I share my approach to using source control for my Umbraco website source code and medi…

Read Post

How to solve the error "Could not load file or assembly 'Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.0.0 ..."

This post will show you how to solve the error "Could not load file or assembly 'Microsoft.CodeDom.P…

Read Post

Boosting your career by creating a personal brand

In this post I share the content of the talk I did at the Umbraco UK Festival 2018 about how to boos…

Read Post

What are Content Apps in Umbraco v8

In this post I tell you about Content Apps in Umbraco v8 and show you what ideas I've had and what I…

Read Post