For pages whose content is relatively static, it is inefficient to regenerate the page for every client request. Instead, pages can be generated once and then cached for subsequent fetches. The OutputCache directive can be added to any ASP.NET page, specifying the duration (in seconds) that the page should be cached. The code shown in Listing 9-1 is an example of using the OutputCache directive to specify that this particular page should be cached for one hour after its first access. In this example, the page prints the date on which it was generated, so if you try accessing this page, you will notice that after the first hit, all subsequent accesses will have the same timestamp until the duration is reached. Listing 9-1 OutputCache Directive Example<%@ Page Language="C#" %> <%@ OutputCache Duration='3600' VaryByParam='none' %> <html> <script runat="server"> protected void Page_Load(Object sender, EventArgs e) { _msg.Text = DateTime.Now.ToString(); } </script> <body> <h3>Output Cache example</h3> <p>Last generated on: <asp:label id="_msg" runat="server"/></p> </body> </html> When using the OutputCache directive, you must specify at least the Duration and VaryByParam attributes. Leaving the VaryByParam attribute set to 'none' , as shown in Listing 9-1, means that one copy of the page will be cached for each request type ( GET , HEAD , or POST ). Subsequent requests of the same type will be served a cached response for that type of request. Table 9-1 shows the complete set of attributes available with the OutputCache directive. Table 9-1. OutputCache Directive Attributes
The attributes that you specify in an OutputCache directive are used to populate an instance of the System.Web.HttpCachePolicy class by calling the System.Web.UI.Page.InitOutputCache() method. This class is accessible programmatically through the Response property of the Page (or Context ) class, as shown in Listing 9-2. Listing 9-2 HttpCachePolicy Classpublic sealed class HttpCachePolicy { public HttpCacheVaryByHeaders VaryByHeaders {get;} public HttpCacheVaryByParams VaryByParams {get;} public void AppendCacheExtension(string extension); public void SetCacheability(HttpCacheability cacheability); public void SetExpires(DateTime date); public void SetLastModified(DateTime date); public void SetMaxAge(TimeSpan delta); public void SetNoServerCaching(); public void SetSlidingExpiration(bool slide); //... } public sealed class HttpResponse { public HttpCachePolicy Cache {get;} //... } public class Page : ... { public HttpResponse Response {get;} //... } The OutputCache directive gives you access to a subset of the functionality available in the HttpCachePolicy class. One useful feature that is only accessible programmatically is the ability to set a sliding expiration on a page. That is, whenever a page is hit, the timeout is reset. This is a useful way to ensure that only items that are being used are kept in your cache. Pages that are cached once and then never accessed again are a waste of resources. The code in Listing 9-3 shows an example of a page whose expiration is set programmatically and uses the sliding expiration scheme. Listing 9-3 Programmatically Setting Page Caching<%@ Page Language="C#" %> <html> <script runat="server"> void Page_Load(Object sender, EventArgs e) { Response.Cache.SetExpires(DateTime.Now.AddSeconds(360)); Response.Cache.SetCacheability(HttpCacheability.Public); Response.Cache.SetSlidingExpiration(true); _msg.Text = DateTime.Now.ToString(); } </script> <body> <h3>Output Cache example</font></h3> <p>Last generated on: <asp:label id="_msg" runat="server"/> </body> </html> 9.2.1 Output Caching LocationSo far, we have discussed the advantage of output caching on the server, where it saves server processing time by loading the page from a cached rendering stored in the ASP.NET worker process instead of dynamically generating it. In addition to server caching, there are two other opportunities for page caching. First, many browsers can cache pages on the client machine. This is the most efficient method of all because it avoids any network traffic and renders the page directly from the client machine's cache. Web pages indicate that they should be cached in client browsers through the Expires header of their HTTP response, indicating the date and time after which the page should be retrieved from the server again. Second, the HTTP 1.1 protocol supports the caching of responses on transparent proxy servers, sitting between the client and the server. Pages can indicate whether they should be cached on a proxy by using the Cache-Control header. If your page is already output cacheable, it usually makes sense to make that page client and proxy cacheable too. It turns out that the OutputCache directive on a page enables all three types of caching ”server, client, and proxy ”by default. This means that when you mark a page with an OuputCache directive, you are effectively saying that this page will not change for a specific period of time, and if it is possible to cache it anywhere in the pipeline between your ASP.NET application and the client browser, please do so. This is useful because with one statement, you can advertise the cache friendliness of your page, specifying the expiration time only once, and let ASP.NET render your page appropriately to whatever client asks for it. On the other hand, sometimes you might need more precise control over exactly where your page is cached. The Location attribute of the OutputCache directive lets you specify where you want your page to be cached. Table 9-2 shows the values of the Location attribute and how they affect the Cache-Control header, the Expires header, and the server caching of your page. Table 9-2. Effect of the Location Attribute in Output Caching
For example, if you specified a value of 'Client' for the Location attribute of an OutputCache directive on a page, the page would not be saved in the server cache, but the response would include a Cache-Control header value of private and an Expires header with a timestamp set to the time indicated by the Duration attribute, as shown in Listing 9-4. Listing 9-4 Designating Private Caching<%@ OutputCache Duration='120' Location='Client' VaryByParam='none' %> ... - generates the following response HTTP/1.1 200 OK Server: Microsoft-IIS/5.1 Date: Tue, 01 Jan 2002 12:00:00 GMT Cache-Control: private Expires: Tue, 01 Jan 2002 12:02:00 GMT ... 9.2.2 Caching Multiple Versions of a PageUsers can request pages in a Web application in several ways. They can issue a plain GET request, a plain HEAD request, a GET request with an accompanying query string with name/value pairs appended, or a POST request with an accompanying body containing name/value pairs. Caching pages that are retrieved using only a GET request with no query string is straightforward, because the page never changes its contents based on the request (except possibly based on client headers, which we will come back to). Caching pages that are accessed with changing query strings or POST variable values becomes more complex, because a distinct version of the page must be cached for each unique query string or variable combination that is submitted. Before you decide to enable output caching on an ASP.NET page, you must decide how many versions of that page should be cached. The options are to cache only one copy of the page for each request type ( GET , HEAD , or POST ); to cache all GET , HEAD , and POST requests ( implying separate cached versions of the page for each request); or to cache multiple versions of a page only if a particular variable in a GET or POST changes. This option is controlled through the VaryByParam attribute of the OutputCache directive, whose values are shown in Table 9-3. If you set the VaryByParam attribute to ' none ', only one version of the page is stored in the output cache for each request type. If a user issues a GET request to a page with an accompanying query string, the output cache ignores the query string and returns the single cached instance of the page for GET requests. If, on the other hand, you set the VaryByParam attribute to ' * ', a new version of that page is cached for each unique query string and each unique collection of POST variables across all client requests. This setting is potentially very inefficient and must be used carefully . For example, suppose a page that accepted a person's name in a query string were marked with the OutputCache directive and specified a VaryByParam value of ' * '. For each client request with a different name, a new copy of the page would be stored in the output cache. Unless many people with the same name hit that page, there would likely be very few cache hits, and the cached pages would just be wasting server memory. This scenario is depicted in Figure 9-2. Figure 9-2. Caching Multiple Copies of a Page
Table 9-3. VaryByParam Values
The VaryByParam attribute can also be set to the name, or list of names, of query string or POST variables. The decision of whether to create a unique entry in the output cache for a page is then based on whether the particular variable (or variables) listed change from one request to another. It is unlikely this capability would be used very often, because query string and POST variables are typically used when you are deciding how to render a page, or at the very least, to store in some back-end data source when the page is posted. In addition to caching different versions of a page based on the parameters passed by a client request, you can cache different versions of a page for a variety of other reasons. The VaryByHeader attribute of the OutputCache directive caches a different version of a page whenever a header string (or set of header strings, which you can specify) differs from one client to the next . This is important if you render your page differently based on the headers supplied by the client (which happens implicitly with many ASP.NET controls). For example, if you conditionally render portions of your page based on the Accept-Language header passed in by clients, you need to make sure that a separate cache entry is made for each language that clients request. The page in Listing 9-5 prints a message in the client's preferred language (as long as it is French, German, or English). If we applied the OutputCache directive to this page without a VaryByHeader constraint, the first client to request it would see his preferred language, but subsequent clients would see the first client's preferred language until the duration expired . Using the VaryByHeader constraint with Accept-Language as a value causes a distinct rendering of this page to be stored in the output cache for each client request with a unique language preference. Listing 9-5 Using VaryByHeader<! File: LanguagePage.aspx > <%@ Page language='C#' %> <%@ OutputCache Location='any' VaryByParam='none' Duration='120' VaryByHeader='Accept-Language' %> <html> <head> <script runat="server"> protected void Page_Load(Object src, EventArgs e) { if (!IsPostBack) { switch (Request.UserLanguages[0]) { case "fr": _msg.Text = "Bonjour! Comment allez-vous?"; break; case "de": _msg.Text = "Guten Tag! Wie geht's?"; break; default: _msg.Text = "Hello! How are you?"; break; } } Response.Write(DateTime.Now.ToString()); } </script> </head> <body> <form runat=server> <asp:Label id='_msg' runat=server /> </form> </body> </html> Finally, you can cache separate page renderings based on the browser type and version, or any other criteria you need, through the VaryByCustom attribute. If you know that a page may render differently for different browsers, it is important that you store a separate cache instance for each browser type that accesses the page. Setting the VaryByCustom attribute of the OutputCache directive to Browser causes a unique instance of the page to be cached for each browser type and major version number that accesses your page. Note that this is different from using the VaryByHeader option with a value of User-Agent because that would store a unique instance in the cache for each user agent string, which would generate many more entries. It is important to realize that many server-side controls render themselves differently based on the browser type and version, including the Calendar , TreeView , Toolbar , TabStrip , and MultiPage controls, to name a few. If you use any of these controls in a page on which you have enabled output caching, you should be sure to include a VaryByCustom attribute set to 'Browser' , as shown in Listing 9-6. Listing 9-6 Using VaryByCustom Set to 'Browser'<%@ Page Language='C#' %> <%@ OutputCache Location='Any' VaryByParam='none' Duration='120' VaryByCustom='Browser' %> <html> <body> <form runat=server> <asp:Calendar id='_cal' runat='server' /> </form> </body> </html> If you render your page conditionally based on any other factor, you can use the VaryByCustom attribute in conjunction with an overridden implementation of HttpApplication.GetVaryByCustomString in your application class. The purpose of this function is to take the string value of the VaryByCustom attribute as a parameter and return a string that is unique with respect to some aspect of the page, request, or application. In most cases, the implementation of GetVaryByCustomString checks some value in the current HttpBrowserCapabilities class and returns a unique string based on that value. For example, suppose that you have built a page that renders differently based on the client browser's level of table support. You might provide an overridden version of GetVaryByCustomString , as shown in Listing 9-7. Listing 9-7 GetVaryByCustomString Implementation<! File: global.asax > <%@Application language='C#' %> <script runat=server> public override string GetVaryByCustomString(HttpContext ctx, string arg) { switch (arg) { case "Tables": return "Tables=" + ctx.Request.Browser.Tables; default: return ""; } } </script> This implementation would return a string value of "Tables=true" for client browsers that supported tables and "Tables=false" for client browsers that did not. This string would then be appended onto the other OutputCache distinguishing strings and used to index the output cache to store and retrieve renderings of this page. An example of a page that used this VaryByCustom attribute is shown in Listing 9-8. Listing 9-8 Using VaryByCustom in a Page<%@ Page language='C#' %> <%@ OutputCache Location='any' VaryByParam='none' Duration='120' VaryByCustom='Tables' %> <html> <head> <script runat="server"> protected void Page_Load(Object src, EventArgs e) { if (Request.Browser.Tables) // render with tables else // render without tables } </script> </head> ... In general, when you add output caching to a page, it is important to ask yourself if this page will render itself differently in different conditions (different client properties, different times of day, and so on) and make sure you compensate for that by indexing the output cache uniquely for all those different rendering possibilities. 9.2.3 Page Fragment CachingEven more common than entire pages that change infrequently are portions of pages that change infrequently. For example, there are often navigation bars, menus , or headers that are common to many pages in an application and that change infrequently ( especially not between different client requests), which makes them ideal for caching. Fortunately, ASP.NET provides a mechanism for caching portions of pages, called page fragment caching . To cache a portion of a page, you must first encapsulate the portion of the page you want to cache into a user control. In the user control source file, add an OutputCache directive specifying the Duration and VaryByParam attributes. When that user control is loaded into a page at runtime, it is cached, and all subsequent pages that reference that same user control will retrieve it from the cache, thus improving throughput. The user control shown in Listing 9-9 specifies output caching for 60 seconds. Listing 9-9 Specifying Page Fragment Caching in a User Control<! File: MyUserControl.ascx > <%@ OutputCache Duration='60' VaryByParam='none' %> <%@ Control Language='C#' %> <script runat=server> protected void Page_Load(Object src, EventArgs e) { _date.Text = "User control generated at " + DateTime.Now.ToString(); } </script> <asp:Label id='_date' runat='server' /> In the sample client page shown in Listing 9-10, the page itself is not output-cached, but the user control embedded in it is. In this example, because both the page and the control it embeds print the time at which they were generated, you will see a discrepancy between the printed times as the page is refreshed and the control is drawn from the cache. Listing 9-10 Cached User Control Client<! File: UserControlClient.aspx > <%@ Page Language='C#' %> <%@ Register TagPrefix='DM' TagName='UserCtrl' Src='MyUserControl.ascx' %> <html> <head> <script runat='server'> protected void Page_Load(Object src, EventArgs e) { _pageDate.Text = "Page generated at " + DateTime.Now.ToString(); } </script> </head> <body> <form runat='server'> <DM:UserCtrl runat='server'/> <br/> <asp:Label id='_pageDate' runat='server' /> </form> </body> </html> User controls also can change their rendering based on the type of request the control is responding to or perhaps based on properties exposed by a control. It is important to determine the circumstances under which the contents of a user control will change before you apply the OutputCache directive to it. There are three ways of indicating that a distinct cache entry is required for a user control caching.
The first of these three options is probably the least likely to be useful, since user controls are typically used from several different pages, whose POST variables will be different. It can be complicated to correctly identify the variables to vary by because of the way the parameters are parsed and sent to user controls (they are scoped by the control name). The second of the three options was added to simplify the process of identifying which parameters should determine unique cache entries for your user control. Instead of referring to POST or GET variables directly, your user control can specify which of its child controls should affect its cache entry. For example, if you built a user control that changed its rendering based on the value of a drop-down list, you would want to be sure that there was a unique entry for every value of that drop-down list. By specifying the drop-down list in the VaryByControl attribute, you ensure that a unique cache entry will be stored for each value selected in the list. The user control shown in Listing 9-11 demonstrates this. Listing 9-11 Specifying VaryByControl in a User Control<! File MyUserControl.ascx > <%@ OutputCache Duration='120' VaryByControl='_favoriteColor' %> <%@ Control Language='C#' %> <p>Select your favorite color</p> <asp:DropDownList AutoPostBack='true' id='_favoriteColor' runat='server'> <asp:ListItem>red</asp:ListItem> <asp:ListItem>green</asp:ListItem> <asp:ListItem>blue</asp:ListItem> </asp:DropDownList> <p>Here it is!</p> <span style='width:50;background-color: <%=_favoriteColor.SelectedItem%>'> </span> The third option for uniquely specifying cache entries for user controls is to expose public properties. There is nothing special you have to do to enable this except to expose public properties and set the property values in the user control creation. For example, if we had a user control that exposed a single public property called FavoriteColor , adding an output cache directive to the control would cache separate versions of the control based on the value of that property on creation. A sample user control that does this is shown in Listing 9-12, and a sample client is shown in Listing 9-13. Listing 9-12 Specifying Unique Cache Entries by Exposing a Public Property<! File MyUserControl.ascx > <%@ OutputCache Duration='120' VaryByParam='none' %> <%@ Control Language='C#' %> <script runat='server'> private string _color; public string FavoriteColor { get { return _color; } set { _color = value; } } </script> <p>Here is your favorite color:</p> <span style='width:50;background-color:<%=_color%>'> </span> Listing 9-13 Client to Cached User Control with Public Property<%@ Page Language='C#' %> <%@ Register TagPrefix='DM' TagName='UserCtrl' Src='MyUserControl.ascx' %> <html> <body> <form runat='server'> <DM:UserCtrl FavoriteColor='green' runat='server'/> </form> </body> </html> 9.2.4 Output Caching Considerations and GuidelinesAs we have seen, you have many options to consider when enabling output caching for a page. It is important to balance the estimated increase in throughput with the additional overhead of saving one or more renderings of a page in memory. While this trade-off is not easy to calculate precisely, here are some guidelines you should consider when deciding whether to enable output caching on a page.
|