ASP.NET provides developers with the ability to cache dynamically generated pages.

This means that it is now possible to cache pages built on posted data and querystrings! For instance, an e-commerce site that generates the same catalog from the database over and over on nearly every user request can now simply cache the catalog pages. Caching saves precious database server CPU cycles and renders pages down to the client much faster. Of course, when the catalog data is updated, the cache can simply refresh itself. Furthermore, developers can define the length of time an item is to be cached, indicate cache dependencies, create cached versions per browser, and indicate where an item should be cached (client, server, proxy, etc.).

Caching is one of the greatest new features that ASP.NET provides regarding application performance.

There is no doubt that caching is one of the best methods of increasing your Web application's performance. ASP.NET and the .NET Framework provide a set of tools that make implementing caching simple. Data that is used over and over in your application, updated semi-regularly (like a catalog, list of contacts, store locations, etc.) make great candidates for caching. The right caching strategy will no doubt increase throughput on your servers and put smiles on your users' faces. This article describes how to implement caching in your applications using the .NET Framework.

Caching Basics (Output Caching)

Output caching involves the strategy of taking a dynamically generated Web page and caching its content so that subsequent requests for the page are fulfilled from an in-memory cache rather than through the execution of code. In high volume Web sites, the right caching strategy can dramatically improve scalability statistics.

ASP.NET gives developers multiple methods of implementing caching. For the most basic page-level caching, developers can use the declarative API, which is simply the term for defining caching parameters at the top of an ASP.NET page using HTML style tags.

First you need to find a candidate page for caching. For example, suppose that you have a Web-based report that displays monthly sales figures by region for the current year. This report's data is updated infrequently, but regularly, as different regions report at different times. The report is accessed many times by many people throughout the organization. Sounds like a good candidate for caching! To do so, you simply place the following declaration at the top of your ASP page.

<%@ OutputCache Duration="3600" VaryByParam="None" %>

This declaration instructs the .NET Framework to cache this Web page for a specified number of seconds (in this example, the page will be cached for 3600 seconds, or one hour). Requests made for the page during this time are returned from cache. The page does not execute its code to satisfy these requests, but instead uses in-memory results, thus conserving server resources and increasing page response times.

This declarative API allows you to indicate a simple and effective caching strategy for most of your pages. However, for developers who want more control over caching within their code, ASP.NET also provides a programmatic API. This API is a set of classes, methods and properties that wrap the caching features exposed by the .NET Framework. These classes enable you to set caching directives from within your ASP.NET applications. For example, the caching directive in the previous example could also be indicated programmatically by the following code:

Page.Response.Cache.SetExpires(Now.AddHours(1))

This code can be added to your code-behind class inside your Page_Init or Page_Load method. Notice that we call the SetExpires method to indicate a date on which the cached page should expire (again, one hour in our example).

This article will walk you through the classes related to caching with ASP.NET. It will further explore different caching strategies. It will also discuss updating the cache, removing items from the cache, and setting cache dependencies.

The Namespaces (System.Web and System.Web.Caching)

There are two namespaces containing ASP.NET caching classes: System.Web and System.Web.Caching. Caching-related classes found inside System.Web are used to cache Web pages and portions of Web pages. The System.Web.Caching class provides developers direct access to the cache, which is useful for putting application data into the cache, removing items from the cache and responding to cache-related events. Table 1 provides a quick reference to the various classes used in ASP.NET caching strategies.

Cacheability (Cache on the Client/Server)

Before a page can be cached, you must indicate its "cacheability." Setting this parameter indicates the devices on which your page can be cached. Valid devices include the client making the request, the server fulfilling the request, proxy servers, etc. To indicate a page's cacheability, you use the following method call at the top of your page:

Page.Response.Cache.SetCacheability( _
  HttpCacheability.Server)

The SetCacheability method takes a member of the HttpCacheability enumeration as its only parameter. The members of this enumeration are designed to allow you to control where your page is cached. Table 2 presents the details for each enumeration member.

Version Caching (Versions of Pages)

Dynamically generated pages are typically created based on a set of parameters sent either to the page in the form of querystring parameters or posted using form fields and a submit button. ASP.NET makes it easy to cache these pages as well. The key to caching these pages is to cache versions of the page. That is, one version of the page for each value for a given parameter (or set of parameters).

The duration parameter in the example code represents the number of seconds the page should remain in the output cache before being refreshed.

For example, suppose our hypothetical regional sales report allows users to filter the data they view by month. Imagine the month parameter is passed to the page via the querystring. If we indicate that the page is cached, but do not cache based on this parameter, then only calls to the page without the querystring parameter will be cached. Each call using the querystring will override the cache and cause the page to execute its code rather than pull from cache. Of course this is not the caching behavior you want. Instead, each call to the page with a new month should result in a new page added to the cache as its own separate item. This way, subsequent requests for the same month can be delivered from the cache.

.NET makes version caching rather simple via the HttpCacheVaryByParams class. This class is accessed by a property exposed by Response.Cache. For example, the following code indicates that the page should cache versions of itself based on the Month querystring parameter.

Page.Response.Cache.VaryByParams( _
  "Month") = True

You'll be happy to know that the same line of code works for parameters posted to the page. For instance, if you submit the month data to the page based on a user form field (text box, dropdown, etc.) with the same name of "month," then the same line of code results in multiple cached versions of the page.

Of course, pages are often rendered based on more than one parameter. For instance, both month and region might be parameters in our example. Each combination of month and region should result in a separate page being added to the cache. You can indicate a group of parameters on which to base caching for a given page using the same VaryByParams property. To do so, you simply separate each parameter with a semicolon, for example:

Page.Response.Cache.VaryByParams( _
  "Month;Region") = True

You can also indicate that a page should be added to the cache based on all its parameters. To do this, you use an asterisk (*) as the VaryByParam value.

Fragment Caching (Page Portions / User Controls)

Many times, it is simply not practical to cache the entire contents of a given page. Suppose, for example, that our fictitious sales report required real-time updates for the current month. Past months might be able to stay in the cache for days without being updated. An ideal strategy in this case would be to cache the previous month's data while updating the current month on every request.

Valid devices include the client making the request, the server fulfilling the request, proxy servers, etc.

To cache portions of a page while dynamically generating the rest of the page you must employ a user control strategy. In the monthly reporting example you could sub-divide the page into a user control that rendered all past months and the page itself, which would be responsible for generating the data for the current month. The key to this type of page fragmentation is to ensure that the user control remains completely self-contained, which means that the user control should not rely on the code inside the Web page for execution (this happens to also be good user control design). Once fragmented, you simply tag the user control to be cached (inside its code-behind class). You can do so using the declarative API (see Caching Basics) or through the attribute class, PartialCachingAttribute. The following is an example using the PartialCachingAttribute class:

<PartialCaching(3600)> _
Public MustInherit Class PastMonths
  Inherits System.Web.UI.UserControl

In this example, we set the caching duration parameter to 3600 seconds (one hour). In addition to the required duration parameter, this attribute class also allows you to employ version caching of your control. To do so, you use the optional parameter, VaryByParam. This parameter works the same way as the VaryByParam property did in the previous section. However, because the control must remain completely self-contained in order to cache properly, the VaryByParam only works for data that is posted from the user control back to its code-behind class. That is, you must submit data from the control back to the control's code. You cannot, for instance, cache the control based on querystring values from the page hosting the control.

Application Data Caching (Frequently Accessed Data)

Often times, our application data does not present itself as a Web page or portion of a page that can be cached. Rather, we might have a data set that was expensive to create and is requested often or a database connection string that we want to add to the cache. Thankfully, in cases like these, ASP.NET provides us direct access to the Cache via the Cache class.

The Cache class represents the collection of cached items in an ASP.NET application. That is, one (and only one) Cache class is created by the framework for your application domain. For this reason, the Cache is dependent on your application. If your Web application is restarted, for instance, the Cache is flushed and re-created by the framework (empty, of course).

If you know how to work with a collection class, then you know how to work with the Cache object. Like a collection class, the Cache class exposes the Item property for accessing items, and a Count property for determining the number of items in the collection. The Cache class is accessed by your code from the HttpContext object, for example:

Response.Write( _ HttpContext.Current.Cache.Item("MyCachedItem"))

Of course, you do not have to specifically reference the HttpContext object in ASP.NET. The same line could be written as follows:

Response.Write(Cache("MyCachedItem"))

Note that when you attempt to retrieve an item from the cache, you will want to respond gracefully if the requested item does not exist. For instance, if you assign a local variable to hold the contents of your cached item and the item is not found, your variable will contain nothing. Therefore, a typical strategy is to check the variable's value using the Visual Basic IsNothing function. If the function returns true, often times you will want to call a procedure to add the item back to the cache.

To place items into the cache, the Cache class exposes both an Add and an Insert method. The Add method exists to support the object's contract as a collection class. Calling it returns a reference to the item added to the cache. However, for most operations, you should use the Insert method. Unlike the Add method, the Insert method allows you to overwrite an item already in the cache without first calling the Remove method (whereas the Add method fails if the item is already in the cache). For example, the following code inserts the contents of the connString variable into the cache:

Cache.Insert(key:="ConnString", _
  value:=connString)

Of course, this is the most basic example. The Insert method has a number of other parameters that enable you to control items added to the cache. One of the most notable is slidingExpiration. You can use the slidingExpiration parameter to indicate when an item should be removed from the cache. Unlike the absoluteExpiration parameter, slidingExpiration enables you to set the number of minutes after the last request that an item in the cache should expire.

If the server requires memory resources it will sometimes claim them from the cache. This process is called scavenging. The framework can automatically remove an item from cache to reclaim memory. In this case, the priority parameter comes in handy. The priority parameter enables you to rank cached items relative to one another. This parameter takes a member of the CacheItemPriority enumeration whose values, in order of least likelihood of being removed, include NotRemovable, High, AboveNormal, Normal, BelowNormal and Low. Table 3 further presents a reference to all of the parameters available from the various overloaded versions of the Insert method.

You can explicitly remove items from the cache. Normally, the .NET caching system automatically manages cached items. However, sometimes you may want to remove items explicitly from the cache based on an event or user action. To do so, you simply call the Remove function as follows:

Cache.Remove(key:="MyCachedItem")

Caching Dependencies

The validity of items in the Cache is often times dependent on other objects. For instance, suppose we create another fictional report to display year-to-date sales. This page can also be cached, but rather than having a specific timeout period, it instead relies on the monthly sales data that was added to the cache separately. The page only needs to be refreshed if there is an update to the cached monthly sales data. Therefore, a dependency exists between this report and the monthly sales data. Additionally, let's suppose that our data store for the monthly sales data rests in an XML file (Listing 1). In this case, the cache only needs to be updated when this file changes. Therefore a dependency between the file and the cached information exists.

If you know how to work with a collection class, then you know how to work with the Cache object.

Thankfully, ASP.NET enables you to enforce these types of dependencies. In the first instance, we can use the AddCacheItemDependency method in the HttpResponse object to indicate a dependency between the year-to-date sales report and the cached monthly sales figures. To set up this dependency, simply add the following call to the response inside the Web page's code-behind class, where cacheKey represents the key item in the cache collection:

Response.AddCacheItemDependency( _
  cacheKey:="MySalesData")

Of course, this works nicely for cached Web pages that are reliant on an item in the cache. However, you might want to use two items directly in the cache that are reliant on one another. In this case, you must access the cache directly through the Cache object (see Application Data Caching). The Insert method of the Cache class enables you to setup this dependency. You do so by creating an instance of the CacheDependency object and passing it as a parameter to the Insert method. Among other things, this class allows you to indicate one or more cacheKeys that the newly added item will be dependent upon. The following code creates an instance of CacheDependency for the Insert method:

Dim cacheDepend As New _
  System.Web.Caching.CacheDependency( _
  fileNames:=Nothing, _
  cacheKeys:=New String(0) {"MyCachedItem"})

Just as there are two distinct ways to set dependencies between cached items (page to cached item and cached item to cached item), similar methods exist for setting dependencies between cached items and files/directories. First, to indicate that the validity of a cached response (Web page) is dependent on a file, you can call the AddFileDependency method in the HttpResponse object as follows:

Response.AddFileDependency( _
  fileName:="C:\data\MonthlySalesByRegion.XML")

In addition, you can indicate that an item being added to the cache is dependent on a file (or set of files). You do so via the Cache class. Again, you call the Insert method and pass an instance of the CacheDependency object. The following code creates a CacheDependency object based on a file:

Now, you simply call Insert and pass the cacheDependency object as a parameter.

Putting it All Together (A Sample Application)

Okay, we've hinted at it long enough, now let's apply this caching knowledge to an actual version of the sales report page. The page we are going to create will read sales data from an XML file. The sales data is split out by region and month. Each region has an attribute called id, which uniquely identifies it. Of course it also has a name. The sales information is found as elements under the region node containing the month attribute. Listing 1 provides the complete contents of the XML file.

Our page is going to present the sales data to the user based on the Month querystring parameter. Therefore, we want to cache different versions of the page based on this parameter. To do so, we call the VaryByParams property of the Response.Cache object.

Additionally, we want to make the cached page dependent on our XML file. That is, when the XML file gets updated, the cached page should automatically expire and refresh itself. We do this by calling the AddFileDependency method off of the Response object. The rest of our code reads the contents of the XML file and adds it to an HTML table displayed to the user. Listing 2 presents the full code for our page's load event. Figure 1 shows the output of sales report.

Figure 1 - Monthly Sales Report

As we've seen, caching can be a simple and effective strategy that can give your ASP.NET applications an extra edge in performance. You should now be ready to employ these same strategies in your code.