Use Caching to Enhance Performance
Robust Web apps must provide fast response times. Learn how to use the Cache object to place commonly requested resources into an area in memory where they can be accessed quickly.
- By Jonathan Lurie
- May 1, 2003
Technology Toolbox: VB.NET, ASP.NET
Enterprise Web applications must be capable of providing good response times, despite having thousands of concurrent users. Server-side caching is one of the most effective means of achieving high levels of performance. I'll explain how to take advantage of the caching functionality within the .NET Framework.
Caching entails placing commonly requested resources, such as product lists and files, into an area in memory where they are accessible quickly, thereby optimizing response times. The performance benefit is significant, because the resource doesn't need to be re-created or retrieved from the database the next time it's requested (see the sidebar, "Don't Confuse Performance With Scalability"). Consider a product list with hundreds of items in it. The application's response time will be relatively slow if it must hit the database every time a user requests the list. This is unnecessary when the product list doesn't change between requests (see Figure 1).
ASP.NET has two caching techniques you must be familiar with in order to build enterprise-ready applications: the Cache object and the output cache. You can use the Cache object to store individual values, such as a file or a computed number. You can use the output cache to store entire ASP.NET pages and user controls. These techniques are significantly different from each other and require individual attention. I'll focus only on the Cache object.
The .NET Framework exposes a class named Cache, which resides in the System.Web.Caching namespace. ASP.NET creates one cache per Web application, and the cache isn't shared across multiple applications. You should use the Cache object for caching, even though many developers use the Application object instead. The Cache object is quite similar to the Application object, in that they both use a key-value pair to store variables. However, the Cache object is tailored for caching, because it supports object expiration from the cache. This is important because as you place more and more resources into the cache, your application begins to consume more and more memory. This has an adverse effect on performance and can even negate any benefit you derive from caching. Expiring objects removes them from the cache. Although no caching solution can implement all available algorithms for determining what should stay and what should go, Microsoft has done a good job of providing you with several alternatives. The Cache object is also different from the Application object in that it implements automatic locking, which means you don't need to worry about concurrent access; you must use an Application.Lock() with the Application object.
The first step in caching is to put things into the cache and retrieve them:
Cache("key") = value
You can place the preceding code in any ASP.NET code-behind page, because the Cache object is a property of the System.Web.UI.Page that all ASP.NET pages derive from. Another advanced technique is to prepopulate the cache when the application starts. Do this by placing the preceding code in the Application_Start method of Global.asax. However, the Global class doesn't derive from System.Web.UI.Page, but rather from System.Web.HttpApplication.
Use Cache in the Global Class
Fortunately, the Cache object is accessible as a property of the HttpContext. HttpContext is in turn a property of System.Web.HttpApplication. Use this code if you want to work with Cache in the Global class (Global.asax):
Context.Cache("key") = value
The key is of utmost importance because it serves to identify the object you're placing into the cache. For example, the Alphabet key in this code identifies a long string to be placed in the cache:
Dim s as new _
String("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
Cache.item("Alphabet") = s
You then use the key to retrieve the long string from the cache when necessary. Once you place the object into the cache, you can use this code to extract it easily:
Dim s as String
s = Cache.item("key")
' or using the default property, it
' could be abbreviated to s = Cache
' ("key")
Always check to see whether the cache returns anything other than Nothing. Nothing is returned if the resource you're requesting isn't found in the cache.
If you need more control over the cache, you can use the Insert or the Add method to populate it. The two methods are similar, but have one important difference: The Add method ignores any calls to it that try to add an item with a key that's being used in the cache already, whereas the Insert method overwrites whatever is in the cache. Moreover, the Add method has no overloaded versions, and the Insert method does. I find it easier to use the Insert method, because it doesn't require you to call the Remove method if something needs to be overwritten. Of course, each method has its place.
Now I'll dig a little deeper and examine the Insert method, which has this signature:
Overloads Public Sub Insert( _
ByVal key As String, ByVal value _
As Object, ByVal dependencies As _
CacheDependency, ByVal _
absoluteExpiration As DateTime, _
ByVal slidingExpiration As _
TimeSpan, ByVal priority As _
CacheItemPriority, ByVal onRemoveCallback As _
CacheItemRemovedCallback)
The first two parameters in the preceding codekey and valueare straightforward. The third parameter sets up a dependency for the resource you're caching. The dependency can be files, directories, or keys to other objects in your application's cache. When the dependency is modified, the cached resource is expired from the cache. This is useful if you load your product list from an XML file, because you want to expire the old product list from the cache when the XML file changes. Simply pass in a null value if your resource has no dependencies.
Set Expiration Times
The third parameter sets up a fixed DateTime after which the cached resource is expired. The fourth parameter sets the interval between the time the cached resource was last accessed and when it is expired. For example, setting the value to TimeSpan.FromHours (1) means the cached resource will expire and be removed from the cache one hour after it was last accessed. If you use this fourth parameter, then you should set the third parameter to DateTime.MaxValue. (You don't need to remember this, because the compiler gives you an error unless you do it.)
The fifth parameter defines a relative importance within the cache so that when the cache expires objects, it expires those of a lower importance than those of a higher importance. You use an enumeration called System.Web.Caching.CacheItemPriority to set this value. Objects that are more time-consuming to re-create should have a higher importance. The last parameter allows you to pass in a delegate, which is a callback to a method that will be called when the object is expired. This is useful if you want to repopulate the cache with a newer version when an object is expired. If you don't wish to use this, simply pass in a null/nothing. A discussion of delegates is beyond this article's scope.
Now try a simple example. In a new Web project, create a file called Products.xml that contains some product names. Keep this file in the same virtual directory as the Web application. Next, drop a DataGrid onto WebForm1.aspx, then place this code in the Load method of the WebForm1.aspx.cs class:
Dim ds As DataSet = Cache("ProductList")
If (ds Is Nothing) Then
ds = New DataSet()
ds.ReadXml(Server.MapPath("Products.xml"))
Cache.Insert("ProductList", ds, New _
System.Web.Caching.CacheDependency( _
Server.MapPath("Products.xml")), _
DateTime.MaxValue, _
TimeSpan.FromHours(1), _
System.Web.Caching. _
CacheItemPriority.AboveNormal, Nothing)
End If
MyClass.DataGrid1.DataSource = ds
MyClass.DataGrid1.DataBind()
The preceding code attempts to find a dataset in the cache. If it doesn't find it there, it loads the dataset from the Products.xml file.
After loading the dataset, you insert it into the cache with the stipulation that it should be removed one hour after its last access or if the Products.xml file is modified. Although you could use other techniques to populate the cachesuch as Application_Start and delegatesthis is a valid technique. The Server.MapPath is useful because it returns the directory path for the current ASP.NET Web application.
Test the preceding code by putting a breakpoint on the line that loads the dataset from the file. Now, run the project. Notice that the breakpoint is hit the first time the page loads, but not after you refresh the page. This happens because the product list is found in the cache. Now, change a product name in Products.xml. The cache removes the product list once you save the file, because you configured a dependency.
If you're patient, you can try waiting an hour to see if your breakpoint gets hit, but more than likely you'll get a session-expired error if you use the same browser. By default, sessions expire after 20 minutes, just as they do with ASP. You can change this in Web.config, but it's much easier to change the TimeSpan so you can test the sliding expiration scheme.
You're on your way to faster application response time. However, before you consider yourself a caching pro, you should do some research on the other caching technique I mentioned at the beginning of this articlethe output cache.