Take Advantage of ASP.NET 2.0’s Data Caching Techniques, Part 1

ne unfortunate by-product of the rapid move away from standalone and client/server applications to the Web-based environment is the disconnected nature of the HTTP protocol. Web servers and clients briefly exchange data, and then (by default) each forgets about the other entirely until the client makes another request. This meet-exchange-forget cycle particularly?affects the way that the individual pages of an application handle data?especially when there is a requirement to store data that is either expensive to create (in terms of resource and processing usage), or which is used regularly throughout the life of the application.

Since version 1.0, ASP has provided techniques to help you maintain state information and cache data for an application, as the Application and Session objects that it exposes. ASP.NET 1.0 added another headline feature, the application-level Cache object (complete with its support for output caching of ASP.NET-generated HTML), which is especially useful because it supports features to expire and invalidate items and pages stored in the cache. Now, ASP.NET version 2.0 adds new features that make using the Cache object and output caching an even more compelling solution.

This article contains an overview of the primary techniques available in ASP.NET for caching values and data between user requests, and then goes on to concentrate on the Cache object and output caching?with particular emphasis on the new features in version 2.0. You’ll see information about:

  • Choosing the right approach for caching data.
  • Application, Session and ViewState data caching.
  • Global caching of data using disk files.
  • The ASP.NET output caching features.
  • Using the ASP.NET Cache object directly.
Author’s Note: The ASP.NET version 2.0 code described in this article is based on the Beta 1 release of the .NET Framework v2.0. All other code is based on the final V1.1 release of the .NET Framework. You can download all the examples. If you install them on your own server, you may have to edit the connection strings in the web.config file, depending on the location of your database. There is a menu page named default.htm in each of the two folders in the samples (one for ASP.NET v1.1 and one for v2.0 Beta1).

Choosing a Caching Technology
The most important feature when choosing one of the available caching technologies is cache visibility. In other words, where do you need to be able to access the cached data? If it is just for the life of a page or over repeated postbacks from a particular page, you can take advantage of the ViewState of that page, which is basically a dictionary object that stores string values encoded into a hidden control on the page. Or you can use output caching to cache a portion or the entire HTML generated for a page so that the server does not have to re-execute the page for every request. In ASP.NET version 2.0, most of the new data source controls can cache their data, so you can also take advantage of that capability.

However, to store data required to be accessible across more than one page, you need an alternative technique. If you only need to make the values available for a specific user, you can use the Session object to store values and data. As long as the user’s browser supports cookies, or you turn on cookieless session support, data in the user’s Session store will be available from any page they open while the session is active.

If you want to make your data visible to all the users of an application, you must use either the ASP.NET Application object or the Cache object. The advantage of the Cache object is that you can program expiration rules and manage the lifetime of the data more closely. Of course, you can use the Application and Cache objects for user-specific data as long as you manage the identification of each instance of the data, probably through unique or user-specific cache key names.

Finally, suppose you want to make cached data available to all users and all applications on a server, or even on a networked group of servers? The only real answer here is to use some sort of disk or memory persistence implemented and managed outside of ASP.NET. In fact this is reasonably easy to do, using the classes implemented in the .NET Framework class library for reading and writing streams (including memory streams) and disk files. In general, disk-based storage is the better option for longer persistence periods, because it can survive server restarts, application failures, and most other events that would cause other forms of cached data to be lost.

Table 1 shows the features of each technique briefly mentioned here, including a note about the volume of data that each is designed to cope with. For example, ASP.NET transmits the ViewState of a page over the network twice with each page request, and so you should keep it to a minimum. If you have a great many concurrent users, even small volumes of data stored in each user’s Session can swallow up valuable memory on the server unless you configure alternative Session support features such as the ASP.NET State Store or storage in a central SQL Server database.

Table 1. Cache Visibility: The table lists data visibility and optimum size for various caching techniques.

Technique Visibility Optimum data size
Page ViewState Page Small
Data source controls Page Large
Page output caching Page Large
ASP Session User Small/Medium
ASP Application Application Medium
ASP.NET Cache Application Large
Disk file Global Large

It’s important to note that you don’t have to choose the most restrictive visibility for your cached data. For example, disk-based storage?perhaps as an XML file?would be an ideal solution for data that is expensive to retrieve from its source and yet changes infrequently (particularly if accessed only occasionally by users). But what if each user requires a different set of data? You could still cache it using some user-specific value in the filename, perhaps the Session ID or user login name. The only downside is that you have to manage refreshing and removing stale data yourself.

The ASP.NET Application, Session and ViewState
You use the Application and Session objects in ASP.NET in essentially the same way as in “classic” ASP, except that you should be aware of data typing issues. It’s always a good idea to cast or convert values to the correct type when you extract them. For example:

   Dim iThisValue As Integer = 42   Application("MyInteger") = iThisValue   Dim iThatValue As Integer = CType( _      Application("MyInteger"), Integer)

In C#, that same code is:

   int iThisValue = 42;   Application["MyInteger"] = iThisValue;   int iThatValue = (int) Application["MyInteger"];

Alternatively, you can use the methods exposed by the HttpApplicationState class (from the System.Web namespace), which is the class used to implement the Application object in ASP.NET. These methods include Add, Clear, Get, Remove, RemoveAt, and Set, plus properties and methods to retrieve information about the keys for the stored items such as AllKeys, Count, GetKey, etc.

Author’s Note: See the .NET Framework SDK topic “HttpApplicationState Members” in the “Reference | Class Library” section for more details.

ASP.NET itself uses thread locking to automatically lock the Application (and Session) to prevent concurrent updates to the values stored there from causing inconsistent results; however if you are updating values in the Application you should use the Lock and Unlock methods as in previous versions of ASP:

   Application.Lock()   Dim iThisValue As Integer = CType( _      Application("MyInteger"), Integer)   iThisValue += iMyIncrement   Application("MyInteger") = iThisValue   Application(UnLock)

You aren’t limited to storing only simple intrinsic .NET value types in the Application (or Session). You can store any serializable class instance, for example a DataSet (in version 2.0, the DataTable is also serializable). As an example, this code stores a DataSet in the Application:

   Application("MyDataSet") = dsMyData   Dim dsRetrievedData As DataSet = _      CType(Application("MyDataSet"), DataSet)

ASP.NET Sessions
The HttpSessionState class (in the System.Web.SessionState namespace) implements ASP.NET’s Session object. The Session object has no Lock or Unlock method, though you can use the IsSynchronized and SyncRoot properties to create your own thread-safe implementations if you wish. However, for general use, the Session works just like the Application for reading and storing values.

Remember that the Session exposes some useful properties such as IsCookieless and IsNewSession, which you may find useful. But the most important feature in ASP.NET in terms of improving page execution efficiency is the implementation of read-only sessions. ASP.NET raises a whole series of events as it loads and executes an ASP.NET page, and two of these are concerned with loading and saving Session data. As a page loads, an event causes ASP.NET to search the Session for any values that apply to the page, and load those it finds into the execution memory space for the page. As execution of the page ends, another event causes ASP.NET to write those values back to the Session store.

So it’s pretty obvious that you can reduce page execution overheads by avoiding the two event calls if you don’t need to read or update session data. And if you only need to read it, but not update it, you can specify read-only access to the Session. To specify that a page does not require access to the user’s session, add this directive:

   <%@Page EnableSessionState="False" ... %>

To allow your application to read session data but not update it, thus avoiding the expensive process of updating the session data at the end of the page, use this directive:

   <%@Page EnableSessionState="ReadOnly" ... %>

Note that there is also a property named IsReadOnly on the HttpSessionState class that you can query in your page code to see if the current session is read-only. You can also specify the session behavior at machine or application level using the element within the section of machine.config or web.config.

   

ASP.NET ViewState
If you need to cache only a single value or a small set of data for an individual page between postbacks, you can use the ASP.NET ViewState. When an ASP.NET page contains a server-side HTML form (a

section declared with the runat=”server” attribute), it generates an HTML control that contains the values of the controls on the page, and other state information, in an encoded format.

You can add values to ViewState and retrieve them using the same approach as with the Application and Session objects:

   Dim iThisValue As Integer = 42   Viewstate("MyInteger") = iThisValue   Dim iThatValue As Integer = CType( _      Viewstate("MyInteger"), Integer)

In C#, the equivalent code is:

   int iThisValue = 42;   Viewstate["MyInteger"] = iThisValue;   int iThatValue = (int) Viewstate["MyInteger"];

Just bear in mind that your data passes across the network with each page request and each postback, and so this is definitely not an ideal approach for storing large volumes of page-specific data. A better solution is to store some key value in the ViewState, and then use that key to store and retrieve the real data from a database, a disk file, or one of the other types cache better suited to storing large volumes of data.

As with sessions, you can disable the storage of ViewState for a page. ViewState is enabled by default, and even when disabled some control and state information is still persisted. However, there is no concept of a “read-only ViewState.” To disable ViewState for the page, use the Page directive:

   <%@Page EnableViewState="False" ... %>

To disable ViewState for all the ASP.NET applications on a server or for all the pages in a site, alter your machine.config or web.config files as follows:

   
Author’s Note: See the .NET Framework SDK topic “Saving Web Forms Page Values Using View State” in the “Building Applications | Creating ASP.NET Web Applications” section for more details.

Caching Data as a String
A useful point to keep in mind whenever you store data that is not a string value, especially when you are dealing with more complex classes that persist their data internally (such as a DataSet), is that most support a method to export the content as a string. For example, with a DataSet (and, in version 2.0, a DataTable), you can use the WriteXml method to write the data as an XML document, which is itself just a text string. You can also provide the writeMode parameter (a value from the XmlWriteMode enumeration) that specifies if a schema is included, and whether you want a simple XML representation of the data or a diffgram that stores change information as well.

The following code shows how to serialize a DataSet as an XML document and store it in the ViewState of the page, and then re-instantiate the DataSet for each postback:

   Imports System.IO ' needed to use StringWriter   Dim dsMyData As New DataSet()   If Page.IsPostBack Then      Dim reader As New _         StringReader(CStr(ViewState("MyDataSet")))      dsMyData.ReadXml(reader)   Else      '... code to fill DataSet from source data store here ...      Dim writer As New StringWriter()      dsMyData.WriteXml(writer)      ViewState("MyDataSet") = writer.ToString()   End If
?
Figure 1. DataSet XML Persistance: When you first load this page, it reads the DataSet data from a database, storing the DataSet as XML in the ViewState, but subsequent page refreshes reconstitute the DataSet from the XML representation in the ViewState rather than reloading it from the database.
Author’s Note: See the .NET Framework SDK topic “StringWriter Members” in the “Reference | Class Library” section for more details.

The downloadable sample code for this article contains an example of caching the contents of a DataSet as an XML string value within the page ViewState. The write-ViewState.aspx example uses the code listed above. The result looks like Figure 1. When the server loads the first page, it fetches the data from the database; however, as you refresh the page, it rebuilds the DataSet each time from the XML cached in the ViewState.

Caching Data as a Disk File
The .NET Framework provides plenty of easy-to-use classes for reading and writing disk files. You can use these when you need to cache data required for more than one ASP.NET application. As an example, suppose you want to cache a DataSet containing data to which all the applications on your server require access. You could capitalize on the just-described technique for calling the WriteXml and ReadXml methods, which work automatically with a disk file?all you have to do is provide the path and filename.

However, you can also use a BinaryFormatter to write data from objects that are serializable to a Stream. The code below demonstrates using a FileStream with a DataSet, but you could just as easily cache the data in memory using a MemoryStream if required. Why bother with binary serialization when the DataSet can serialize itself as XML? Well, the one extra feature here revolves around the fact that, in version 2.0, the DataSet gains a new property named RemotingFormat. This can be set to a value from the SerializationFormat enumeration (the two options are Xml or Binary). When set to Binary, the volume of data generated from a DataSet is generally reduced by between 20 and 80 percent, which results in smaller disk files, faster loading, and ultimately better performance:

   ' create BinaryFormatter and Stream   Dim f As IFormatter = New BinaryFormatter()   Using s As New FileStream(datPath, _      FileMode.Create, FileAccess.Write, FileShare.None)        ' specify Binary remoting format for the DataSet     ' (new in v 2.0)     dsMyDataSet.RemotingFormat = SerializationFormat.Binary        ' serialize the contents to the Stream as binary data     f.Serialize(s, dsMyDataSet)     s.Close()   End Using

To rebuild the DataSet from the disk file, you can use this code:

   ' now de-serialize the data back into a new DataSet   Dim dsMyDataSet As DataSet   Dim f As IFormatter = New BinaryFormatter()   Using s As New FileStream(datPath, FileMode.Open, FileAccess.Read, FileShare.Read)        ' de-serialize file contents into a DataSet     dsMyDataSet = CType(f.Deserialize(s), DataSet)     s.Close()      End Using
?
Figure 2. DataSet Serialization: The figure illustrates the process of serializing a DataSet to a disk File and later deserializing it back into a DataSet.
Author’s Note: The preceding code takes advantage of the Using construct, which was available in C# in v1.1 and is now available in VB.NET in v 2.0.

Figure 2 shows the provided example (serialize-to-disk.aspx) that uses this code to serialize a DataSet to a disk file, and then de-serialize it back into a DataSet again. The page also displays some rows from the DataSet to prove that it worked.

Many objects support the ISerializable interface, and so can be used with a BinaryFormatter as shown above (though the actual format of the persisted data will vary with the object type). Suitable classes include collection types such as HashTable and NameValueCollection, the ADO.NET DataSet and DataTable, and the Image class that is used to create and store bitmap images.

Author’s Note: See the .NET Framework SDK topic “BinaryFormatter Members” in the “Reference | Class Library” section for more details.

ASP.NET Output Caching
One extremely useful way to boost performance in ASP.NET is to avoid running server-side script to provide a new version of the page for each request. To do that, you can use output caching, which is useful when users regularly or repeatedly load the same page. ASP.NET implements output caching internally, so it’s seamless for developers. By specifying a directive in the ASP.NET page, the server automatically caches the HTML generated by the page according to the conditions you specify.

The simplest output caching directive is:

   <%@OutputCache Duration="#seconds" VaryByParam="None" />

The preceding line instructs ASP.NET to cache the HTML for the specified number of seconds, irrespective of the parameters sent with the page request from the client. “Parameters” in this sense means any values in the Request collections (Form, QueryString, Cookies, and ServerVariables). While the page is cached, every request will return the HTML generated the last time that the page was executed being sent to the client. In IIS 6.0, in Windows Server 2003, this detection of cached pages is actually done inside the kernel-level HTTP redirector module, and so it is blindingly fast?quicker than in IIS 5.0 and, of course, a great deal faster that re-executing the page.

There are many alternative ways to configure output caching in ASP.NET. The OutputCache directive provides a range of options, for example:

   <%@ OutputCache Duration="#seconds"       Location="[Any|Client|Downstream|Server|None]"       Shared="[true|false]"       VaryByControl="control-name(s)"       VaryByParam="[None|*|parameter-name(s)]"        VaryByHeader="header-name(s)"        VaryByCustom="[Browser|custom-string"] %>

Table 2 defines the attribute values used above.

Table 2. OutputCache Directive Attributes: The table lists the attributes you can use to fine-tune the OutputCache directive.

Attribute Description
Duration Required in every ASP.NET OutputCache directive. The number of seconds that the generated HTML will be cached.
Location Defines if page output can be cached on a proxy or downstream client. Often omitted, so that caching is performed in the most efficient way. However, it is a good idea to include Location=”Any” where you do not actually need to control the location.
Shared Used only in a User Control. Defines if multiple instances of the control will be served from one cached instance. If False (the default) multiple instances will be cached.
VaryByControl Used only in a User Control. Declares the ID of ASP.NET controls that the cache will be varied by. For example: VaryByControl=”MyTextBox;MyListBox” will cause a new version of the page to be cached for each differing value in the specified controls within the user control.
VaryByParam Required in every ASP.NET OutputCache directive. Defines the names of the parameters that will be used to determine if a cached version of the page will be delivered. For example: VaryByParam=”txtName;lstSelection” will cause a new version of the page to be cached for each differing value in the QueryString, Form, Cookies or ServerVariables collections that has one of the specified names.
VaryByHeader The name(s) of any HTTP Header(s) that are used to determine if a cached version of the page will be delivered. For example: VaryByHeader=”USER-AGENT;SERVER;REMOTE-HOST”
VaryByCustom The special value “Browser” specifies that the cached version of the page will only be delivered to browsers with the same name and major version: VaryByCustom=”Browser”. Alternatively, you can place a custom function in global.asax that returns a String value based on some criteria you require, and the page will be varied for each client that causes a different value to be returned. For example: VaryByCustom=”MyGlobalFunction” (see the following note for an example of this).

Author’s Note: Remember that ASP.NET varies the HTML it generates based on the browser type (“uplevel“?meaning Internet Explorer 5 and above; or “downlevel“?meaning all the rest). Therefore you might want to consider including the VaryByCustom=”Browser” attribute if your page uses ASP.NET controls that can generate browser-specific output. Examples are controls such as Menu and Panel. The way that ASP.NET specifies style information for most other Web Forms controls also changes depending on the browser type, and may affect the appearance. You can experiment with these effects using a sample page from the book “Pro ASP.NET 1.1 Web Forms Techniques”, see: http://www.daveandal.net/books/7868/control-compatibility/default.aspx . The asp-cache-by-level option shows how you can implement custom “vary by” functions. I’ve included the function code within the downloadable code samples for this article.

?
Figure 3. Output Caching: The figure illustrates how the VaryByParam=”*” attribute affects output caching.

You can use the example page named output-cache-params.aspx (see Figure 3) to experiment with output caching. As the VaryByParam attribute is set to “*”, all the controls on the page will invalidate the cached HTML copy, and so changing the value in any one will cause the page to be re-executed (the time it was last executed is shown in the page). However, refreshing the page within the 10-second cache duration period specified in the OutputCache directive does not cause it to be re-executed.

One interesting point demonstrated in this example is that using the ASP.NET auto-postback feature causes the page to be re-executed more than once. For example, changing the value in the second drop-down list initiates a postback automatically. And, because the values in the controls have changed, ASP.NET invalidates the cache and re-executes the page. However, if you then click the Refresh button, it executes the page again?rather than serving it from the output cache. This is because the name and caption of the button are included in the parameters posted back to this page when the button is clicked, whereas they are not included when the postback is caused by the drop-down list that has the AutoPostback=”True” setting.

Partial Page Caching (Fragment Caching)
ASP.NET also supports partial caching of a page, sometimes referred to as fragment caching. To use this, you create sections of the page as user controls, containing the Control directive and with the .ascx file extension. Inside each user control you can specify an OutputCache directive (which can specify the ID of any control(s) it contains in a VaryByControl attribute, or use a VaryByParam attribute):

<%@OutputCache Duration="30" VaryByControl="MyListBox" />

However, when you specify output caching for the page hosting the user control, ASP.NET caches the whole page based on the Duration value specified in the hosting page; therefore, any user controls included on the page will be re-executed only when the page itself expires. For this reason, you should set the Duration attribute in the user control(s) to the same, or to a multiple of the Duration attribute value in the hosting page.

?
Figure 4. Partial Page Caching with a User Control: The page cache Duration is 5 seconds, while the cache Duration setting of the user control contained on the page is 10 seconds.

You can see the effects of partial page caching in the example page named output-cache-fragment.aspx. That page hosts a simple user control that contains a drop-down list box. As you can see from Figure 4, the output cache duration for the user control is ten seconds, while the duration for the hosting page is only five seconds. As you refresh the page, the execution times show that the page is re-executed every five seconds, while the user control is only re-executed every ten seconds. However, the hosting page contains the VaryByParam=”*” attribute in its OutputCache directive, and so any change to the selected value in the list box?even though it is within the user control?will cause ASP.NET to re-execute both the hosting page and the user control the next time the page is requested.

If your user control generates output that is not specific to one page, or if you use multiple instances in the same page and they do not need to be varied individually, you can reduce output cache requirements by including the Shared attribute:

   <%@OutputCache      Duration="60"      VaryByControl="MyListBox"      Shared="True" />

Alternatively, if you create user controls as Class files, you can use an attribute to specify partial caching. In VB.NET the attribute is , for example:

    _   Public Class MyControl     Inherits UserControl     ... implementation here ...   End Class

In C#, the equivalent syntax is:

   [PartialCaching(120)]   public class MyControl : UserControl {     ... implementation here ...   }

Configuring Output Caching in ASP.NET 2.0
ASP.NET 2.0 adds new capabilities to the output cache feature, as well as supporting new configuring options for ASP.NET pages and user controls in the machine.config and web.config files. There new version supports a section where you can define the operation of output caching and SQL database cache invalidation (a topic you’ll see explored in more depth in the second part of this article). This is the overall layout of the section:

                                                                                                                                        
  • The element defines the settings for memory-based caching and whether cached items will be expired automatically when the server requires the memory or disk space they are occupying (see the section “Cache Memory and Automatic Invalidation“).
  • The section specifies the global setting for output caching, whether it is enabled, and where and whether cached items can be saved to disk. Disk caching is enabled by default, and the maximum disk usage per application is set to 5 MB.
  • You use the section to define individual output caching profiles that you can inherit in your pages, so that you don’t have to edit the OutputCache directive in every page (see the section “Output Cache Profiles in ASP.NET 2.0“).
  • The section defines the databases and connection details that are used for the new automatic SQL data cache invalidation feature in ASP.NET 2.0. You’ll see more about that in Part 2 of this article.

Cache Memory and Automatic Invalidation
It’s important to remember that the ASP.NET cache will hold on to items only while there is enough memory available without compromising operation of the server. If you continue to add items to the cache after the free cache memory is exhausted, ASP.NET invalidates and removes the oldest, least used, or lowest priority items. So, just because you cached an item doesn’t guarantee it will still be there even if you specified a priority other than the default.

So it’s a good idea to test for the presence of an item before or as you retrieve it from the ASP.NET cache. You can also use cache dependencies and an event handler (as demonstrated later on) to detect when items are removed from the cache, though this is not always possible in a Web application as the events are not likely to be fired when your ASP.NET page is executing and can handle them.

Output Cache Profiles in ASP.NET 2.0
If you regularly use the same sets of values in OutputCache directives across different ASP.NET pages, you can set up cache profiles in ASP.NET version 2.0. Simply declare the profile in the section of machine.config or web.config using an element. Within the element, specify the attributes you want for this profile?for example:

?
Figure 5. Using a CacheProfile: The figure shows the results of using a CacheProfile value in the OutputCache directive.
                       

Then, in any ASP.NET page or user control, you just specify this profile in your OutputCache directive:

   <%@ OutputCache CacheProfile="MyPageProfile" %>

The example page named output-cache-profile.aspx demonstrates the use of output caching profiles. When you run the page, it looks just like the example in Figure 3. However, as you can see if you view the source for the page (use the “[view source]” link at the bottom of every example page, as shown in Figure 5), you will see that it uses the OutputCache directive shown above.

In the next part of this article, you’ll see how to capitalize on ASP.NET’s new SQL Cache invalidation support, which lets you cache and reuse data from SQL Server until that data has changed?a capability that nearly every data-driven Web application sorely needs. SQL Cache Invalidation enlists SQL Server to solve the problem of how often to refresh data stored in a database. Essentially, rather than arbitrarily deciding to refresh data on a schedule or having to run a query to check whether data has changed, you can force SQL Server to notify your application whenever specific data changes. By doing this you can ensure that your application displays the most up-to-date information possible, while simultaneously ensuring that your application queries the database as infrequently as possible. This has the effect of improving the efficiency of your application while reducing the demands on your database server.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: