The easiest and most effective way to increase the performance of ASP.NET applications is through the use of the built-in caching engine. Although it is possible to build your own cache (as discussed in “Caching with .NET—when to buy, when to build”), you’ll likely find that the caching engine has so much functionality built into it that you needn’t bother. For the most part, ASP.NET developers wrap the functionality of the caching engine directly into presentation and data access classes in Web applications, and as you’ll see, it’s very easy to do so.
ASP.NET’s caching engine supports three kinds of caching:
- Page output caching is the caching of the rendered HTML for an entire page the first time a page is requested. Subsequent requests are then served the cached copy.
- Fragment caching specifically applies caching a fragment of HTML, like the output from a Web user control. It gets its name from the fact that these are said to contribute a fragment of HTML to a page.
- Data caching is concerned with caching individual variables or data items. It operates at a lower level than either of the other caching types.
Caching entire pages with page output caching
Page output caching is the simplest kind of caching and is accomplished declaratively by adding a single directive to the page in question, OutputCache. Page output caching allows you to effectively skip reprocessing a page’s Init, Load, PreRender, Render, and Unload events. If those events access back-end systems such as databases, the timesavings become more dramatic. ASP.NET can cache several variants of a page and associate each page with subsequent requests. This is all controlled through the OutputCache directive, which is placed at the top of an ASPX page, and takes the following format:
Location="Any | Client | Downstream | Server | None"
VaryByCustom="browser | customstring"
VaryByParam="parameter name" %>
The OutputCache directive may have up to five associated attributes that control the cache’s behavior. The possible attributes and their meanings are summarized in Table A.
The OutputCache directive can easily be used to create a single version of a static page in memory, like so:
It can also cache multiple versions based on the id query string parameter like this:
In both of these cases, the page will be flushed from the cache after 5 minutes, or 300 seconds, as per the amount specified for the Duration attribute.
Cache based on custom information
The VaryByCustom attribute can be used to create cached versions of a page based on custom information you provide. For example, to create a version of the page for each type of browser, you could use the following directive:
You can also use VaryByCustom to cache pages based on application-specific information. For example, to create a cached version of a page based on the current user’s department name, you could use the following directive:
Then, in the code-behind class for the Global.asax file, you would override the GetVaryByCustomString method as shown below in VB.NET:
Public Overrides Function GetVaryByCustomString( _
ByVal context As System.Web.HttpContext, _
ByVal custom As String) As String
If custom = "department" Then
' return the department for the current user
The GetVaryByCustomString method is passed the custom value used in the directive. When this method is called by the ASP.NET runtime, your own custom algorithm can be used to determine the current user’s department based on the input value and return it from the method.
Cache based on header information
The VaryByHeader attribute can be used to create multiple cached versions of a page based on one or more of the HTTP headers the page receives. For example, to create and cache versions based on the language of the browser you could use the following directive:
Checking the headers
To see the values of the various HTTP headers sent with a particular Web request, you can turn on ASP.NET page tracing by placing the trace and traceMode attributes in the page’s Page directive. Alternatively, you can do this through Visual Studio .NET’s Properties sheet.
In addition to caching whole pages declaratively, you can also set a page’s caching options programmatically by using the Cache property of the Response object exposed by the Page class. The Cache property exposes the HttpCachePolicy object for the page and can be used to set the cache for five minutes on the server, as in the following C# snippet:
private void Page_Init( object sender, System.EventArgs e )
Caching partial pages with fragment caching
Using fragment caching (which allows you to cache fragments of HTML) is very efficient because it lets you flexibly combine parts of a page that change frequently with those that do not, while still retrieving the static pieces from the cache. A practical example would be using fragment caching for Web user controls that make calls to XML Web services. Doing so is particularly effective since it allows your Web site to be less tightly coupled to the Web service while increasing performance dramatically.
To use fragment caching you once again place the OutputCache directive at the top of an HTML page. This time, however, you’ll be placing it in the .ASCX page of the Web user control. However, with fragment caching, the Location and VaryByHeader attributes are not supported, although the additional VaryByControl attribute is.
Using the VaryByControl attribute, you can specify one or more properties of the user control in a semicolon-delimited list. Cached versions will be created for each combination of property values. For example, assume your user control exposes a custom State property that controls which elements of the user control are displayed. You can cache a version of the rendered control in the cache for each value of State using the following directive:
<%@ OutputCache Duration="300" VaryByControl="State" %>
However, when caching a Web user control, it is important to remember that the ASP.NET runtime simply substitutes the cached HTML for the actual control, sidestepping any control processing that would typically occur. The practical implication is that code executing in the page will not be able to programmatically manipulate a cached user control or any of its properties. In other words, the Web user control must be fully self-sustaining and be able to initialize itself through its Load and Init events to be effectively cached.
Fragment caching can also be accomplished declaratively using an attribute rather than the OutputCache directive. The PartialCaching attribute can be placed on a class derived from UserControl in the code-behind file so that the ASP.NET runtime can read it and cache the rendered HTML accordingly. For example, the following declaration of the code-behind class will cache the Web user control for five minutes based on the id value in the query string.
<PartialCaching(300, "id", Nothing, Nothing)> _
Public MustInherit Class MyHeaderControl
Keep your data around with data caching
The final type of caching supported by the ASP.NET caching engine, data caching, is by definition, lower level than either page output or fragment caching. Data caching is useful in scenarios where the same data, for example, a list of products, will be needed on several different pages that will display the data in different ways. Of course, the performance benefit of data caching is tied to reducing the number of calls required to the back-end database.
To add an item to the cache you can use the Cache property of either the Page or UserControl class since both of these classes ultimately derive from the Control class. The Cache property exposes the System.Web.Caching.Cache object, which allows you to store data as a combination of keys and values. Using this property, a developer can write code to populate an item and place it in the cache or simply pull it out of the cache if it already exists as shown here in C#:
DataTable dt = null;
if (this.Cache["Products"] == null)
// Go get the data from the database
this.Cache.Insert("Products", dt, null, _
dt = this.Cache["Products"] As DataTable;
In the above example, I first check to see if the item with the key Products is in the cache. If it isn't, an ADO.NET DataTable is retrieved from the back-end database and then placed in the cache using the Insert method. In this case, I use the Insert overload that allows me to specify an absolute expiration for the cached object of six hours without a sliding expiration interval. If, on the other hand, the item is found in the cache, I simply pull it back out and cast it back to a DataTable using the As expression.
It should be noted that when caching data retrieved using ADO.NET you should cache either DataTable objects as shown here or entire DataSet objects, because both of these objects are fully disconnected from any data source and do not hold onto database connections. It makes little sense to cache data readers, such as SqlDataReader, because they will be used only once (they are forward-only readers) and will tie up a database connection for the entire time they are open.
The flexibility and features of the ASP.NET caching engine make it one of the most important features in creating high-performance ASP.NET applications. Using the basic information covered in this article, you should easily be able to put the caching engine to work in your applications.