Practical .NET
Speed Up Your ASP.NET MVC Application with Doughnut Hole Caching
The OutputCache attribute is a great way to improve both response time and scaleability, except there are many times when you can’t use it. Here’s how to leverage the HtmlHelper Action method to handle those exceptions.
In this article, I want to do two things. First, I want to provide a more flexible way for you to use the OutputCache method that will allow you to use it more frequently by targeting it more precisely. Second, and on my way to that first goal, I want you to think about the distinction between layout Views and ordinary Views differently. To be more specific, I want to convince you to think about your ordinary Views as a kind of layout View.
There is no doubt that the OutputCache is a wonderful thing. Just by slapping the OutputCache attribute on an action method in your ASP.NET MVC controller, you can achieve two goals: reduce response time for the request associated with that method and reduce demands on your server. The OutputCache attribute achieves that goal by capturing the HTML generated from the View specified by the method and, the next time that method is executed for an "identical" request, bypasses the method altogether and simply returns the previously generated HTML.
But, often, developers find they can’t use the OutputCache attribute because OutputCache caches the whole page -- both your View and the layout View that encloses the view. To put it another way, that "Welcome back, <Customer Name>" in your page’s header can prevent you from ever using OutputCache on your site.
However, that’s just the more limiting case. Fundamentally, if you have any variable content in your layout View (even the time of day), then you probably can’t use OutputCache. But, if you look at the View itself, you can probably identify some parts of the page that are variable and some parts that aren’t. So, picking on the layout View is sort of unfair: The problem also exists inside individual Views. If you could apply output caching on a finer-grained scale than "the whole page," you’d be better off.
What you’re thinking about at this stage is a technique called doughnut hole caching. But, to take advantage of it, you have to think about caching your content differently.
Doughnut and Doughnut Holes
The idea of a doughnut applied to a Web page reflects the way most pages are constructed. With most pages there’s a core of variable content (the doughnut hole) surrounded by a ring of non-variable content (the doughnut). In ASP.NET MVC, when we recognize this structure in our pages, we move the non-variable content to a layout View and place the variable content in the Views and Partial Views referenced by our methods.
But as I’ve suggested, the distinction between layout and View content isn’t perfect. A better way to think about your page’s content is to divide your content into two categories:
- The part that varies from one request to another
- The part that’s independent of any particular request
With that perspective, your ordinary Views also consist of request-dependent content and request-independent content. In fact, the "doughnut hole" metaphor breaks down here because, from that perspective, your typical View consists of multiple holes: Wherever you insert specific content into your View, you have a hole that might be eligible for caching.
Traditionally, the way we fill all of those holes is by retrieving all the data for all the holes in an action method and then passing it as one huge lump to the View. Some of that data is related to the request -- but much is not. If we really wanted to exploit caching our doughnut holes, then we’d retrieve our data for a View differently.
Action and Partial Views
Imagine, for example, a View that displays a sales order. While it’s entirely possible that the sales order’s information may have changed, you’re probably also displaying some customer, product, shipping/billing terms information that probably haven’t changed since the last time someone retrieved it. For example, the details of any specific shipping/billing terms probably haven’t changed in years -- you just assign a particular billing code to the order.
The tools for implementing doughnut hole caching are relatively simple: You just need the HtmlHelper’s Action method and Partial Views. Rather than retrieve all the data for a View in your controller, you retrieve only the data that’s guaranteed to have changed as a result of the request.
There are three stops in the process:
- You develop Action methods that return any variable data (customer, billing, products and so on)
- You replace the part of your View that displayed that data with a call to those Action methods using the HtmlHelper’s Action method
- You place the part of the View that displays that data in a Partial View
For the first step, a typical method to return the billing terms might look like the following code. The OutputCache attribute I’ve added to the method ensures that the code only runs the first time a new billingTermCode is passed to the method; the ChildActionOnly attribute ensures that the method can’t be called except from a View (that is, not from a URL):
<OutputCache(VaryByParam="billingTermCode")>
<ChildActionOnly>
Public Function GetBillingTerm(billingTermCode As BillingEnum) As ActionResult
Dim billingTerm As BillingTerm
'...code to retrieve billing terms into billingTerm using the billingTermCode
Return PartialView("~/Views/SalesOrders/BillingTerms.cshtml", billingTerm)
End Function
I’ll skip over the View to get the final step: To call this Action method in your View, retrieve the Partial View, and incorporate it into your page, you use the HtmlHelper’s Action method, passing the name of the method and any data required:
@Html.Action("GetBillingTerm", New With {.billingTerm = Model.billingTermCode});
In this example, I’ve assumed that the Action method is in the same Controller as the Action method that called this View. If that wasn’t true, I’d include the Controller name in the anonymous object I pass to the Action method.
What’s attractive about this solution is that all of the cache management is handled declaratively. You aren’t responsible for writing any of the code for managing the cache -- that’s all taken care of by ASP.NET. If you’re lucky, you’ll find other Views in your project where you can use this Action method and View.
What Can Go Wrong
However, there is a very real potential for your performance to get worse using this technique. For example, it’s possible you could have retrieved all the customer and product information with a single request for the sales order (by, for example, using the Include method in a LINQ/Entity Framework query). If so, then making multiple trips to the database for customers and products is going to give you worse performance than making that single trip.
The strategy I’ve suggested here assumes that those costs will be offset by the amount of cached data that eliminates both trips to the database and regenerating existing HTML. Because that may not be true, you’ll need to be judicious about how aggressively you want to use this technique.
In the scenario that I’ve used here, I’d be more inclined to cache my customer data (which might generate one extra trip to the database per sales order) than my product data (which might generate multiple trips to the database per sales order). Even for customer data, I’d also want to determine how long I can reasonably expect customer information be cached and how many unique vs. repeated customer requests happen in the period. On the other hand, I would unhesitatingly cache the billing terms data.
Caching is a powerful tool. Thinking about the holes in your Views can open up opportunities for caching that can significantly improve your application’s performance.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.