Tip/Trick: Implement "Donut Caching" with the ASP.NET 2.0 Output Cache Substitution Feature

Some Background:

One of the most powerful, yet too often under-used, feature areas of ASP.NET is its rich caching infrastructure.  ASP.NET's caching features enable you to avoid repeating work on the server for each new request received from clients.  Instead, you can generate either html content or data structures once, and then cache/store the results within ASP.NET on the server and re-use them for later web requests.  This can dramatically improve performance for your applications, and lower the load on critical backend resources like databases.

Steve Smith wrote a good ASP.NET 1.1 caching article on MSDN a few years ago that covers some of the basics of the ASP.NET 1.1 caching features and provides a good summary of how to use them.  If you haven't used ASP.NET caching before, I'd recommend checking it out and giving each feature a try.  I'd also highly recommend watching this 15 minute ASP.NET Caching "How Do I" video in the free ASP.NET 2.0 video series to see a live walkthrough of ASP.NET caching in action. 

ASP.NET 2.0 has added two very important improvements to the caching feature set that make it even better:

1) SQL Cache Invalidation Support - This enables you to automatically invalidate/re-generate a cached page or data structure when a database table or row it depends on is updated.  For example, you can now output cache all of your product listing pages within an e-commerce site - and make sure that anytime that their prices change in the database the pages are immediately re-generated on the next request (and do not show stale pricing data to users). 

2) Output Cache Substitution - This nifty feature enables you to implement what I sometimes call "donut caching" -- where you output cache everything on a page except for a few dynamic regions that are contained within cached regions.  This enables you to implement full page output caching more aggressively, and not have to split your pages into multiple .ascx user control files to order to implement partial page caching.  The below tip/trick tutorial explains the motivation and implementation of this feature better.

Real-World Scenario:

You want to implement a product listing page within your site that lists all products within a given product category.  You want to output cache this page so that you don't have to hit the database on each request.  You can easily accomplish this by declaratively adding an <%@ OutputCache %> directive to the top of a Products.aspx page that contains an <asp:datalist> control which is databound to product data returned from your middle-tier.

Note below how the page is configured to output cache its contents for 100,000 seconds or until the northwind's products table is updated with new pricing data (in which case it will immediately regenerate the page on the next request).  The OutputCache directive also has a "VaryByParam" attribute that tells ASP.NET to store a separate cached version of the page for each unique categoryID (for example: a separate page for Products.aspx?categoryId=1, Products.aspx?categoryId=2, etc). 

Products.aspx:

<%@ Page Language="VB" MasterPageFile="~/Site.master" AutoEventWireup="false" CodeFile="Products.aspx.vb" Inherits="Products" %>
<%@ OutputCache Duration="100000" VaryByParam="CategoryID" SqlDependency="northwind:products" %>

<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">

<div class="catalogue">

    
<asp:DataList ID="DataList1" RepeatColumns="2" runat="server">
        
<ItemTemplate>
        
            
<div class="productimage">
                
<img src="images/productimage.gif" />
            </
div>
        
            
<div class="productdetails">
            
                
<div class="ProductListHead">
                    
<%#Eval("ProductName")%>
                
</div>
                
                
<span class="ProductListItem">
                    
<strong>Price:</strong>
                    
<%# Eval("UnitPrice", "{0:c}") %>
                
</span>
                
            
</div>
        
        
</ItemTemplate>
    
</asp:DataList>
    
    
<h3>Generated @ <%=Now.ToLongTimeString()%></h3>

</div>

</asp:Content>

Products.aspx.vb:

Partial Class Products
    
Inherits System.Web.UI.Page

    
Sub Page_Load(ByVal sender As ObjectByVal As System.EventArgs) Handles Me.Load

        
Dim products As New NorthwindTableAdapters.ProductsTableAdapter

        DataList1.DataSource 
products.GetProductsByCategoryID(Request.QueryString("categoryId"))
        DataList1.DataBind()

    
End Sub

End Class

When accessed by a browser, the below page is returned from the server:

Note that the timestamp at the bottom of the page will only be updated every 100,000 seconds or if the pricing data in the products table has been updated.  It will be cached for all the other HTTP requests - allowing us to process 1000s of requests per second on a production server and avoid ever having to hit the database (making things super fast).

Problem:

The one problem we are going to encounter in the above example is with the welcome message and username we output at the top-right of the page (circled in red above).  This is currently being generated within our Site.Master master-page file using the new ASP.NET 2.0 <asp:loginname> control like so:

<div class="header">
    
<h1>Caching Samples</h1>
            
    
<div class="loginstatus">
        
<asp:LoginName FormatString="Welcome {0}!" runat="server" />
    </
div>        
</div>

The problem we are going to run into is that because we've added full-page output caching to our page, the username of the first user to hit the site is going to be saved in the cached output from the page - which means that by default the users who hit the site in the 100,000 seconds after that initial request are going to receive back an incorrect welcome message (and worse - an incorrect name!).

Solution:

There are two ways to solve this problem. 

The first solution would be to have the overall page be dynamic (so remove the top-level <%@ OutputCache %> directive), and refactor the page contents so that all of the "cacheable" content is encapsulated within ASP.NET User Controls (which are implemented in .ascx files).  You'd then add <%@ OutputCache %> directives at the top of each of these .ascx user control files to make them separately cacheable.  This avoids you having to hit the database on each request, and ensures that the username is always correctly output (since it is not within a cached user control region).  This approach works today with ASP.NET 1.1 and of course can still be done with ASP.NET 2.0.

The downside with this first solution, though, is that it requires us to refactor our code and layout within the page in order to make caching work.  If we have only a few places within the page that we want to keep dynamic, this refactoring can be really inconvenient.  The good news is that ASP.NET 2.0 has added support for Output Cache Substitution block support that provide a much cleaner way to handle this scenario.

Output Cache Substitution Blocks using the <asp:substitution> control:

Output Cache Substitution blocks enable you to OutputCache an entire page's output -- while leaving a few dynamic region markers to indicate places in the HTML output where you want to dynamically "fill-in" content on later requests (for example: the username message in our sample above).  I sometimes call this the "donut caching feature" - since the outer content of a page is all cached, with only a few holes in the middle of the content stream that are dynamic.  This is the exact opposite of using user controls with partial page caching - since in the partial page caching case the overall page is dynamic, with cached regions in the middle. 

You implement output cache substitution by output caching a page using full page output caching (exactly the same syntax as the Products.aspx code sample above).  You can then indicate regions of the page that you want to dynamically fill-in using substitution blocks by adding <asp:substitution> controls to the page like so:

<div class="header">
    
<h1>Caching Samples</h1>
    
    
<div class="loginstatus">
        
<asp:Substitution ID="Substitution1" runat="server" MethodName="LoginText" />
    </
div>
</div>

The <asp:Substitution> control is unlike any other control in ASP.NET.  It registers a callback event with the ASP.NET output-cache that will cause a static method on your page or masterpage to be invoked when the page content is served out on subsequent requests from the ASP.NET Output Cache.  This static method will be passed an HttpContext object at runtime that contains the standard ASP.NET Request, Response, User, Server, Session, Application intrinsics, and which you can then use to return a string that ASP.NET will automatically inject into that region of the page before the content is sent back to the client. 

For example, to handle the scenario above where we want to dynamically output a welcome message into the output-cached products.aspx page we'd simply add this method to our Site.Master code-behind file and have it be invoked by the <asp:substitution> control above:

Partial Class Site
    
Inherits System.Web.UI.MasterPage

    
Shared Function LoginText(ByVal Context As HttpContext) As String
        Return 
"Hello " & Context.User.Identity.Name
    
End Function

End Class

Now the entire page will be output cached, except for the contents of the <asp:substitution> control representing the welcome message on the top-right of our page. 

We could obviously extend this further if we wanted to include additional personalized information like how many items the user had within their shopping cart, etc.  The cool thing is that all other content on the page remains fully cached - and we never have to hit the products database in order to generate it on cached requests (meaning we can process thousands of product pages a second on a single server).  In fact, no controls on the page are created during the request, and no code other than the static method above ever runs on later requests - making everything super fast. 

Output Cache Substitution Blocks using the Response.WriteSubstitution method:

In addition to using <asp:substitution> controls to indicate replaceable substitution blocks on a page, you can alternatively use the Response.WriteSubstitution method instead.  This method takes as a parameter a delegate object to a HttpResponseSubstitutionCallback method that you can implement on any class within your application (it is not limited to only going against static methods on your code-behind class). 

The <asp:substitution> control internally uses this method to wire-up delegates in the code-behind classes of pages.  You can likewise use it within your own controls or pages for maximum control and flexibility.

Conclusion:

I have yet to find a single ASP.NET application that could not benefit from using the ASP.NET caching features.  Because ASP.NET supports full page output caching, partial page output caching, and now donut-level caching - and allows you to vary the cached content based on any parameter or custom logic you want, and now allows you to automatically invalidate/re-generate the cached contents when a database changes, you shouldn't find yourself ever building an application that can't use caching to at least some degree. 

I definitely recommend spending time checking all of the ASP.NET caching features out.  To find some more caching samples I've done, please download my Tips/Tricks talk from the recent ASP.NET Connections event.  Within that presentation I include slides+samples that show how to use full-page caching, partial page caching, substitution block caching, and SQL Cache Invalidation.

For additional ASP.NET Tips/Tricks blog posts of mine, please review my ASP.NET Tips, Tricks and Resources page.

Hope this helps,

Scott

24 Comments

  • The substitution control is brilliant. Not because it allows you to do things that were otherwise impossible before, but because it makes it so much easier and more natural to use in a lot of cases.

  • So maybe a good idea would be to active Cache on the MasterPage(s) and using Donut Caching on the content placeholders.

    Of course, this would be best used on a MasterPage with a lot of navigation controls and other functions.

    Right?

  • Sorry I was writing the message and I click enter! I was trying to ask, can you define donut caching? Looks like you explain donut caching, but I thought that's just called caching? I am a little confused, please do define donut caching!

    Cheers
    Al

  • When use caching, what percentage of the iis server memory would you recommend to allocate? Thanks!

  • Scott, I'm trying to solve the same issue in the example with the logged in user's name being displayed. I'm currently using the asp:substitution control, but it doesn't play nicely with ASP.NET AJAX Beta 2. I assume this is from the control being different than all the other controls.

    Specifically, when an UpdatePanel is updated and sends information to the client, it appears the string from the static function the asp:substitution control calls is injected into the XML stream, causing all kinds of disaster at the client side.

    Is there a way around this, or is refactoring with user controls the only way around?

  • One problem I've run into with the cache substitution is that the method called needs to be static. If you have page specific state that you need to get at to determine the 'donut hole filling' this sometimes doesn't work and potentially requires a bit of forethought of passing values on querystrings or in the POST data to get at the state.

  • Hi Matt,

    Any chance you could send me an email (scottgu@microsoft.com) with a small sample that shows the problem you are having with using this technique combined with Atlas? I'd love to investigate this more.

    Thanks,

    Scott

  • Hi Ray,

    In general for performance the key thing you want to avoid is ever having the web-server page memory out aggresively. Once you get into a heavy swapping situation, your performance will really degrade.

    I'd recommend trying to keep the IIS worker process memory usage to no more than 60% of the physical memory on the system. This then keeps the rest for the OS and/or any other services on the box to run fine.

    Hope this helps,

    Scott

  • Yes , I have tried caching at many sites I was working at and also My personal site and could see the difference in the page loads with the caching of pages and data

  • Hi Scott, great post, as always.
    To me it looks like with substitution caching you're simply defining another way of implementing AJAX-like functionality, since callbacks are being used to insert updated information before the complete response (cached and updated) is returned. So which one would you say is the better solution?

  • I plan on using caching for my site currently in development, but it's tricky when also using dynamic menus. I'm using menu security trimming as well as logged-in/loged-out trimming (in VB code-behind). How can I exclude the whole menu control from caching - it'll take more than text substitution.

  • Thanks Scott,

    donut caching seems useful. However, since it returns a string, is it possible to render user controls programmatically?

    The kind of donut caching I've always wished ASP.Net had is to have page caching while being able to have some user controls specifically not cached. Maybe in the next version?

  • Hi Pieter,

    Caching and AJAX are actually quite different.

    With Caching you are actually saving the content on the server and not re-generating it.

    With AJAX you are transferring regions of content or data back to the client (but the server still re-generates the content).

    Hope this helps,

    Scott

  • Hi Jerome,

    The static method with an control does just return a string.

    If you want to use conttrols and/or a .ascx template to help generate the content returned by the string, you can use the "ViewManager" class I built as part of this blog post here: http://weblogs.asp.net/scottgu/archive/2006/10/22/Tip_2F00_Trick_3A00_-Cool-UI-Templating-Technique-to-use-with-ASP.NET-AJAX-for-non_2D00_UpdatePanel-scenarios.aspx

    This provides a nicer way to generate complex content.

    Hope this helps,

    Scott

  • Thanks Scott!

    Yes, using your ViewManager with donut caching is an elegant method.

  • OK you're right - so this means clever use of Caching could well outperform using AJAX Extensions?

  • May be I don't get something but WHY they didn't do mechanisms for:
    1) wrap controls on the page with e.g. ... so I don't have to move controls in separate file.
    2) the same for instead of just simple string return in
    ?

  • Very cool, thanks for the concise and clear explanation. However, I wonder if you should consider calling it "Swiss Cheese Caching" since, like a slice of swiss cheese and unlike a donut, it may have 0 to n holes. ;-)

  • Hello Scott,
    Very useful article, but I've noticed that you mentioned that when using cache substitution feature, passed HttpContext object will have Session in it. But it doesn't seem so. When I use substitution control the Session property within HttpContext is always null. Is there a workaround for this?

    Thank you.

  • Hi Egor598,

    That is a very good question. Can you send me an email with it? I will then loop someone in who might know (I'm not 100% sure myself).

    Thanks,

    Scott

  • Hi Scott,
    Thanks for a very useful article. I wanted to ask you some questions which are somehow related to this article. In our application we are also displaying the products in a datalist based on the category. But instead of having category in the querystring we have added a dropdownlist containing the list of all categories. Selecting a particular category posts back the page and gets the specific products.
    So in that case how can I set the output cache parameters? I have tried with VaryByControl but it isn’t working.
    Second question the products are displayed with respect to the user rights. So every user has specific rights associated with his direction and profile. In that case if the page will be cached for a specific category won’t it have the same products for all the users irrespective of their rights??
    Thanking you in anticipation.

    Khurram, Paris

  • I used your ViewManager method so that I could load a usercontrol with Substitution, and this works fine the first time the page loads but when I refresh, so that the cached page is displayed I get a 'System.NullReferenceException: Object reference not set to an instance of an object' at the point where the control is loaded. any ideas what I am doing wrong?

  • When I try and get the session id for the Substitution control on page load using the methof below, all is fine.

    Shared Function getSessionID(ByVal Context As HttpContext) As String
    Return HttpContext.Current.Session.SessionID.ToString
    End Function

    but after I refresh the page to display the cached version I get the error message:

    System.NullReferenceException: Object reference not set to an instance of an object. at AppMaster.getSessionID(HttpContext Context)

    How can I retrieve the users session id?

  • Hi Scott,

    I think the error you are seeing is because you are trying to access the session object.

    Unfortunately I don't think you can access the session in this scenario - since the session object is never populate (since the page is never created).

    Sorry!

    Scott

Comments have been disabled for this content.