Twitter About Home

Partial Output Caching in ASP.NET MVC

The ASP.NET platform provides two major caching facilities:

Published Oct 15, 2008
  • Data caching, which lets you cache arbitrary .NET objects in an HttpContext’s Cache collection. You can specify expiration rules and cache entry priorities.
    • Output caching, which tells pages and user controls to cache their own rendered output and re-use it on subsequent requests. This is designed to sit neatly in WebForms’ system of server control hierarchies.</ul> Data caching continues to work perfectly well in ASP.NET MVC, because it’s just about getting objects in and out of a collection, and isn’t specific to any particular UI technology.

    If only output caching was so simple! ASP.NET’s output caching facility is deeply stuck in WebForms thinking, which makes it problematic in ASP.NET MVC. You *could *try to use ASP.NET output caching with ASP.NET MVC, but then you’d have the following issues:

    • No usable support for partial caching. ASP.NET output caching can cache complete responses or individual server controls, but hang on: we’re not using server controls in ASP.NET MVC, so all that’s left is complete response caching.
      • Bypasses authorization and other filters. ASP.NET output caching runs very early in the request-processing pipeline (see HttpApplication’s ResolveRequestCache event), long before MVC comes in with its controllers, actions, and filters. It can’t behave like an action filter is supposed to. See update below.</ul> Unfortunately, the [OutputCache] filter that ships with ASP.NET MVC is merely a thin wrapper around ASP.NET output caching, so it has exactly those problems. The MVC team have explained that they’re aware of the issues, but it’s very difficult to make ASP.NET output caching fit into MVC’s design, and they are focusing on other things first. And personally I’m happy with that: I’d rather see a finished 1.0 RTM release this year than fuss about output caching.

      Update: Since the Beta release, the [Authorize] filter now does some clever trickery to co-operate with ASP.NET output caching. Specifically, it registers a delegate using HttpCachePolicy.AddValidationCallback(), so that it can intercept future cache hits and tell ASP.NET output caching not to use the cache when [Authorize] would reject the request. This solves the problem of ASP.NET output caching bypassing the [Authorize] filter. If you’re going to write your own authorization filter, be sure to derive it from AuthorizeAttribute so you can inherit this useful behaviour.

      Note that this doesn’t stop ASP.NET output caching from bypassing any of your other action filters, and it doesn’t add any support for partial caching. If that’s a problem for you then consider using [ActionOutputCache] (below) instead.

      Fixing it ourselves

      One reason why I don’t mind [OutputCache]‘s limitations so much is that ASP.NET MVC is extremely extensible, and without too much trouble we can replace the output caching system with something new and more suitable.

      We can quite easily create a new caching filter that captures actions’ output and uses ASP.NET’s data caching facility to store it for next time. This filter will fit properly into the MVC pipeline, not strangely bypassing authorization or other earlier filters (it will run at the right time in whatever ordered set of filters you’ve using). And if you’re using something like the PartialRequest system for widgets that I described yesterday, it will naturally let you cache PartialRequests’ output separately from the actions that host them, which is also known as partial output caching.

      I must first acknowledge that the following code is very similar to the custom output caching filter that Maarten Balliauw presented way back in June. The reason I think this warrants a whole new post is because the following code works better for partial caching with PartialRequest and with MVC Contrib’s subcontrollers, and because it would be good for more MVC developers to discover it. But Maarten was first to write a good blog post on this subject.

      So here it is. Drop the following class somewhere in your MVC project:

      public class ActionOutputCacheAttribute : ActionFilterAttribute
      {
      // This hack is optional; I'll explain it later in the blog post
      private static MethodInfo _switchWriterMethod = typeof(HttpResponse).GetMethod("SwitchWriter", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
       
      public ActionOutputCacheAttribute(int cacheDuration)
      {
      _cacheDuration = cacheDuration;
      }
       
      private int _cacheDuration;
      private TextWriter _originalWriter;
      private string _cacheKey;
       
      public override void OnActionExecuting(ActionExecutingContext filterContext)
      {
      _cacheKey = ComputeCacheKey(filterContext);
      string cachedOutput = (string)filterContext.HttpContext.Cache[_cacheKey];
      if (cachedOutput != null)
          filterContext.Result = new ContentResult { Content = cachedOutput };
      else
          _originalWriter = (TextWriter)_switchWriterMethod.Invoke(HttpContext.Current.Response, new object[] { new HtmlTextWriter(new StringWriter()) });
      }
       
      public override void OnResultExecuted(ResultExecutedContext filterContext)
      {
      if (_originalWriter != null) // Must complete the caching
      {
          HtmlTextWriter cacheWriter = (HtmlTextWriter)_switchWriterMethod.Invoke(HttpContext.Current.Response, new object[] { _originalWriter });
          string textWritten = ((StringWriter)cacheWriter.InnerWriter).ToString();
          filterContext.HttpContext.Response.Write(textWritten);
       
          filterContext.HttpContext.Cache.Add(_cacheKey, textWritten, null, DateTime.Now.AddSeconds(_cacheDuration), Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.Normal, null);
      }
      }
       
      private string ComputeCacheKey(ActionExecutingContext filterContext)
      {
      var keyBuilder = new StringBuilder();
      foreach (var pair in filterContext.RouteData.Values)
          keyBuilder.AppendFormat("rd{0}_{1}_", pair.Key.GetHashCode(), pair.Value.GetHashCode());
      foreach (var pair in filterContext.ActionParameters)
          keyBuilder.AppendFormat("ap{0}_{1}_", pair.Key.GetHashCode(), pair.Value.GetHashCode());
      return keyBuilder.ToString();
      }
      }

      Now you can use [ActionOutputCache] instead of MVC’s built-in [OutputCache]. The advantage of [ActionOutputCache] is that it’s a fully native MVC action filter, and doesn’t rely on or inherit the problems of ASP.NET’s WebForms-oriented output caching technology. So, for example, [ActionOutputCache] plays nicely with MVC’s [Authorize] filter. Update: Since the beta release, the built in [Authorize] and [OutputCache] filters now play nicely together too.

      You can use [ActionOutputCache] to cache the output of any action method just like [OutputCache], but it’s perhaps most interesting when you combine it with the PartialRequests method of rendering widgets (or use it with Html.RenderAction(), which isn’t compatible with the built-in [OutputCache]). Put an [ActionOutputCache] attribute on the widget’s action method (not on the action that hosts it), then you’ll have partial page caching, as shown in the following code.

      public class BlogController : Controller
      {
      [ActionOutputCache(60)] // Caches for 60 seconds
      public ActionResult LatestPosts()
      {
      ViewData["currentTime"] = DateTime.Now;
      ViewData["posts"] = new[] {
          "Here's a post",
          "Here's another post. Marvellous.",
          "Programmer escapes from custody"
      };
      return View();
      }
      }

      image 

      This is great if your widget displays some relatively static data (e.g., a list of the “most recent” things), or is an action method whose output is constant for a given set of parameters (e.g, a dynamically-built navigation menu that highlights the visitor’s current location, where the current location is one of the parameters passed to the action method), and you don’t want to recompute it on every page hit.

      Support for partial caching is a major advantage of PartialRequest over the use of viewdata-populating filters and partial views to render widgets. The filter/partialview technique can never support proper output caching, because inherently it mixes the widget’s viewdata with the main page’s viewdata, and the two can’t be distinguished by the time you’re actually rendering the view. The closest you could get would be to limit yourself to data caching, but that’s more complex and not always viable anyway, such as if you’re using an IQueryable to defer a SQL query until view rendering time.

      Notes

      To keep the [ActionOutputCache] code short and easy to understand, and because its current behaviour is adequate for my own current project’s needs, there are a number of limitations and caveats you should know about:

      • It uses reflection to access HttpResponse’s private SwitchWriter() method. That’s how it’s able to intercept all the output piped to Response during subsequent filters and the action method being cached. It’s unfortunate that SwitchWriter() is marked private, but it is. If you don’t want to bypass the “private” access modifier this way, or if you can’t (e.g., because you’re not hosting in full-trust mode), then you can download an alternative implementation that uses a filter to capture output instead. This isn’t quite as straightforward, but some people will prefer/need it.
        • It’s hard-coded to generate cache keys that vary by all incoming action method parameters and route values, and not by anything else. You would have to modify the code if you needed the ability to vary cache entry by other context parameters (such as unrelated querystring or form values).
          • When generating cache keys, it assumes that the action method parameter types and route value types all have sensible implementations of GetHashCode(). This is fine for primitive types (strings, ints, etc.), but if you try to use it with a custom parameter types that have no proper implementation of GetHashCode(), it will pick a different cache key every time and appear not to be caching. So implement GetHashCode() properly on any such custom parameter types.
            • It doesn’t attempt to cache and replay HTTP headers, so it’s not suitable for caching action methods that issue redirections.</ul> In other words, it works great for most straightforward widget output caching scenarios, but if you’re doing something more complex then please be prepared to dive into the code yourself! Hope this is useful to a few people.
READ NEXT

Partial Requests in ASP.NET MVC

In your ASP.NET MVC application, it can be tricky to combine multiple independent “widgets” on the same page. That’s because a WebForms-style hierarchy of independent controls clashes awkwardly against a purist’s one-way MVC pipeline. Widgets? I’m taking about that drill-down navigation widget you want in your sidebar, or the “most recent forum posts” widget you’d put in the page footer. Things that need to fetch their own data independently of the page that hosts them.

Published Oct 14, 2008