DevTrends

Donut Output Caching in ASP.NET MVC 3

Donut caching is the one form of output caching that is conspicuously absent from ASP.NET MVC 3 (and MVC4 as we speak) and is greatly missed by many developers. This post describes MvcDonutCaching, a new open-source NuGet package that adds donut caching to MVC3 in a simple and performant manner.

Output Caching in ASP.NET MVC

Output caching in MVC is very easy to understand. In fact, it is not so different from the output caching in ASP.NET WebForms that most people will already been familiar with. In MVC, output caching allows you to store the output of controller actions and return these cached results instead of actually executing the controller actions on subsequent calls. Given that most pages in a commercial web application will typically include data that comes from one or more services or databases, it is easy to see that returning a cached result instead of calling these services will be significantly quicker and will also allow the server to respond to considerably more requests.

Some people seem to be rather dismissive of output caching, believing that it is not suitable or offers no benefit for their particular web apps but in fact, the vast majority of sites can benefit from output caching. Two important points to make are: 1) Very small cache durations of a few seconds can have a dramatic impact on a high-traffic site. This means that even near real-time applications can make use of output caching. 2) Even if you are caching heavily within the application, output caching can still add significant performance boosts. Retrieving data from a cache and then rendering the page using this data is typically slower than retrieving the whole page from the output cache.

What is Donut Caching?

When we talk about output caching, there are three different scenarios that we may encounter:

Full page caching

Full page caching is where you cache the entire page with no substitutions. It is handled with the built-in OutputCache attribute that has been available since ASP.NET MVC 1.

Partial page caching (Donut hole caching)

Donut hole caching is where you cache one or more parts of a page, but not the entire page. It is handled by using the built-in OutputCache attribute on one or more child actions (called using Html.Action or Html.RenderAction from the parent view). It has been available since ASP.NET MVC 3.

Partial page caching (Donut caching)

Donut caching is where you cache the entire page except for one or more parts. It was possible in ASP.NET MVC 1 by using the substitution control, but is not available out of the box in ASP.NET MVC 3.

The omission of donut caching in MVC3 is unfortunate as it is an extremely popular requirement. In fact, any site that allows a user to log in will usually not be able to use full page caching (at least, not without employing secondary ajax requests as a workaround).

How should donut caching work?

In ASP.NET MVC, output caching is implemented using an action filter attribute which can be applied to a controller or to individual controller actions. When an action is executed for the first time, the OutputCacheAttribute intercepts the ActionResult before the output is returned to the client and stores this output for a configurable period of time. On subsequent requests, the output is returned from the cache rather than executing the controller action, significantly reducing the amount of work necessary to service the request.

To implement donut caching, the concept is fairly simple. We still cache the page output and return it from the cache on subsequent requests, but we do require some extra steps. Before we cache the page, we need to identify the areas that we do not want to cache (the donut holes). These areas must be marked in some way so that when we retrieve the page from the cache, we can replace these areas with non-cached versions before returning the page to the client.

Sounds easy...so what's the problem?

Whilst ASP.NET MVC is built on top of the ASP.NET framework, it has some quite significant differences that have made the re-use of ASP.NET features rather difficult in MVC. One such difficulty is the way that output caching works. If you look at the MVC3 source code, you will find that full page output caching and partial page output caching are implemented in completely different ways. Whilst full page caching hooks into standard ASP.NET page caching and uses an HttpModule (OutputCacheModule), partial page caching uses a separate code path that saves the output using the MemoryCache object introduced in .NET 4. When retrieving an item, there is code in the OutputCacheAttribute to get a partial page from the MemoryCache. For a full page however, the OutputCacheAttribute never even gets hit and the page is retrieved using the OutputCacheModule without even touching the MVC framework.

Once you know this, it is easier to understand why some features are available in full page caching that do not work in partial page caching (see next section). In terms of implementing donut caching, hooking into the OutputCacheModule HttpModule would be very difficult/impossible, so instead we must rewrite the OutputCacheAttribute completely, saving both full and partial pages in the same caching store and fixing some of the inconsisitencies seen in the current implementation.

Other limitations of output caching in MVC3.

Many people have already described several limitatons with child action caching in MVC 3 including this post by Greg Roberts. Greg proposes some nice solutions which we will include in our new cache attribute.

In summary, the four main limitations are:

Cache Profiles are not supported

It is very useful to allow each output cache to share a common configuration rather than hard-coding a numeric duration for each one. The standard mechanism for this in ASP.NET is to use cache profiles. Cache profiles allow you to use the web.config to specify one or more profiles for your caching requirement. Each ouput cache definition then references a profile. This allows you to change the cache configuration for multiple items at once and without requiring a code deployment.

<caching>
  <outputCacheSettings>
    <outputCacheProfiles>
      <add name="Level3" duration="300" varyByParam="*" />
    </outputCacheProfiles>
  </outputCacheSettings>
</caching>

In ASP.NET MVC 3, cache profiles are supported for standard controller actions, but not for child actions.

Disabling caching in the web.config is ignored

The standard ASP.NET ouput caching framework allows you to disable output caching by changing a web.config value. This is extremely useful for running your code locally where caching can interfere with debugging.

<caching>
  <outputCache enableOutputCache="false"/>
  <outputCacheSettings>
  ...
  </outputCacheSettings>
</caching>

Unfortunately, while full page caching does adhere to this value, partial page caching completely ignores it.

With the advent of web.config transformations, it is now very simple to automate the process of enabling caching when releasing code. Simply add the following to web.config.release and when you build your application in release mode, your web.config will automatically be transformed to remove the enableOutputCache="false" configuration

<system.web>
  <compilation xdt:Transform="RemoveAttributes(debug)" />
  <caching>
    <outputCache xdt:Transform="Remove" />
  </caching>    
</system.web>

It is not easy to remove output cache items, particularly for child actions

It is often quite useful to be able to remove items from the outputcache dynamically. If you have this ability, it is possible to ramp up cache durations quite significantly because you can automatically invalidate the cache when you know that data has changed.

In MVC, for non web farm scenarios, we can make use of the ASP.NET RemoveOutputCacheItem method on the Response where the path parameter is the relative path to the page. Because it is URL based, if you have an action with arguments that are part of the route such as a page number, it is not possible to remove all cached pages for that particular action. Instead, you need to call the method for each different argument which is not ideal.

Response.RemoveOutputCacheItem(path)

For child actions, it is even worse. The only method that I could find was posted on the ASP.NET forums:

OutputCacheAttribute.ChildActionCache = new MemoryCache("NewDefault");

Clearly this is not ideal. Instantiating a new MemoryCache will prevent the OutputCache from retrieving ALL previously cached child actions, not just the one(s) that we wanted to remove. Another problem is the fact that these items are not actually removed from the cache, so they are still in memory. Because the OutputCacheAttribute uses MemoryCache.Default, no garbage collection will take place and the newly instantiated MemoryCache will slowly fill up will duplicate items.

Using a custom output cache store is problematic

Whilst the standard ASP.NET in-process caching mechanism is sufficient for single web servers, enterprise level applications typically run on large web farms. In these kind of environments, a distributed cache such as memcached or Windows Server AppFabric Caching makes much more sense. A useful feature introduced in .NET 4.0 is the ability to write your own implementation of OutputCacheProvider and configure ASP.NET to use it via the web.config. This feature makes is extremely easy to change your site to use distributed caching. Unfortunately, the build-in OutputCacheAttribute does not use the configured OutputCacheProvider for child actions. Instead, the MVC team have added a static property on the OutputCacheAttribute class named ChildActionCache of type ObjectCache.

Although none of these issues are dealbreakers, it makes sense to address these issues if we are going to rewrite the OutputCacheAttribute. In the next section, we will see exactly how we have done this.

Introducing the MvcDonutCaching NuGet package

As we have already discussed, in order to implement donut caching, we first need a way of expressing that a particular piece of content should not be cached within a page. There are many different approaches that we could take. Maarten Balliauw's donut caching article from 2008 uses a substitution HTML helper which allows you to specify a static method to call, but we are going to take a slightly different approach.

We are going to add multiple overloads to the Html.Action helper method to allow a user to specify that the action should be excluded from the page cache.

public static MvcHtmlString Action(this HtmlHelper html, string actionName, 
                       string controllerName, RouteValueDictionary routeValues, 
                       bool excludeFromParentCache)
{
  if (excludeFromParentCache)
  {
    var settings = new ActionSettings
    {
      ActionName = actionName,
      ControllerName = controllerName,
      RouteValues = routeValues
    };

    var serialisedSettings = new ActionSettingsSerialiser().Serialise(settings);

    return new MvcHtmlString(serialisedSettings);
  }

  return html.Action(actionName, controllerName, routeValues);
}

If the excludeFromParentCache parameter is set to true, instead of actually rendering the action, we serialise the action arguments and return an html comment containing the arguments. This comment will be included in the cached page and when we come to the substitution code later on, we will be able to find all the areas that need replacing very easily.

So that was very easy and the code is pretty clean. The next thing to look at is the OutputCacheAttribute itself. As we have already discussed, the built-in OutputCacheAttribute hooks into the ASP.NET output caching mechanism which uses an HttpModule to intercept requests for cached content very early in the request pipeline. Trying to add substutition to this mechanism would be very difficult, hence the reason that Microsoft has not implemented donut caching in the last few releases of ASP.NET MVC. Therefore, we cannot simply subclass OutputCacheAttribute and instead need to build our caching action filter by subclassing ActionFilterAttribute and implementing the caching logic from scratch.

Note that I am not going to show all the code here, just the interesting bits, but you can download the full source code from CodePlex if you are curious.

When subclassing ActionFilterAttribute, we have four methods that we can override:

Our caching action filter has two main functions. Firstly, it must check for a cached action output and secondly, it must cache the output if it is not already cached. Let's look at each function in turn.

Retrieving a cached action output

The code that attempts to retrieve a cached action output must execute as early as possible to avoid unnecessary code execution. Therefore the OnActionExecuting method is used.

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
    _cacheSettings = BuildCacheSettings();

    if (_cacheSettings.IsCachingEnabled)
    {
        _cacheKey = _keyGenerator.GenerateKey(filterContext, _cacheSettings);

        var content = _outputCacheManager.GetItem(_cacheKey);

        if (content != null)
        {
            filterContext.Result = new ContentResult 
  {
    Content = _donutHoleFiller.ReplaceDonutHoleContent(content, filterContext) 
  };
        }
    }
}

If caching is disabled or the cached content is not found, the filterContext.Result is not set and the MVC framework will continue on to executing the controller action. But by setting the filterContext.Result when a cached item is found, the controller action will not get executed and the cached page will be served. Note that we are passing the cached output into the ReplaceDonutHoleContent method which is used to make the substitutions.

public string ReplaceDonutHoleContent(string content, ControllerContext filterContext)
{
    if (filterContext.IsChildAction)
    {
        return content;
    }

    return DonutHoles.Replace(content, new MatchEvaluator(match =>
    {
        var settings = _actionSettingsSerialiser.Deserialise(match.Groups[1].Value);

        return InvokeAction(filterContext.Controller, settings.ActionName, settings.ControllerName, settings.RouteValues);
    }));
}

The ReplaceDonutHoleContent uses a regular expression to find all actions that need substituting. Remember that we are using HTML comments to pass information about the actions that need to be executed. For each match found, We deserialise the action settings and replace the HTML comment with the result of the InvokeAction method. This means that the page that is returned does not include the HTML comment so the end user has no idea of the caching mechanism being used.

private static string InvokeAction(ControllerBase controller, string actionName, 
                       string controllerName, object routeValues)
{
  var viewContext = new ViewContext(controller.ControllerContext, 
                    new WebFormView(controller.ControllerContext, "tmp"),
                    controller.ViewData, controller.TempData, 
                    TextWriter.Null);

  var htmlHelper = new HtmlHelper(viewContext, new ViewPage());

  return htmlHelper.Action(actionName, controllerName, routeValues).ToString();
}

The code in InvokeAction is a bit hacky but it is all necessary in order to be able to execute the html helper action method from within the action filter, rather than from the view where it was intended to be called from. Obviously, we do not have a viewContext available, so we must build a fake one in order to instantiate the HtmlHelper.

Caching the action output

I initially assumed that I would use one of the OnResultXXX methods for caching the action output. It makes logical sense to allow the action to execute normally and then just before returning the result to the client, we save the result to the cache. If this was all that we needed to do, then this approach would suffice, but there is a bit more work to be done. Remember that if a page uses one of our Html.Action overloads then instead of rendering the action, it will render an HTML comment instead. Therefore, after we save the action ouput containing the HTML comment to the cache, we need to replace the comment with the actual output in the same way that we are doing in OnActionExecuting. Remember that the code in OnActionExecuting will be used once the item is in the cache whereas the OnActionExecuted code will be used the first time the action is requested and the cache is empty.

If we try this approach though then it will soon become apparent that the OnResultXXX do not allow you to change the filterContext.Result making it impossible to do the substitutions. This means that we need to use OnActionExecuted instead and rather than using the framework to execute the action, we must do it ourselves.

public override void OnActionExecuted(ActionExecutedContext filterContext)
{
    var viewResult = filterContext.Result as ViewResultBase; // only cache views/partials

    if (viewResult != null)
    {
        var content = _actionOutputBuilder.GetActionOutput(viewResult, filterContext);

        if (_cacheSettings.IsCachingEnabled)
        {
            _outputCacheManager.AddItem(_cacheKey, content, 
                DateTime.Now.AddSeconds(_cacheSettings.Duration));
        }

        filterContext.Result = new ContentResult 
        {
          Content = _donutHoleFiller.ReplaceDonutHoleContent(content, filterContext) 
        };
    }
}

Again the code is very simple. First we cast the filterContext result to ViewResultBase. ViewResultBase is the base class of both View and PartialView (the only ActionResults we are supporting), so if the cast is not null, we proceed. The call to ActionOutputBuilder returns us the HTML output of the action. Any donut holes will be marked as HTML comments. At this stage, unless caching is disabled via the web.config, we cache the output, complete with donut hole comments so we can find a replace them with updated content on subsequent calls. Next we make a call to the DonutHoleFiller (my favourite class name ever) which replaces the donut hole comments with the actual HTML content. The final HTML is then wrapper up in a content result and returned.

Usage

Using the MvcDonutCaching NuGet package is very simple. Add the package to your MVC project from within Visual Studio. To do this, select Tools | Library Package Manager and then choose either Package Manager Console or Manage NuGet Packages. Via the console, just type install-package MvcDonutCaching and hit return. From the GUI, just search for MvcDonutCaching and click the install button.

Once installed, you can add the DonutOutputCache attribute to one or more controller actions or to a whole controller if you want all actions within the controller to use donut caching. Many of the options from the built in OutputCache attribute are available to use, so all of the following are valid:

[DonutOutputCache(Duration=5)]
public ActionResult Index()

[DonutOutputCache(CacheProfile="FiveMins")]
public ActionResult Index()

[DonutOutputCache(Duration=50, VaryByCustom="whatever")]
public ActionResult Index()

[DonutOutputCache(Duration=50, VaryByParam="something;that")]
public ActionResult Index(string something)

[DonutOutputCache(Duration=50, VaryByParam="none")]
public ActionResult Index(int referrerId)

If you are using a cache profile, do not forget to add the profile to the web.config

<caching>
  <outputCacheSettings>
  <outputCacheProfiles>
    <add name="FiveMins" duration="300" varyByParam="*" />
  </outputCacheProfiles>
  </outputCacheSettings>
</caching>

You can read more about the OutputCacheAttribute options on MSDN.

If you run your application at this stage, the DonutOutputCache will just act in the same way as the built-in OutputCache and cache the full page. In order to start donut caching, we need to introduce a donut hole. Remember that we have implemented this by adding several overloads to the Action HTML helper. In all overloads, we just need to add a final true parameter to the call indicating that we do not want it cached with the parent page.

@Html.Action("Login", "Account", true)  

Run the application again and you will see the donut caching in action. Whilst the parent page will be cached for whatever duration you specified, the donut hole action will be executed every request. Alternatively, you can add the OutputCacheAttribute or DonutCacheAttribute to the child action as well but use a lower cache duration. This allows you to have, for example, part of the page cached for five minutes, but the rest of the page cached for one hour.

To remove an item from the output cache, you can use the OutputCacheManager class which exposes a number of different methods and overloads that can be used for cache item removal.

var cacheManager = new OutputCacheManager();

//remove a single cached action output (Index action)
cacheManager.RemoveItem("Home", "Index");

//remove a single cached action output (page 1 of List action)
cacheManager.RemoveItem("Home", "List", new { page = 1 });

//remove all cached actions outputs for List action in Home controller
cacheManager.RemoveItems("Home", "List");

//remove all cached actions outputs for Home controller
cacheManager.RemoveItems("Home");

//remove all cached action outputs
cacheManager.RemoveItems();

Conclusion

Output caching can significantly increase the speed and throughput of a web server, even if you are caching heavily elsewhere in your application. Many web pages have some user customisation making them unsuitable for full page caching, but by using the new MvcDonutCaching NuGet package, you can now benefit from output caching on these pages as well. Add the MvcDonutCaching NuGet Package to your application to get started or download the code from codeplex if you are curious or want to contribute.

Next time, we will look at how easily we can configure an application using MvcDonutCaching to use distributed caching.

Useful or Interesting?

If you liked the article, we would really appreciate it if you would share it with your Twitter followers.

Share on Twitter

Comments

Comments are now closed for this article.