Donut Hole Caching in ASP.NET MVC

asp.net, code, asp.net mvc 0 comments suggest edit

A while back, I wrote about Donut Caching in ASP.NET MVC for the scenario where you want to cache an entire view except for a small bit of it. The more technical term for this technique is probably “cache substitution” as it makes use of the Response.WriteSubstitution method, but I think “Donut Caching” really describes it well — you want to cache everything but the hole in the middle.

However, what happens when you want to do the inverse. Suppose you want to cache the donut hole, instead of the donut?

House of Sims
Photostream

I think we should nickname all of our software concepts after tasty food items, don’t you agree?

In other words, suppose you want to cache a portion of the view in a different manner (for example, with a different duration) than the entire view? It hasn’t been exactly clear how do to do this with ASP.NET MVC.

For example, the Html.RenderPartial method ignores any OutputCache directives on the view user control. If you happen to use Html.RenderAction from MVC Futures which attempts to render the output from an action inline within another view, you might run into this bug in which the entire view is cached if the target action has an OutputCacheAttribute applied.

I did a little digging into this today and it turns out that when you specify the OutputCache directive on a control (or page for that matter), the output caching is not handled by the control itself. Rather, it appears that compilation system for ASP.NET pages kicks in and interprets that directive and does the necessary gymnastics to make it work.

In plain English, this means that what I’m about to show you will only work for the default WebFormViewEngine, though I have some ideas on how to get it to work for all view engines. I just need to chat with the members of the ASP.NET team who really understand the deep grisly guts of ASP.NET to figure it out exactly.

With the default WebFormViewEngine, it’s actually pretty easy to get partial output cache working. Simply add a ViewUserControl declaratively to a view and put your call to RenderAction or RenderPartial inside of that ViewUserControl. If you’re using RenderAction, you’ll need to remove the OutputCache attribute from the action you’re pointing to.

Keep in mind that ViewUserControls inherit the ViewData of the view they’re in. So if you’re using a strongly typed view, just make the generic type argument for ViewUserControl have the same type as the page.

If that last paragraph didn’t make sense to you, perhaps an example is in order. Suppose you have the following controller action.

public ActionResult Index() {
  var jokes = new[] { 
    new Joke {Title = "Two cannibals are eating a clown"},
    new Joke {Title = "One turns to the other and asks"},
    new Joke {Title = "Does this taste funny to you?"}
  };

  return View(jokes);
}

And suppose you want to produce a list of jokes in the view. Normally, you’d create a strongly typed view and within that view, you’d iterate over the model and print out the joke titles.

We’ll still create that strongly typed view, but that view will contain a view user control in place of where we would have had the code to iterate the model (note that I omitted the namespaces within the Inherits attribute value for brevity).

<%@ Page Language="C#" Inherits="ViewPage<IEnumerable<Joke>>" %>
<%@ Register Src="~/Views/Home/Partial.ascx" TagPrefix="mvc" TagName="Partial" 
%>
<mvc:Partial runat="server" />

Within that control, we do what we would have done in the main view and we specify the output cache values. Note that the ViewUserControl is generically typed with the same type argument that the view is, IEnumerable<Joke>. This allows us to move the exact code we would have had in the view to this control. We also specify the OutputCache directive here.

<%@ Control Language="C#" Inherits="ViewUserControl<IEnumerable<Joke>>" %>
<%@ OutputCache Duration="10000" VaryByParam="none" %>

<ul>
<% foreach(var joke in Model) { %>
    <li><%= Html.Encode(joke.Title) %></li>
<% } %>
</ul>

Now, this portion of the view will be cached, while the rest of your view will continue to not be cached. Within this view user control, you could have calls to RenderPartial and RenderAction to your heart’s content.

Note that if you are trying to cache the result of RenderPartial this technique doesn’t buy you much unless the cost to render that partial is expensive.

Since the output caching doesn’t happen until the view rendering phase, if the view data intended for the partial view is costly to put together, then you haven’t really saved much because the action method which provides the data to the partial view will run on every request and thus recreate the partial view data each time.

In that case, you want to hand cache the data for the partial view so you don’t have to recreate it each time. One crazy idea we might consider (thinking out loud here) is to allow associating output cache metadata to some bit of view data. That way, you could create a bit of view data specifically for a partial view and the partial view would automatically output cache itself based on that view data.

This would have to work in tandem with some means to specify that the bit of view data intended for the partial view is only recreated when the output cache is expired for that partial view, so we don’t incur the cost of creating it on every request.

In the RenderAction case, you really do get all the benefits of output caching because the action method you are rendering inline won’t get called from the view if the ViewUserControl is outputcached.

I’ve put together a small demo which demonstrates this concept in case the instructions here are not clear enough. Enjoy!

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

28 responses

  1. Avatar for Mohamed Meligy
    Mohamed Meligy May 12th, 2009

    Cool :D
    One of the GREAT benefits of the fact that ASP.NET MVC Framework is simply still "ASP.NET" :) :) :).
    This fact is usually a BIG saver in many situations.
    Thanks a lot. Many would question/doubt whether that should work (Y).

  2. Avatar for Erik van Brakel
    Erik van Brakel May 12th, 2009

    I agree with Mohamed, having the full .NET backend is really useful at times. I'm not quite at the point of needing caching yet, but knowing how and where I can apply it is very useful!
    Oh, and on-topic: I thought there was an unwritten rule in software engineering to use corny names and bad acronyms when it comes to naming your internal systems ;-) Either that or use codenames named after for instance X-men or funny alliterations (see: ubuntu release names).

  3. Avatar for huey
    huey May 12th, 2009

    Is there a plan on what to do with RenderAction, or the role that it fills?

  4. Avatar for KevDog
    KevDog May 12th, 2009

    I can't help it, but after hearing about donuts, holes, making things fit, I have to quote History of the World, Part I:


    Empress Nympho: Say Bob, do I have any openings that this man might fit?
    Crowd: Whooooaaaaaaa!
  5. Avatar for Jim Geurts
    Jim Geurts May 12th, 2009

    Phil, How are you passing the view data to the <mvc:Partial runat="server" /> control when there is no code behind for the view?

  6. Avatar for Chad Moran
    Chad Moran May 12th, 2009

    Aha, you have saved my fragment caching woes.
    Thanks again Phil, great post.

  7. Avatar for Tony Chevis
    Tony Chevis May 13th, 2009

    Regarding the population of ViewData, I guess if the query is being differed by LINQ then you're good to go.

  8. Avatar for Syed Ahmed
    Syed Ahmed May 14th, 2009

    Thanks Phil for cool caching thing..

    its a good idea to name after the eatable like MVC choclate..
    marshmallow filtering...

  9. Avatar for Ferry Meidianto
    Ferry Meidianto May 14th, 2009

    Great, you have solved how to cache the donut and the hole itself.
    Which part of the donut you want to cache next?
    Cool stuff ^_^ thanks a lot!

  10. Avatar for ari
    ari December 16th, 2009

    hi, can you please fix the rar file?

  11. Avatar for Abu
    Abu January 6th, 2010

    This was really helpful, in fact the examples made the concepts very clear and simple to understand

  12. Avatar for Saajid Ismail
    Saajid Ismail February 8th, 2010

    Thanks Phil. I was struggling with this for a while, and even posted on StackOverflow about my problem - ASP.Net MVC Database-driven menu with caching.
    Sadly no-one was able to help. Then I bumped into your post, and kicked myself coz I read it before but it just didn't click in my recyle bin of a brain...

  13. Avatar for Will
    Will February 14th, 2010

    I couldn't open/unzip your partialcachedemo.zip file. Is that file broken?
    The MD5SUM of this file is 2e3bd8bf72a149c875afcd0646a3368c.

  14. Avatar for Roy
    Roy February 27th, 2010

    I can't open the Zip file, error in zip file

  15. Avatar for Whynot
    Whynot March 10th, 2010

    Why went I use Html.RenderPartialView("Menu")
    If Menu is menu.ascx, the OutputCache was totaly ignored
    but if Menu is menu.aspx, the OutputCache work as expected.
    How can I modify RenderPartialView to support OutputCache for ViewUserControl?
    Thanks

  16. Avatar for haacked
    haacked March 24th, 2010

    Hi all, I've fixed the download. Sorry about that. It's now built with ASP.NET MVC 2.

  17. Avatar for Wes
    Wes March 25th, 2010

    Name of software’s after food item, sounds good. Chocolate ASP.NET, NCache Cherry and Mango Velocity (so mouth watering)
    More on a serious node, I think it’s a good idea but as far as my personal experience is concerned, ASP.NET cache is not very good when it comes to apply for large web farms. And the reason which I figured out behind this limitation of ASP.NET is that the cache in ASP.NET is a local cache which is stand alone in nature. So it unable to locate the distributed servers in the web farms which ends up in performance and scalability problems. To overcome these problems a third party distributed caching solution like NCache can be a good option

  18. Avatar for Smashd
    Smashd April 4th, 2010

    I think MVC 2 breaks this approach IF you're using RenderPartial() calls within the donut hole to be cached. We have several projects where the main nav and footer nav are partials, and they in turn make their own calls to RenderPartial() to build out pieces of the navigation. With MVC 1 we're able to successfully use your approach outlined above to cache the main nav and footer nav.
    However, after updating a project to MVC 2 the following behavior occurred:
    1.) The first page request returned the HTML out-of-order: for example, the HTML output by the RenderPartial() calls in the footer nav appeared before the "static" HTML that wrapped those calls.
    2.) The second page request returned only the static HTML portions, as if the footer nav's calls to RenderPartial() had not output any HTML.
    Removing the OutputCache directive from the footer nav and main nav fixes the problem, but then obviously that also removes the caching. :) I suppose I could flatten the footer nav and main nav (remove the RenderPartial() calls within them), but wanted to bring the issue to your attention. Thanks!
    Side note - I think your comment form doesn't like Google Chrome. I was having trouble submitting this form until I switched to IE.

  19. Avatar for Smashd
    Smashd April 7th, 2010

    As a follow-up to my previous comment, I found a simple workaround for the situation I described. Simply call Html.Partial() instead of Html.RenderPartial() (making sure to output the string from Html.Partial()) and everything caches correctly.
    There's a performance hit for using Partial() instead of RenderPartial(), but I think that's less of an issue since the output is getting cached anyway.

  20. Avatar for haacked
    haacked April 8th, 2010

    @Smashd that's a nice elegant simple workaround!

  21. Avatar for MkpH
    MkpH May 11th, 2010

    Has anyone got this working with RenderAction? After being cached (and displaying fine) on the first hit I then don't get any output for any subsequent page requests.
    Thanks.

  22. Avatar for Ciaran
    Ciaran May 17th, 2010

    I downloaded and ran the sample but I'm a little confused. If I click refresh the cached date doesn't change as expected but yet a call is made to the Controller Action. Does this mean that a call is still made to the repository to retrieve the data?

  23. Avatar for Jon
    Jon August 5th, 2010

    @Smashd Your solution saved me. Thanks man!

  24. Avatar for John Clayton
    John Clayton November 4th, 2010

    Any word if this will work as expected in MVC 3 and Razor? As of the beta calling Action() or RenderAction() on an action with the output cache attribute still does not cache it.

  25. Avatar for Dan
    Dan August 31st, 2011

    We are thinking of implementing donut hole caching. The current caching we are using is within an MVC controller on the main Index ActionResult... here is the syntax:
    [OutputCache(Duration = int.MaxValue, VaryByParam = "data", Location = OutputCacheLocation.Server,Order = 2)]
    public ActionResult Index(string data)
    {
    ...
    We have found that using the OutputCache this way causes the App Session to reset after the first request. Does anyone know why the App Session would reset when using OutputCache? When we comment out the cache syntax the App Session sticks just fine. Also, the OutputCache seems to also cache the HTTP Refer. Would donut hole caching fix these issues?
    Thanks in advance.
    Dan

  26. Avatar for Mehul Mehta
    Mehul Mehta March 16th, 2012

    Is it possible when my MVC 3.0 application is multilingual?
    ie. (localhost:xxxx/en-US/Users/UserStream/7)
    (localhost:xxxx/nl-NL/Users/UserStream/7)
    I have applied Donut Hole Caching in ASP.NET MVC 3.0 application but issue arise when i change language. caching return the same Culture(en-US) from cache when change language Eng to other language.
    Hopping for help....
    Thank in advance.
    With regards,
    Mehul Mehta

  27. Avatar for Lelala
    Lelala March 3rd, 2013

     @78fbc0e7b6d1068af9a01e204cf34279:disqus : Check your resx files for the language that you wanna set; if implemented properly ("as the wizard does it"), there woudn't be any issue with multilanguage, even with exotic languages :-)

  28. Avatar for Annonymusis
    Annonymusis October 10th, 2014

    Nice