ASP.NET Caching

 

Rob Howard
Microsoft Corporation

April 26, 2001

In this month's column we're going to look at another one of the new features in ASP.NET—caching. In Beta 1, there are two distinct caching features—Page output caching and the new Cache object. Beta 2 of ASP.NET will introduce several more caching features that we'll discuss in this column after Beta 2 is released.

Caching

Caching is the process of storing frequently used data, usually data that is costly to generate, for reuse. Typically this data is stored in memory since retrieving data from memory is much more efficient than retrieving the data from other locations, such as a database.

ASP.NET has several facilities for supporting caching: a Cache API for storing arbitrary data and an Output Cache used to store frequently requested Pages.

Let's discuss a quick example.

A catalog used on an e-commerce site might only change once a week. An ASP.NET Web application could be built that provides a front-end interface for that catalog, allowing customers to purchase products. When a customer is simply browsing the catalog, the system is making (in most cases) network calls to a back-end database server. The database server is also doing calculations on the data, such as a join query, and returning results.

This type of configuration is quite common, be it a catalog or some other type of commonly requested data from a database. However, the design in the above example can be improved upon. We know that the data in the database only changes once a week, and we know that there are several performance costs associated with retrieving the data:

  1. Executing of the ASP.NET code to make the database request.
  2. Use of the network for the Web server to communicate with the database server.
  3. Work done on the database server to compile and execute the query (or simply execute stored procedure).

Caching allows us to eliminate much of the above work and improve the performance and scalability of our application. We can improve performance by caching the results and serving them statically (versus dynamically on each request) and our scalability increases since we're using fewer resources to service each request.

Beta 1 of ASP.NET introduced the Cache API as well as Page output caching, which uses the Cache API. Let's discuss these two features in more detail.

Cache API

Storing frequently requested data in memory is nothing new for ASP developers. We've had two types of objects that solve this problem:

  • Session objects
  • Application objects

Session is used to store per-user data across multiple requests. There have been some changes for Session in ASP.NET but most of these changes are application level changes and don't affect the way Session is used, that is, it's still a simple key/value pair.

The Application object from ASP is also carried forward to ASP.NET, and it to remains identical in function (key/value pairs). For example, we could write the following in either ASP or ASP.NET:

Application("SomeInterestingData") = "Example data"
Response.Write(Application("SomeInterestingData")

The same semantics are used for Session. We simply name a key, in this case we used SomeInterestingData, and assign it a value, in this case the string "Example data".

ASP.NET introduces another key/value pair object—Cache. In addition to simply storing key/value pairs, Cache adds additional functionality specifically designed to store transient data:

  • Dependencies—A key added to the Cache can set up dependency relationships that can force that key to be removed from the Cache. The supported dependencies include key, file, and time.
  • Automatic Expiration—Underused items added to the Cache that have no dependencies will automatically expire if they are underused.
  • Support for Callback—Code paths can be added that will be called when an item is removed from the Cache, giving us an opportunity to update the Cache or not remove the item.

When we code our applications to use the Cache we do have to make one consideration:

Always check if the item exists in the Cache before attempting to use it.

Because the Cache will expire, items based on dependencies or under use, we must always write our code to create or retrieve the item we need if it doesn't exist within the Cache.

For example, if we wrote a function that returned a populated DataSet:

Private Function LoadDataSet() As DataSet
  Dim sqlConnection As SQLConnection
  Dim sqlAdapater As SQLDataSetCommand
  Dim datasetProducts As New DataSet()
  Dim sqlDSN As String
  Dim sqlSelect As String

  ' Connection String and Select statement
  sqlDSN = "server=localhost;uid=<user id>;pwd=<password>;database=grocertogo"
  sqlSelect = "Select * From Products"

  ' Connect
  sqlConnection = new SQLConnection(sqlDSN)
  sqlAdapater = new SQLDataSetCommand(sqlSelect, sqlConnection)

  ' Fill dataset create product table
  sqlAdapter1.FillDataSet(datasetProducts, "products")

  Return products
End Function

We could easily write code that took advantage of the Cache. Executing LoadDataSet() only when the resulting DataSet is not already in the Cache:

Public Function GetProductData() As DataSet
  If (IsNothing(Cache("ProductData")) Then
    Cache("ProductData") = LoadDataSet()

  Return Cache("ProductData")
End Function

However, this isn't really all that different from using Application. We essentially could accomplish exactly this same thing. Where this gets interesting is when we set up dependencies.

Cache Dependencies

Dependencies allow us to invalidate a particular item within the Cache based on changes to files, changes to other Cache keys, or at a fixed point in time. Let's look at each of these dependencies.

File-based Dependency

File-based dependency invalidates a particular Cache item when file(s) on disk change. For example, if instead of loading our ProductData from a database, we loaded it from an XML file:

Dim dom As XmlDocument()
dom.Load(Server.MapPath("product.xml")
Cache("ProductData") = dom

We obviously would also want to invalidate the data within the Cache if product.xml changes. Assuming product.xml is in the same directory as the requesting application:

Dim dependency as new CacheDependency(Server.MapPath("product.xml"))
Cache.Insert("ProductData", dom, dependency)

In the above code sample, we're creating an instance of a CacheDependency class, dependency, and passing in the path to the product.xml file. We then use the Insert() method of the Cache to create our ProductData key that is dependent upon the file it retrieves its data from.

Key-based Dependency

Key-based dependency invalidates a particular Cache item when another Cache item changes. For example, if our application added multiple DataSets to the Cache, such as ProductData, SalesData, and MarketingData and SalesData and MarketingData rely upon ProductData being valid. We could use a key-based dependency to invalidate SalesData and MarketingData if ProductData changes. We set up this dependency when we create the Cache entries for SalesData and MarketingData:

Dim dependency (1) As String
dependencyKey(0) = "ProductData"
Dim productDataDependency As new CacheDependency(nothing, dependencyKey)

Cache.Insert("SalesData", LoadDataSet("Sales"), productDataDependency)

In the above example code, we use the Insert() method to create a new Cache entry named SalesData passing in an instance of a CacheDependency class named productDataDependency which names the Cache key ProductData. Now, whenever ProductData changes, SalesData will be removed from the Cache.

Time-based Dependency

Time-based dependency simply expires the item at a defined point in time. Again, we would use the Insert() method of the Cache to create this dependency. We have two options for time-based dependency:

  • Absolute—Sets an absolute time; for example, current time + 10 minutes for the Cache entry to expire.
  • Sliding—Resets the time for the item in the Cache to expire on each request.

We can use the Sliding expiration option to Cache our ProductData DataSet for a maximum of 10 minutes. As long as requests for ProductData are made within a 10-minute window, the data is made valid for another 10 minutes:

' 10 minute time span
Dim span As New TimeSpan(0,10,0)
Cache.Insert("ProductData", LoadDataSet(), nothing, nothing, span)

Although this has been a relatively lightweight discussion of the Cache API, hopefully you've seen just how easy it is to use. Underneath the covers, ASP.NET makes use of the Cache API and it's time based expiration policy to support the concept of page output caching. In a future column will dig into more detail on the Cache API.

Page Output Caching

Page output caching is a feature of ASP.NET that allows for the entire contents of a given page to be stored in the Cache. We've been using the Cache API in the above example to store DataSets in the Cache. What if, rather than simply storing the DataSet, we determine that the entire page (that displays the results of the DataSet) can be cached? The benefit here is that rather than dynamically executing the ASP.NET page on each request, we serve it statically from memory. This can be a huge performance enhancement!

There is both a low-level and high-level API for working with Page Output Caching. We're only going to discuss the high-level API in the remainder of this article, but we'll discuss the low-level API in a future column (post Beta 2).

The high-level API consists of a Page directive that instructs ASP.NET to Cache the results from the page for a period of time:

<%@ OutputCache Duration="10" %>

This directive, added to the top of an ASP.NET page, simply instructs ASP.NET to cache the results of the page for 10 seconds. After 10 seconds the page will re-execute. Here's an example that uses this directive:

<%@ OutputCache Duration="10" %>
<Script runat="server">
  Public Sub Page_Load()
    span1.InnerHtml = DateTime.Now.ToString("r")
  End Sub
</Script>
<font size=6>The time is: <font color=red><span id="span1" runat="server"/></font></font>

This page simply executes on the first request, displays the time at which it was requested, and then is served from the Cache for a period of 10 seconds. For example, if we request the page at 10:30:12, we would see that value for the output for 10 seconds, after which the page is re-executed.

Summary

The Cache is a new feature for us ASP developers. However, it is both easy to use and powerful. The API is similar to Application and Session, a simple key/value pair dictionary, but unlike Application or Session, items within the Cache can expire. Unlike Application, the Cache supports file, key, and time based dependencies, as well as callbacks, which we'll save for our post Beta 2 discussion. The Cache is also used by ASP.NET for ASP.NET Page Output Caching. In a future column we'll discuss some of the new Cache features that will be appearing in Beta 2, such as Partial Page Caching and Web Service Caching, and we'll also look at some of the more advanced features like callbacks.

Rob Howard is a program manager for ASP.NET on the .NET Framework team. He spends whatever spare time he has either with his family or fly fishing in Eastern Washington.