Caching is an important concept in computing. When applied to ASP.NET, it can greatly enhance the performance of your Web applications. In this article, I will discuss some of the techniques for caching ASP.NET pages on the server side.
Output caching caches the output of a page (or portions of it) so that a page's content need not be generated every time it is loaded.
Consider the following page where a user logs in to a site and reads the latest news. The page will display the user's name and retrieve the latest news since he last logged in.
![]() |
| Figure 1: Displaying the latest news |
In a typical ASP.NET page, every time the user views the page, the Web server will have to dynamically generate the content of the page and perform the relevant database queries (which is a very expensive task to do).
Considering the fact that the page does not change for a certain period of time, it is always a good idea to cache whatever is non-static so that the page can be loaded quickly.
Let's add a new OutputCache page directive to the ASP.NET page:
<%@ OutputCache Duration="15" VaryByParam="*" %>
<%@ Page Language="vb" AutoEventWireup="false"
Codebehind="WebForm1.aspx.vb" Inherits="Caching.WebForm1"%>
The OutputCache directive specifies the various criteria for caching the page.
In this case, the page will be cached for 15 seconds. This means that if the
page is loaded and within the next 15 seconds it is refreshed, the Web server
will serve the same content without needing to dynamically regenerate the page.
This can be verified by the time displayed on the page.
The VaryByParam attribute specifies how the caching should be performed, based
on the query string supplied to the page. For example, if I used the following
URL:
http://localhost/Caching/WebForm1.aspx?name=John&newsid=12
The query string passed to the page is name=John&newsid=12. So now, if I
change the VaryByParam attribute to:
<%@ OutputCache Duration="15" VaryByParam="Name" %>
The page will be cached according to the Name key. This means that if I issue
the following two URLs, the second page will still be from the cache, as the
cache will only be refreshed if the Name value changes:
http://localhost/Caching/WebForm1.aspx?name=John&newsid=12
http://localhost/Caching/WebForm1.aspx?name=John&newsid=45
The following URLs will cause the second page to be refreshed:
http://localhost/Caching/WebForm1.aspx?name=John&newsid=12
http://localhost/Caching/WebForm1.aspx?name=Michael&newsid=12
If you want to cause the page to be regenerated if the Name and NewsID keys
change, then you simply add the NewsID key into the VaryByParam attribute:
<%@ OutputCache Duration="15" VaryByParam="Name;NewsID" %>
In this case, this is equivalent to using a "*", which means all keys will
cause the page to be regenerated:
<%@ OutputCache Duration="15" VaryByParam="*" %>
If you want to cache the page regardless of query string, you can use the
value "none":
<%@ OutputCache Duration="15" VaryByParam="none" %>
While output caching is useful, a much more powerful and flexible application would be to cache individual objects in your Web application.
For example, I have a Web application that retrieves news from a SQL Server
database and displays the news titles in a ComboBox. As users are expected to
read the news by selecting the title from the ComboBox, I want to minimize the
number of times that the application connects to SQL Server for retrieving the
news content. So I cache the DataSet containing the news.
![]() |
| Figure 2: Caching the DataSet containing the news |
The first time the page is loaded, the user has to click on the Get News button to get the news from the SQL Server:
Private Sub Button1_Click(ByVal sender As _
System.Object, _
ByVal e As System.EventArgs)_
Handles Button1.Click
Dim ds As DataSet
ds = loadData()
Dim i As Integer
DropDownList1.Items.Clear()
For i = 0 To ds.Tables("News").Rows.Count - 1
Dim item As New _
ListItem(ds.Tables("News").Rows(i).Item(2).ToString,_
ds.Tables("News").Rows(i).Item(0).ToString)
DropDownList1.Items.Add(item)
Next
End Sub
This calls the loadData() method, which loads the data from SQL server and then
displays the data in the ComboBox.
Public Function loadData() As DataSet
Dim ds As New DataSet
If Cache("News") Is Nothing Then
Dim sql As String = "SELECT * FROM NewsHeader"
Dim conn As New SqlConnection ( _
"server=localhost; uid=sa;" & _
" password=; database=News")
Dim comm As New SqlCommand(sql, conn)
Dim dataAdapter As New SqlDataAdapter(comm)
dataAdapter.Fill(ds, "News")
Cache("News") = ds
Else
ds = CType(Cache("News"), DataSet)
End If
Return ds
End Function
Note that I use a Cache object to see if it already contains a copy of the
DataSet (which contains the news). If it doesn't (Is Nothing), I will retrieve
it from the SQL Server and then store it using the Cache object, using the key
"News". Subsequent requests to the loadData() method will load the data from the cached DataSet.
When the user selects a new title from the ComboBox, the news content is retrieved from cache:
Private Sub DropDownList1_SelectedIndexChanged(ByVal _
sender As System.Object, _
ByVal e As System.EventArgs) Handles _
DropDownList1.SelectedIndexChanged
Dim index As Integer = _
DropDownList1.SelectedIndex()
Dim ds As DataSet
ds = CType(Cache("News"), DataSet)
If ds Is Nothing Then
ds = loadData()
End If
Label3.Text = _
ds.Tables("News").Rows(index).Item(1).ToString()
Label1.Text = _
ds.Tables("News").Rows(index).Item(3).ToString()
End Sub
Note that you have to perform a type conversion using the CType() method.
Using this method, I can reduce the number of connections I have to make to SQL
Server. This will improve the performance of my Web application.
What happens if there are new items in the database and you need to display them? You need to explicitly clear the cache via the Clear Cache button:
Private Sub Button2_Click(ByVal sender As _
System.Object, ByVal e As _
System.EventArgs) Handles Button2.Click
Cache.Remove("News")
End Sub
The caching technique described in the last section is useful but not brilliant. You don't expect the reader to know when to clear the cache. A better way would be to design the cache to automatically refresh itself when some external events happen. Let me extend the example.
Suppose our application loads a text file containing the header (that changes daily) for the day's news:
![]() |
| Figure 3: Displaying the header |
Assuming that the news changes daily, so it is logical that whenever the
header changes, the data in the cache should be refreshed as well. So there is
a dependence on the header.txt file. To express the cache's dependency on the
file, use the System.Web.Caching.CacheDependency class.
Hence, rewriting my loadData() method:
Public Function loadData() As DataSet
Dim ds As New DataSet
Dim header As String
Dim file As New _
System.IO.StreamReader(Server.MapPath("header.txt"))
header = file.ReadLine
file.Close()
lblHeader.Text = header
If Cache("News") Is Nothing Then
Dim sql As String = "SELECT * FROM NewsHeader"
Dim conn As New _
SqlConnection("server=localhost; " & _
"uid=sa; password=; database=News")
Dim comm As New SqlCommand(sql, conn)
Dim dataAdapter As New SqlDataAdapter(comm)
dataAdapter.Fill(ds, "News")
Dim depends As New _
System.Web.Caching.CacheDependency _
(Server.MapPath("Header.txt"))
Cache.Insert("News", ds, depends)
Else
ds = CType(Cache("News"), DataSet)
End If
Return ds
End Function
Notice that I have used the insert() method of the Cache class to insert the
DataSet and file dependency:
Cache.Insert("News", ds, depends)
From now on, if the content of the header.txt file is changed, the content in the cache would be cleared.
Another technique for caching is based on time. For example, the cache can expire on a certain date, or it will only be available for a certain period of time. There are two ways in which you can use time-based caching:
For example, if you have:
'===Absolute expiration===
Cache.Insert("News", ds, Nothing, _
DateTime.Now.AddMinutes(2), _
Cache.NoSlidingExpiration)
The cache is set to expire exactly two minutes after the user has retrieved the data.
For sliding expiration, you can use:
'===Sliding expiration===
Cache.Insert("News", ds, Nothing, _
Cache.NoAbsoluteExpiration, _
TimeSpan.FromMinutes(1))
This will cause the cache to be cleared if the user does not reload the page within one minute. If the user reloads the page within the one-minute time frame, the data in the cache will be valid for another minute.
As in life, anything that is good comes with a price. Enabling caching in ASP.NET causes the cache content to be stored in the Web server's memory. Caching takes up valuable system resources and can easily eat up all of your available memory.
When server memory runs out, the contents of your cache will be evicted. The criteria for cache eviction is based on priority, which you can optionally set when adding data to your cache:
Cache.Insert("News", ds, Nothing, _
Cache.NoAbsoluteExpiration, _
TimeSpan.FromMinutes(1), _
System.Web.Caching.CacheItemPriority.High, _
Nothing)
There are seven levels of priority:
NotRemovable, High, AboveNormal, Default, Normal, BelowNormal, and Low.
Caching of Web pages is an effective way of increasing performance while minimizing the use of precious server resources. Choosing the appropriate level for caching data is important for balancing caching versus memory usage. One of the most effective strategies to good Web application performance is to cache data only when necessary.
Wei-Meng Lee (Microsoft MVP) http://weimenglee.blogspot.com is a technologist and founder of Developer Learning Solutions http://www.developerlearningsolutions.com, a technology company specializing in hands-on training on the latest Microsoft technologies.
Return to ONDotnet.com
Copyright © 2009 O'Reilly Media, Inc.