Razor Donut Caching

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

Donut caching, the ability to cache an entire page except for a small region of the page (or set of regions) has been conspicuously absent from ASP.NET MVC since version 2.

MMMM Donuts! Photo by Pzado at sxc.hu

This is something that’s on our Roadmap for ASP.NET MVC 4, but we have yet to flesh out the design. In the meanwhile, there’s a new NuGet package written by Paul Hiles that brings donut caching to ASP.NET MVC 3. I haven’t tried it myself yet, so be forewarned, but judging by the blog post, Paul has done some extensive research into how output caching works.

One issue with his approach is that to create “donut holes”, you need to call an action from within your view. That works for ASP.NET MVC, but not for ASP.NET Web Pages. What if you simply want to carve out a region in your view that isn’t cached?

Well to implement such a thing requires that we make changes to Razor pages itself to support substitution caching. I’ve been tasked with the design of this, but I’ve been so busy that I’ve fallen behind. So I’m going to sketch some thoughts here and get your input, and then turn in your work as if I had done it. Ha!

Ideally, Razor should have first class support for carving out donut holes. Perhaps something like:


<h1>This entire view is cached</h1>
@nocache {
  <div>But this part is not. @DateTime.Now</div>
}

As this seems to be the most common scenario for donut holes, I like the simplicity of this approach. However, there may be times when you do want the hole cached, but at a different interval than the rest of the page.


<h1>The entire view is cached for a day</h1>
@cache(TimeSpan.FromSeconds(10)) {
  <div>But this part is cached for 10 seconds. @DateTime.Now</div>
}

If we have the second cache directive, we probably don’t really need the nocache directive as its redundant. But since I think it’s the most common scenario, I’d want to keep it anyways.

The final question is whether these should be actual Razor directives or simply methods. I haven’t dug into Razor enough to know the answer, but my gut feel is that it would require changes to Razor itself to support it and can’t be added on as method calls as method calls run too late.

What do you think of this approach?

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

Comments

avatar

28 responses

  1. Avatar for Matthew Abbott
    Matthew Abbott November 26th, 2011

    This can likely be implemented using a custom Span and visitor that you implement by decorating the code parser. That would allow you to modify the generated code before it is compiled. I guess the next step is update the viewpage infrastructure to add the specific mechanism for handling the caching. This should work without change to the Razor parser as it already supports custom spans.

  2. Avatar for Brian Vallelunga
    Brian Vallelunga November 26th, 2011

    This looks very nice. Would we see support for any sort of vary by param/custom caching in the hole? I'm imagining a scenario where you might want the hole to contain cached default content for anonymous users and custom data for authenticated users.

  3. Avatar for Diganta Kumar
    Diganta Kumar November 26th, 2011

    Both @nocache and @cache will be great to have it in razor. Do we still have to set manifest file?

  4. Avatar for Rick
    Rick November 26th, 2011

    This would be a boon! I would request that it support outputcache profiles in the web.config as well.

  5. Avatar for haacked
    haacked November 26th, 2011

    @Brian, I would imagine that the @cache directive would support all the cache parameters you'd expect such as the timespan and vary by.

  6. Avatar for Fujiy
    Fujiy November 26th, 2011

    The possibility to cache internals parts with different intervals is great!
    Do OutputCacheAttribute still work together? Will be unified as Paul Hiles did?
    Thanks!

  7. Avatar for Ignat Andrei
    Ignat Andrei November 26th, 2011

    I like
    @cache(TimeSpan.FromSeconds(10))
    Then we can define
    @nocache = @cache(TimeSpan.FromMilliSeconds(TimeSpan.MinValue.TotalMilliseconds+1))
    And will be just accurate ...

  8. Avatar for bitesized
    bitesized November 26th, 2011

    My point exactly.. But why the nocache alternative? I want clean uncomplicated consistent interfaces.. If you wanna marry you can have "nocache", otherwise I'll only support your cache (TimeSpan)..

  9. Avatar for Alexander Beletsky
    Alexander Beletsky November 26th, 2011

    I vote both @cache and @nocache, case they exactly express what you want to do. In my opinion both are Razor expressions rather that function calls.
    Overall idea of donut caching sounds very fine to me )

  10. Avatar for Matt
    Matt November 27th, 2011

    I like @nocache, and reckon @cachefor should be the other option. It's a little more obvious what it's used for, without being substantially longer.
    Is there the option to support 'caching until method X tells you to bust the cache'? For example, having a code block that is cached until a file is updated, or some other custom method that busts a cache that is more complicated than just a time span. @cacheuntil could be a good name for that - although I do prefer to keep then together rather than having different names to use for built-in vs. self-made functionality.

  11. Avatar for Malcolm Sheridan
    Malcolm Sheridan November 27th, 2011

    I like the @cache directive.
    Keep it part of Razor and not a method as it sits on the page. Also keep it simple. Default the timeout to seconds or milliseconds so you can remove the need to add TimeSpan. So like this:
    @cache(10) {
    }
    Less is more.

  12. Avatar for jbogard
    jbogard November 27th, 2011

    Not sure if I like this approach - this would pretty much be the nail in the coffin that Razor is only used in the context of a web request, as opposed to an awesome stand-alone templating engine. I didn't think I would care about that, but Spark seems to keep popping up for us in the craziest of places - like WinForms apps, for example, to format HTML reports for users.
    Unless this was an extension of Razor, instead of baked in, that is.

  13. Avatar for Sean
    Sean November 27th, 2011

    Instead of @nocache, would it not make more sense to add an overload for @cache so you could just express it as @cache(false)?

  14. Avatar for Yann
    Yann November 27th, 2011

    Love the fact that there is a nocache directive, it's very explicit.
    And I agree with Malcolm Sheridan, using the timespan makes it "complicated", the default should be seconds.

  15. Avatar for Quent
    Quent November 27th, 2011

    I assume cache directives can be embedded within each other? This would essentially define a tree of independent parts to be assembled at query time.
    Maybe make the caching definitions be part of the Partial View system? Partial Views are a good way (as are Sections) to define page assembly parts.
    That would also solve a lot of problems about what to do with the Model and ViewBag within the page. Does this get cached also? Personally, I think those decisions need to be pushed off to the developer.

  16. Avatar for Jarrett Meyer
    Jarrett Meyer November 27th, 2011

    Well, Jimmy beat me to it. Razor is a templating engine. Using Razor for outbound HTML-formatted emails is awesome. But adding a cache/nocache Razor-level directive doesn't really make sense in this case.
    You could just as easily create an extension such as @using (Html.Cache.NoCache) { } or @using (Html.Cache.FromSeconds(15) { } to accomplish what you want.

  17. Avatar for Jesse
    Jesse November 27th, 2011

    Two points on this. 1) having both @nocache and @cache seems like the best choice. They are very explicit and, while you can achieve the same nocache results using cache, it would be simpler to just say @nocache. As you said, that's the most likely scenario. 2) I'm with Jimmy and Jarrett. Razor is about templating. Baking cache directives into it means it's now concerned with more than just generating HTML. Jarrett's suggested extensions are pretty much spot on.

  18. Avatar for Malisa Ncube
    Malisa Ncube November 27th, 2011

    A good cache should allow the developer to investigate the hits and misses so parameters can be adjusted accordingly.
    I think a better design would be to simply have extension methods that can do the caching. This can enable extensibility too. I may want to use the Windows AppFabric for my cache. The freedom to override some of the behavior would be easier if Razor view engine remains untouched.

  19. Avatar for haacked
    haacked November 27th, 2011

    Hey all, remember that the discussion is about a syntax for ASP.NET Output Caching declarable within Razor. So things like using AppFabric for the cache would be covered by existing extensibility points for output caching.
    I'll have to check with Andrew Nurse to see if a directive can be "layered" on so that the core Razor doesn't have it, but Razor for Web does.

  20. Avatar for Paul Litwin
    Paul Litwin November 28th, 2011

    What does this have to do with Razor? This should be view language independent. Everything IMHO also needs to be supported in ASPX view engine.

  21. Avatar for Ivan Franjic
    Ivan Franjic December 2nd, 2011

    I've been using Razor outside ASP.NET a lot lately and wouldn't like to see it become a specialized templating engine. So as others suggested, it would be better if this was added as an extension of Razor.

  22. Avatar for Sipke Schoorstra
    Sipke Schoorstra December 3rd, 2011

    I like the clean and short @cache and @nocache directives, but it's important to prevent the templating engine from becoming a specialized web page view engine.
    But I guess that can easily be achieved by having an abstract CacheProvider that Razor depends on, with having for example both an in memory provider to support the cases where we use Razor outside of ASP.NET, as well as an ASP.NET implementation of the cache provider.
    I agree with Paul Litwin that it should also be supported by the WebForms View Engine, and other View Engines as well. So perhaps the implementation of @cache and @nocache should be aliases to some base methods that all view engines inherit from (and all view engines should have the ability to override the default implementation, if so required). Just like the C# int and the VB.NET Integer are in fact aliases to System.Int32.
    It's been long since I've seen WebForms syntaxtm but it could be something like: <asp:nocache runat="server">Username</</asp:nocache>, or a directive: <%nocache %>Username</%>
    Other view engines could define their own tags / directives.
    Ultimately, the directives are aliases to protected methods of their base view engine.
    Although some argue that caching is only relevant within the context of the web, the web is probably the biggest context in which view engines are used. And with the Cache being an extensibilty point, having a @cache directive in views outside the context of ASP.NET, should do no harm.

  23. Avatar for Konstantin Tarkus
    Konstantin Tarkus December 7th, 2011

    Having "named" cache policies defined in web.config would also be useful.
    @cache("SomeCachePolicy") {

    This part is cached for @cahe.Duration seconds. @DateTime.Now


    }

  24. Avatar for Jan
    Jan April 23rd, 2012

    It works in IE and Chrome but it doesn't work, for me, in Firefox 11.0 and Safari. Is anyone else experiencing the same issue? thanks.

  25. Avatar for felickz
    felickz August 5th, 2012

    So this did not make the MVC4 release?
    ofps.oreilly.com/.../ch_Caching.html
    The MvcDonutCaching NuGet Package. The above example shows a simplified version of Donut Caching and is not well suited to more advanced scenarios. While out-of-box Donut caching is still not available with ASP.NET MVC 4, the MvcDonutCaching NuGet package can help you implement more advanced scenarios with ease.
    This package adds several extensions to existing HTML helper methods as well as adding a custom DonutOutputCacheAttribute that can be placed on any action that needs Donut caching.

  26. Avatar for Aaron
    Aaron August 22nd, 2012

    With the proposed design, let's say I have a portion of the page from a @Html.Action("TodaysDiscountDesert") that returns a PartialViewResult to display today's special desert. Every view I reference it in I surround it with @cache. This seems to imply that the @cache { @Html.Action("TodaysDiscountDesert") } on the Home page and on the About page are two different caches(unless Razor does some analysis to determine that the two cache blocks in separate pages are identical contents). I also would be duplicating the cache duration setting for that particular item across many pages. If I want a VaryByParam on the action, how is the cache block aware of that param being passed to the child action, so that the child action knows to perform a substitution?
    If possible, rather than having @cache, I'd rather the Razor engine look at the OutputCache attribute of any Action calls, and assume none with that attribute are cached with the parent page, and instead perform a substitution on each request of the cached(or not) parent page. The process of evaluating that Html.Action call would go through the same check for a cache of that Action based on it's OutputCache configuration. This ensures the caching configuration goes with the child action method, and rather than duplicating that configuration across all the pages that reference that child action. It makes more sense to me this way also because when I consider caching for an action, I am thinking in terms of what that action is doing, and I want any coder who changes that action method to be aware that it is cached, so that they can re-evaluate if the changes the made to the code would warrant changing how caching is configured for that action. I don't want to have to search through all the Views that call upon that child action and change the cache configuration in each case. They would all be identical because the Child Action's behavior and how often data becomes stale is the same in all those contexts.
    This is how a page which is not cached already works. RenderAction will retrieve a cached result if that Action has an OutputCache attribute. You just need to bring this behavior in alignment for a parent page that is cached, but making it not include any Action calls in its cache, and waiting to make a substution for the child actions whenever the parent page is being retrieved from cache. In which case the retrieval of the child actions is done using the same logic that would be used if the parent page is not even cached. This would avoid the gotchas/surprise when you find that your partials retrieved form child actions suddenly don't refresh when they are expected to because you configured caching for a parent page.

  27. Avatar for Aaron
    Aaron August 22nd, 2012

    Tthe challenge with my design proposal implies that the cached parent page might be passing Model parameters to the child ActionMethod. So whenever a cache is generated for the parent page, in addition to preserving information about the @Html.Action call, you would also need to evaluate all parameters to literals. This will avoid having to also cache the @Model or any viewbag values being passed to the Action. So @Html.Action("Blah", new { id = @Model.Id }) becomes @Html.Action("Blah", new { id = 5 }) in the cached parent page. Consider how this would solve the existing problem of displaying the username of a user in the Layout page but caching the FAQ. User A visits the FAQ question ID=1, the FAQ is configured to vary by param id. A cache for parent page FAQ is generated for id=1. The call to @Html.Action("QuestionText", new { id = Model.Id }) becomes @Html.Action("QuestionText", new { id = 1 }). The QuestionText child action also has OutputCache configured for varyby param, which is coincidental, because when I wrote that action, I know that the result of the action varies based on what question is being retrieved. I don't really think about the parent page when deciding the caching config of child actions. I do have to think about the caching config of parent pages though, because if I configured the FAQ page only on duration, and not ID, then the first request for ID=1 will be retrieved for ID=2, and the literal "id=1" will be passed to the action method instead of 2. No on the other hand, the @Html.Action("WelcomeUserName") is completely independent of its parent view, because it takes no parameters, and thus even if the parent view is cached, it will be called and eveluated based on its own output cache, which would probably not be cached or have a VaryByCustom based on retrieving the current logged in user.

  28. Avatar for Jason
    Jason February 22nd, 2013

    I would say both are needed so you can cause the whole page to cache except a certain section.  and the default is nocache on all