Donut Caching in ASP.NET MVC

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

UPDATE: Due to differences in the way that ASP.NET MVC 2 processes request, data within the substitution block can be cached when it shouldn’t be. Substitution caching for ASP.NET MVC is not supported and has been removed from our ASP.NET MVC Futures project.

This technique is NOT RECOMMENDED for ASP.NET MVC 2.

With ASP.NET MVC, you can easily cache the output of an action by using the OutputCacheAttribute like so.

[OutputCache(Duration=60, VaryByParam="None")]
public ActionResult CacheDemo() {
  return View();
}

One of the problems with this approach is that it is an all or nothing approach. What if you want a section of the view to not be cached?

mmmm-doughnut Well ASP.NET does include a <asp:Substitution …/> control which allows you to specify a method in your Page class that gets called every time the page is requested. ScottGu wrote about this way back when in his post on Donut Caching.

However, this doesn’t seem very MVC-ish, as pointed out by Maarten Balliauw in this post in which he implements his own means for adding output cache substitution.

However, it turns out that the Substitution control I mentioned earlier makes use of an existing API that’s already publicly available in ASP.NET. The HttpResponse class has a WriteSubstitution method which accepts an HttpResponseSubstitutionCallback delegate. The method you supply is given an HttpContext instance and allows you to return a string which is displayed in place of the substitution point.

I thought it’d be interesting to create an Html helper which makes use of this API, but supplies an HttpContextBase instead of an HttpContext. Here’s the source code for the helper and delegate.

public delegate string MvcCacheCallback(HttpContextBase context);

public static object Substitute(this HtmlHelper html, MvcCacheCallback cb) {
    html.ViewContext.HttpContext.Response.WriteSubstitution(
        c => HttpUtility.HtmlEncode(
            cb(new HttpContextWrapper(c))
        ));
    return null;
}

The reason this method returns a null object is to make the usage of it seem natural. Let me show you the usage and you’ll see what I mean. Referring back to the controller code at the beginning of this post, imagine you have the following markup in your view.

<!-- this is cached -->
<%= DateTime.Now %>

<!-- and this is not -->
<%= Html.Substitute(c => DateTime.Now.ToString()) %>

On the first request to this action, the entire page is rendered dynamically and you’ll see both dates match. But when you refresh, only the lambda expression is called and is used to replace that portion of the cached view.

I have to thank Dmitry, our PUM and the first developer on ASP.NET way back in the day who pointed out this little known API method to me. :)

We will be looking into hopefully including this in v1 of ASP.NET MVC, but I make no guarantees.

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

Comments

avatar

34 responses

  1. Avatar for David Pellerin
    David Pellerin November 5th, 2008

    very interesting. looking forward to trying this out.

  2. Avatar for Andrei Rînea
    Andrei Rînea November 5th, 2008

    This definitely must make it into V1. Definitely.

  3. Avatar for Jason
    Jason November 5th, 2008

    Yeah, get this in there. We'll wait a few more days/weeks. :)

  4. Avatar for PK
    PK November 5th, 2008

    +1111111111111111111111 to getting this into v1 pwitty please! Awesome post. Donut caching is one of those wikidly kewl secret gems in ASP.NET.
    Cheers Phil!

  5. Avatar for $104688327
    $104688327 November 5th, 2008

    yesss please please pleeeease include this in v1.
    and dont forget to include the ability to define custom validation messages at the model level. this is the only thing im missing at the moment with the beta.
    keep up the good work!

  6. Avatar for Jason Whitehorn
    Jason Whitehorn November 5th, 2008

    Yum, tasty. Especially if you guys can get this in v1.
    This would actually come in very useful for several things I am currently working on. So I am really hoping you guys can pull this off.

  7. Avatar for Shiju Varghese
    Shiju Varghese November 5th, 2008

    Phil,
    please include this in v1.

  8. Avatar for Ken Egozi
    Ken Egozi November 5th, 2008

    I still can't see the merit in the whole Partial caching thing.
    Server side caching should be about putting results of expensive logic in a cache. Views are supposed to be just a bunch of Write() calls into the response stream. this writing-to-the-stream thing will still happen even if the rendered view is cached, as it's needed to be, er, written.
    I think that there's no performance gain here. If anything, the memory overhead will only cause more damage than good.
    and if one finds out that view renderings start to get expensive, then perhaps there's too much responsibility in the wrong place

  9. Avatar for Gergely Orosz
    Gergely Orosz November 5th, 2008

    @Ken: I agree with most of what you say. Just one addon: I often see developers not separating their BL logic and mixing it with the code (take inline coding as the best example). I feel that this approach is yet another small hack which can be helpful in a _lot_ of scenarios but I would prefer designing the program in a way that there's no need in using this feature.

  10. Avatar for tasarım
    tasarım November 5th, 2008

    This is really a cool hack. I hope it makes in v1 of MVC, cause it could be very useful for some things i am working on.

  11. Avatar for Dan Atkinson
    Dan Atkinson November 5th, 2008

    This would be extremely useful, if only sub-controllers were part of the core (not just community projects like MVCContrib), which would then allow me to cache multiple actions rendered inside the master page.

  12. Avatar for Scott Watermasysk
    Scott Watermasysk November 5th, 2008

    I am with Ken and Gergely on this one. Just because you can do it, doesn't mean you should.
    Output caching is one of those things that demo's really well and completely falls apart when you really need.
    In addition to mixing your BL into your views you also in many cases now need to work with that logic in your back end so that you can clear the cache something changes some where else in the site.
    -Scott

  13. Avatar for Chris
    Chris November 5th, 2008

    +1 in v1
    just wanted to note, our main subdomain, is totally an mvc app.

  14. Avatar for Kyorry Wang
    Kyorry Wang November 6th, 2008

    hi, Phil
    how about to present ViewUserControl?
    i test the Html.RenderParial(partialName), to render user control that i had set the cache like this <%@ OutputCache Duration="60" VaryByParam="none" %>, but it does not work, why?

  15. Avatar for Torkel &#214;degaard
    Torkel &#214;degaard November 6th, 2008

    I am also missing how this solves the real problem. This only caches the view generation, which is in most cases pretty cheap, what one wants to cache is part of the view & including the controller action logic that is responsible for the viewdata for that part of the view.
    I think the real solution for donate caching in an MVC app is subcontrollers and actions, where the complete action and rendering of that sub-action can be cached.

  16. Avatar for brad dunbar
    brad dunbar November 6th, 2008

    While I can't comment on whether this solves the issue, I will say this is a great response to real world feedback. I've heard the stackoverflow team talk about this constantly and I think its amazing that you're taking steps to fix it so quickly.
    This is a glimpse of a new, transparent Microsoft that I would like to see more of. Great work Phil, keep it up.

  17. Avatar for Torkel
    Torkel November 6th, 2008

    Maybe to clarify, I think that this solution can be very useful and it would be great if it makes it into v1. Great work Phil :) But as I said it only caches the view generation, which in a WebForms app makes sense because the view generation and logic & data fetches is intermingled, but in a mvc app it makes less sense.

  18. Avatar for Jon Davis
    Jon Davis November 12th, 2008

    Ironically, I think I could lose wait by eating these donuts...

  19. Avatar for DanielChow
    DanielChow November 13th, 2008

    Great job,Phil! It's Every useful!

  20. Avatar for labilbe
    labilbe November 13th, 2008

    How to substitute something like an Html.ActionLink?
    Maybe something like should be helpful
    using (Html.Substiture()) {
    Html.ActionLink...;
    }

  21. Avatar for roni
    roni November 30th, 2008

    why not simple take out the cache from your application and put it into an external application like my sharedcache? Finally MS understood in version 4.0 and velocity project - caching should not be within the same app.

  22. Avatar for BorisCallens
    BorisCallens December 23rd, 2008

    I must be missing something.
    I have an action method with the same [OutputCache(Duration = 60, VaryByParam = "None")] attribute.
    Then in my master there is a "Welcome[Username]" string that obviously should vary if a new user logged in, so I put <%= Html.Substitute(c => String.Format(ViewData.Model.T9nProvider.TranslateById("CommonWeb.Welcome"), Context.User.Identity.Name)) %>
    And then put a breakpoint there.
    First time everything runs smoothly. Second time round, it never even hits the method (because it is fetched from cache I presume.
    What am I missing?

  23. Avatar for gene
    gene March 25th, 2009

    Did this make it into 1.0?

  24. Avatar for Harry
    Harry March 31st, 2009

    Nope! this did not make it into version 1 :(

  25. Avatar for Ann Onymouse
    Ann Onymouse December 25th, 2009

    This did not make it into version 2 either: "Due to changes in the rendering behavior of MVC view engines, the Html.Substitute helper method does not work and has been removed" (from Futures).
    What's up? Is Donut Caching now officially a don't-go-there in MVC?

  26. Avatar for headache
    headache January 19th, 2010

    How would you substitute an partial view ?
    Most of the times obtaining that Html is not that easy as calling DateTime.Now so it would be nice to do something like
    <%= Html.SubstituteAction("SomeAction", "SomeController") %>
    Right now there is no easy/fun way to render a view on the controller:
    stackoverflow.com/.../render-a-view-as-a-string

  27. Avatar for Richard
    Richard February 4th, 2010

    This doesn't work with ASP.NET MVC 2 RC 2, does it?

  28. Avatar for Eric
    Eric March 4th, 2010

    UNFORTUNATELY, this only works for strings. You can't get this to work if you are using RenderAction or RenderPartial because they return void.
    Silly that nobody has come up with an easier way to exclude these methods from cache.

  29. Avatar for headache
    headache March 21st, 2010

    Now (in version 2.0) we have Html.Action so we can obtain the Html. Yeeii
    But Response.WriteSubstitution doesn't work anymore. Aahhhhh

  30. Avatar for Pascal Lacroix
    Pascal Lacroix April 27th, 2010

    Dear Phil,
    Is there any way to get this working with ASP.NET MVC 2?
    Some ideas?

  31. Avatar for Fabio Milheiro
    Fabio Milheiro May 2nd, 2010

    Yes, I have been all day try to get this to work with ASP.NET MVC 2!!! HELP!

  32. Avatar for Chicago mover
    Chicago mover August 11th, 2010

    I too have the same problem and is there any solution to get this working with ASP.NET MVC 2?

  33. Avatar for imperialx
    imperialx October 22nd, 2010

    Is this issue been resolved Phil? Thanks.

  34. Avatar for JanJ
    JanJ August 28th, 2011

    Donut Caching is now available for ASP.NET MVC 2 and 3 in 'Moth' (available on NuGet). See the wiki pages: Donut caching.