If someone put a gun to my head and told me that I had 5 minutes to improve the performance of a website, then I would immediately think of caching. By taking advantage of caching, you can dramatically improve the performance of your web applications.
The slowest operation that you can perform in an ASP.NET page is database access. Opening a database connection and retrieving data is a slow operation. The best way to improve the performance of your data access code is not to access the database at all.
By taking advantage of caching, you can cache your database records in memory. Retrieving data from a database is dog slow. Retrieving data from the cache, on the other hand, is lightning fast.
In this chapter, you learn about the different caching mechanisms supported by the ASP.NET Framework. The ASP.NET 2.0 Framework provides you with an (almost) overwhelming number of caching options. I attempt to clarify all these caching options over the course of this chapter.
In the final section of this chapter, you learn how to use SQL Cache Dependencies. A SQL Cache Dependency enables you to reload cached data automatically when data changes in a database table. You learn how to use both polling and push SQL Cache Dependencies.
The ASP.NET 2.0 Framework supports the following types of caching:
Page Output Caching enables you to cache the entire rendered contents of a page in memory (everything that you see when you select View Source in your web browser). The next time that any user requests the same page, the page is retrieved from the cache.
Page Output Caching caches an entire page. In some situations, this might create problems. For example, if you want to display different banner advertisements randomly in a page, and you cache the entire page, then the same banner advertisement is displayed with each page request.
The AdRotator
control included in the ASP.NET Framework takes advantage of a feature called post-cache substitution to randomly display different advertisements even when a page is cached. Post-cache substitution is described later in this chapter.
Partial Page Caching enables you to get around this problem by enabling you to cache only particular regions of a page. By taking advantage of Partial Page Caching, you can apply different caching policies to different areas of a page.
You use DataSource Caching with the different ASP.NET DataSource
controls such as the SqlDataSource
and ObjectDataSource
controls. When you enable caching with a DataSource
control, the DataSource
control caches the data that it represents.
Finally, Data Caching is the fundamental caching mechanism. Behind the scenes, all the other types of caching use Data Caching. You can use Data Caching to cache arbitrary objects in memory. For example, you can use Data Caching to cache a DataSet across multiple pages in a web application.
In the following sections, you learn how to use each of these different types of caching in detail.
When configuring and debugging caching, having a tool that enables you to monitor the HTTP traffic between web server and browser is extremely helpful. You can download the free Fiddler tool, which enables you to view the raw request and response HTTP traffic, from http://www.FiddlerTool.com.
You enable Page Output Caching by adding an <%@ OutputCache %>
directive to a page. For example, the page in Listing 23.1 caches its contents for 15 seconds.
Example 23.1. CachePageOutput.aspx
<%@ Page Language="VB" %> <%@ OutputCache Duration="15" VaryByParam="none" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Sub Page_Load() lblTime.Text = DateTime.Now.ToString("T") End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Cache Page Output</title> </head> <body> <form id="form1" runat="server"> <div> <asp:Label id="lblTime" Runat="server" /> </div> </form> </body> </html>
The page in Listing 23.1 displays the current server time in a Label
control. The page also includes an <%@ OutputCache %>
directive. If you refresh the page multiple times, you will notice that the time is not updated until at least 15 seconds have passed.
When you cache a page, the contents of the page are not regenerated each time you request the page. The .NET class that corresponds to the page is not executed with each page request. The rendered contents of the page are cached for every user that requests the page.
The page is cached in multiple locations. By default, the page is cached on the browser, any proxy servers, and on the web server.
In Listing 23.1, the page is cached for 15 seconds. You can assign a much larger number to the duration attribute. For example, if you assign the value 86400 to the duration parameter, then the page is cached for a day.
There is no guarantee that a page will be cached for the amount of time that you specify. When server memory resources become low, items are automatically evicted from the cache.
Imagine that you need to create a separate master and details page. The master page displays a list of movies. When you click a movie title, the details page displays detailed information on the movie selected.
When you create a master/details page, you typically pass a query string parameter between the master and details page to indicate the particular movie to display in the details page. If you cache the output of the details page, however, then everyone will see the first movie selected.
You can get around this problem by using the VaryByParam
attribute. The VaryByParam
attribute causes a new instance of a page to be cached when a different parameter is passed to the page. (The parameter can be either a query string parameter or a form parameter.)
For example, the page in Listing 23.2 contains a master page that displays a list of movie titles as links.
Example 23.2. Master.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Master</title> </head> <body> <form id="form1" runat="server"> <div> <asp:GridView id="grdMovies" DataSourceID="srcMovies" AutoGenerateColumns="false" ShowHeader="false" GridLines="none" Runat="server"> <Columns> <asp:HyperLinkField DataTextField="Title" DataNavigateUrlFields="Id" DataNavigateUrlFormatString="~/Details.aspx?id={0}" /> </Columns> </asp:GridView> <asp:SqlDataSource id="srcMovies" ConnectionString="<%$ ConnectionStrings:Movies %>" SelectCommand="SELECT Id,Title FROM Movies" Runat="server" /> </div> </form> </body> </html>
If you hover your mouse over the links displayed in Listing 23.2, you can see the query string parameter passed by each link in the browser status bar (see Figure 23.1). For example, the first movie link includes a query string parameter with the value 1
, the second link includes a query string parameter with the value 2
, and so on. When you click a movie link, this query string parameter is passed to the details page in Listing 23.3.
Example 23.3. Details.aspx
<%@ Page Language="VB" %> <%@ OutputCache Duration="3600" VaryByParam="id" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Details</title> </head> <body> <form id="form1" runat="server"> <div> <%= DateTime.Now.ToString("T") %> <hr /> <asp:DetailsView id="dtlMovie" DataSourceID="srcMovies" Runat="server" /> <asp:SqlDataSource id="srcMovies" ConnectionString="<%$ ConnectionStrings:Movies %>" SelectCommand="SELECT * FROM Movies WHERE Id=@Id" Runat="server"> <SelectParameters> <asp:QueryStringParameter Name="Id" Type="int32" QueryStringField="Id" /> </SelectParameters> </asp:SqlDataSource> </div> </form> </body> </html>
The page in Listing 23.3 uses a DetailsView
to display detailed information on the movie selected from the master page (see Figure 23.2). The DetailsView
is bound to a SqlDataSource
control that includes a QueryStringParameter SELECT
parameter that represents the id
query string parameter.
Notice that the Details.aspx
page includes an <%@ OutputCache %>
directive. The VaryByParam
attribute in the <%@ OutputCache %>
directive has the value id
. If you request the Details.aspx
page with a different value for the id
query string parameter, then a different cached version of the page is created.
It is important to understand that using VaryByParam
results in more caching and not less caching. Each time a different id
parameter is passed to the Details.aspx
page, another version of the same page is cached in memory.
The Details.aspx
page displays the current time. Notice that the time does not change when you request the Details.aspx
page with the same query string parameter.
You can assign two special values to the VaryByParam
attribute:
none
—. Causes any query string or form parameters to be ignored. Only one version of the page is cached.
*
—. Causes a new cached version of the page to be created whenever there is a change in any query string or form parameter passed to the page.
You also can assign a semicolon-delimited list of parameters to the VaryByParam
attribute when you want to create different cached versions of a page, depending on the values of more than one parameter.
The VaryByControl
attribute enables you to generate different cached versions of a page depending on the value of a particular control in the page. This attribute is useful when you need to create a single-page Master/Details form.
For example, the page in Listing 23.4 contains both a DropDownList
and GridView
control. When you select a new movie category from the DropDownList
, a list of matching movies is displayed in the GridView
(see Figure 23.3).
Example 23.4. MasterDetails.aspx
<%@ Page Language="VB" %> <%@ OutputCache Duration="3600" VaryByControl="dropCategories" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Master/Details</title> </head> <body> <form id="form1" runat="server"> <div> <%= DateTime.Now.ToString("T") %> <hr /> <asp:DropDownList id="dropCategories" DataSourceID="srcCategories" DataTextField="Name" DataValueField="Id" Runat="server" /> <asp:Button id="btnSelect" Text="Select" Runat="server" /> <br /><br /> <asp:GridView id="grdMovies" DataSourceID="srcMovies" GridLines="none" Runat="server" /> <asp:SqlDataSource id="srcCategories" ConnectionString="<%$ ConnectionStrings:Movies %>" SelectCommand="SELECT Id,Name FROM MovieCategories" Runat="server" /> <asp:SqlDataSource id="srcMovies" ConnectionString="<%$ ConnectionStrings:Movies %>" SelectCommand="SELECT Title,Director FROM Movies WHERE CategoryId=@CategoryId" Runat="server"> <SelectParameters> <asp:ControlParameter Name="CategoryId" ControlID="dropCategories" /> </SelectParameters> </asp:SqlDataSource> </div> </form> </body> </html>
The page in Listing 23.4 contains an <%@ OutputCache %>
directive. This directive includes a VaryByControl
parameter. The ID of the DropDownList
control is assigned to this parameter.
If you neglected to add the VaryByControl
attribute, then the same list of movies would be displayed in the GridView
regardless of which movie category is selected. The VaryByControl
attribute causes different cached versions of the page to be created whenever the DropDownList
represents a different value.
Another option is to use the VaryByHeader
attribute to create different cached versions of a page when the value of a particular browser header changes. Several standard browser headers are transmitted with each page request, including
Accept-Language
—. Represents a prioritized list of languages that represent the preferred human language of the user making the request.
User-Agent
—. Represents the type of device making the request.
Cookie
—. Represents the browser cookies created in the current domain.
For example, the page in Listing 23.5 includes an <%@ OutputCache %>
directive that has a VaryByHeader
attribute with the value User-Agent
. When you request the page with different browsers, different versions of the page are cached.
Example 23.5. VaryByHeader.aspx
<%@ Page Language="VB" %> <%@ OutputCache Duration="3600" VaryByParam="none" VaryByHeader="User-Agent" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Vary By Header</title> </head> <body> <form id="form1" runat="server"> <div> <%= DateTime.Now.ToString("T") %> <hr /> <%= Request.UserAgent %> </div> </form> </body> </html>
I don’t recommend using the VaryByHeader
attribute with the User-Agent
header. The problem with this attribute is that it is too fine-grained. If there is any variation in the User-Agent
header, then a different cached version of a page is generated.
Consider the User-Agent
header sent by the Internet Explorer browser installed on my computer. It looks like this:
Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)
This header includes the major and minor version of the browser, the platform (Windows XP), a string indicating that Service Pack 2 has been installed (SV1), and the versions of the .NET framework installed on my machine. If someone else requests the same page with a slight difference in the User-Agent
header, then a different cached version of the page is generated. In other words, the web server must do more work rather than less, which defeats the point of caching.
Instead of using the VaryByHeader
attribute, I recommend that you use the VaryByCustom
attribute described in the next two sections.
A better way to create different cached versions of a page that depend on the type of browser being used to request the page is to use the VaryByCustom
attribute. This attribute accepts the special value browser
. When VaryByCustom
has the value browser
, only two attributes of the browser are considered important: the type of browser and its major version.
For example, a page request from Internet Explorer results in a different cached version of the page than does one from FireFox. A page request from Internet Explorer 5 rather than Internet Explorer 6.5 also results in a different cached version. Any other variations in the User-Agent
header are ignored.
The page in Listing 23.6 illustrates how you can use the VaryByCustom
attribute with the value browser. The page displays the current time and the value of the User-Agent
header. If you request the page with Internet Explorer and request the page with Firefox, different cached versions of the page are created.
Example 23.6. VaryByBrowser.aspx
<%@ Page Language="VB" %> <%@ OutputCache Duration="3600" VaryByParam="none" VaryByCustom="browser" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Vary By Browser</title> </head> <body> <form id="form1" runat="server"> <div> <%= DateTime.Now.ToString("T") %> <hr /> <%= Request.UserAgent %> </div> </form> </body> </html>
The VaryByCustom
attribute is named the VaryByCustom
attribute for a reason. You can specify a custom function that determines when a different cached version of a page is generated.
You can use any criteria that you please with the custom function. You can create different cached versions of a page depending on the browser minor version, the browser DOM support, the time of day, or even the weather.
You create the custom function in the Global.asax
file by overriding the GetVaryByCustomString()
method. For example, the Global.asax
file in Listing 23.7 illustrates how you can override the GetVaryByCustomString()
method to create different cached versions of a page depending on a particular feature of a browser. If the VaryByCustom
attribute in a page has the value css
, then the function returns a string representing whether or not the current browser supports Cascading Style Sheets.
Example 23.7. Global.asax
<%@ Application Language="VB" %> <script runat="server"> Public Overrides Function GetVaryByCustomString(ByVal context As HttpContext, ByVal custom As String) As String If String.Compare(custom, "css") = 0 Then Return Request.Browser.SupportsCss.ToString() End If Return MyBase.GetVaryByCustomString(context, custom) End Function </script>
The page in Listing 23.8 displays one of two Panel
controls. The first Panel
contains text formatted with a Cascading Style Sheet style and the second Panel
contains text formatted with (outdated) HTML. Depending on whether a browser supports CSS, either the first or second Panel
is displayed.
Example 23.8. VaryByCustom.aspx
<%@ Page Language="VB" %> <%@ OutputCache Duration="3600" VaryByParam="none" VaryByCustom="css" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Sub Page_Load() If Request.Browser.SupportsCss Then pnlCss.Visible = True Else pnlNotCss.Visible = True End If End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Vary By Custom</title> </head> <body> <form id="form1" runat="server"> <div> <asp:Panel id="pnlCss" Visible="false" Runat="server"> <span style="font-weight:bold">Hello!</span> </asp:Panel> <asp:Panel id="pnlNotCss" Visible="false" Runat="server"> <b>Hello!</b> </asp:Panel> </div> </form> </body> </html>
You can detect browser capabilities by using the HttpBrowserCapabilities
class exposed by the Request.Browser
property. This class includes dozens of properties that enable you to detect the features of the browser being used to request a page.
The page contains an <%@ OutputCache %>
directive with a VaryByCustom
attribute set to the value css
. Two different cached versions of the same page are generated: one version for CSS browsers and another version for non-CSS browsers.
You can use the Location
attribute of the <%@ OutputCache %>
directive to specify where a page is cached. This attribute accepts the following values:
Any
—. The page is cached on the browser, proxy servers, and web server (the default value).
Client
—. The page is cached only on the browser.
Downstream
—. The page is cached on the browser and any proxy servers, but not the web server.
None
—. The page is not cached.
Server
—. The page is cached on the web server, but not the browser or any proxy servers.
ServerAndClient
—. The page is cached on the browser and web server, but not on any proxy servers.
By default, when you use Page Output Caching, a page is cached in three locations: the web server, any proxy servers, and the browser. There are situations in which you might need to modify this default behavior. For example, if you are caching private information, then you don’t want to cache the information on the web server or any proxy servers.
When Windows authentication is enabled in the web configuration file (the default), the Cache-Control
header is automatically set to the value private
, and the setting of the Location
attribute is ignored.
For example, the page in Listing 23.9 caches a page only on the browser and not on any proxy servers or the web server. The page displays a random number (see Figure 23.4).
Example 23.9. CacheLocation.aspx
<%@ Page Language="VB" %> <%@ OutputCache Duration="3600" VaryByParam="none" Location="Client" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Sub Page_Load() Dim rnd As New Random() lblRandom.Text = rnd.Next(10).ToString() End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Cache Location</title> </head> <body> <form id="form1" runat="server"> <div> <%= DateTime.Now.ToString("T") %> <hr /> Your random number is: <asp:Label id="lblRandom" Runat="server" /> <br /><br /> <a href="CacheLocation.aspx">Request Page</a> </div> </form> </body> </html>
If you click the link located at the bottom of the page in Listing 23.9 and request the same page, then the page is retrieved from the browser cache and the same random number is displayed. If you reload the page in your web browser by clicking your browser’s Reload button, then the page is reloaded from the web server and a new random number is displayed. The page is cached only in your local browser cache and nowhere else.
You can create a dependency between a cached page and a file (or set of files) on your hard drive. When the file is modified, the cached page is automatically dropped and regenerated with the next page request.
For example, the page in Listing 23.10 displays the contents of an XML file in a GridView
. The page is cached until the XML file is modified (see Figure 23.5).
Example 23.10. OutputCacheFileDependency.aspx
<%@ Page Language="VB" %> <%@ OutputCache Duration="9999" VaryByParam="none" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Sub Page_Load() Response.AddFileDependency(MapPath("Movies.xml")) End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Output Cache File Dependency</title> </head> <body> <form id="form1" runat="server"> <div> <%= DateTime.Now.ToString("T") %> <hr /> <asp:GridView id="grdMovies" DataSourceID="srcMovies" Runat="server" /> <asp:XmlDataSource id="srcMovies" DataFile="Movies.xml" Runat="server" /> </div> </form> </body> </html>
The page in Listing 23.10 displays the current time. Notice that the time does not change until you modify the Movies.xml
XML file.
The page in Listing 23.10 uses the Response.AddFileDependency()
method to create a dependency between the cached page and a single file on disk. If you need to create a dependency on multiple files, then you can use the AddFileDependencies()
method instead.
You can remove a page from the cache programmatically by using the Response.RemoveOutputCacheItem()
method. For example, imagine that you are caching a page that displays a list of products. Furthermore, imagine that your website includes a separate page for adding a new product. In that case, you’ll want to remove the first page programmatically from the cache when the list of products is updated.
The page in Listing 23.11 uses a GridView control to display a list of movies. The page is cached for one hour with an <%@ OutputCache %>
directive.
Example 23.11. MovieList.aspx
<%@ Page Language="VB" %> <%@ OutputCache Duration="3600" VaryByParam="none" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Movie List</title> </head> <body> <form id="form1" runat="server"> <div> <%= DateTime.Now.ToString("T") %> <hr /> <asp:GridView id="grdMovies" DataSourceID="srcMovies" Runat="server" /> <asp:SqlDataSource id="srcMovies" ConnectionString="<%$ ConnectionStrings:Movies %>" SelectCommand="SELECT Title, Director FROM Movies" Runat="server" /> <br /><br /> <a href="AddMovie.aspx">Add Movie</a> </div> </form> </body> </html>
The page in Listing 23.12 contains a DetailsView
control that enables you to add a new movie. When you insert a new movie into the database, the Response.RemoveOutputCacheItem()
method is called to remove the MovieList.aspx
page from the cache. Because this method accepts only a “virtual absolute” path, the Page.ResolveUrl()
method is used to convert the tilde into the application root path.
Example 23.12. AddMovie.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Sub dtlMovie_ItemInserted(sender as object, e as DetailsViewInsertedEventArgs) HttpResponse.RemoveOutputCacheItem(Page.ResolveUrl("~/MovieList.aspx")) Response.Redirect("~/MovieList.aspx") End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Add Movie</title> </head> <body> <form id="form1" runat="server"> <div> <h1>Add Movie</h1> <asp:DetailsView id="dtlMovie" DefaultMode="Insert" DataSourceID="srcMovies" AutoGenerateRows="false" AutoGenerateInsertButton="true" Runat="server" OnItemInserted="dtlMovie_ItemInserted"> <Fields> <asp:BoundField DataField="Title" HeaderText="Title:" /> <asp:BoundField DataField="Director" HeaderText="Director:" /> </Fields> </asp:DetailsView> <asp:SqlDataSource id="srcMovies" ConnectionString="<%$ ConnectionStrings:Movies %>" InsertCommand="INSERT Movies (Title, Director) VALUES (@Title, @Director)" Runat="server" /> </div> </form> </body> </html>
The Response.RemoveOutputCacheItem()
method enables you to remove only one page from the cache at a time. If you need to remove multiple pages, then you can create something called a key dependency. A key dependency enables you to create a dependency between one item in the cache and another item. When the second item is removed from the cache, the first item is removed automatically.
For example, the page in Listing 23.13 also displays a list of movies. However, the page is cached with a dependency on an item in the cache named Movies.
Example 23.13. MovieListKeyDependency.aspx
<%@ Page Language="VB" %> <%@ OutputCache Duration="3600" VaryByParam="none" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) Cache.Insert("Movies", DateTime.Now) Response.AddCacheItemDependency("Movies") End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Movie List Key Dependency</title> </head> <body> <form id="form1" runat="server"> <div> <%= DateTime.Now.ToString("T") %> <hr /> <asp:GridView id="grdMovies" DataSourceID="srcMovies" Runat="server" /> <asp:SqlDataSource id="srcMovies" ConnectionString="<%$ ConnectionStrings:Movies %>" SelectCommand="SELECT Title, Director FROM Movies" Runat="server" /> <br /><br /> <a href="AddMovieKeyDependency.aspx">Add Movie</a> </div> </form> </body> </html>
The page in Listing 23.14 enables you to add a new movie to the Movies database table. When the new movie is inserted, the Movies item is removed and any pages that are dependent on the Movies item are dropped from the cache automatically.
Example 23.14. AddMovieKeyDependency.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Sub dtlMovie_ItemInserted(ByVal sender As Object, ByVal e As DetailsViewInsertedEventArgs) Cache.Remove("Movies") Response.Redirect("~/MovieListKeyDependency.aspx") End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Add Movie Key Dependency</title> </head> <body> <form id="form1" runat="server"> <div> <h1>Add Movie</h1> <asp:DetailsView id="dtlMovie" DefaultMode="Insert" DataSourceID="srcMovies" AutoGenerateRows="false" AutoGenerateInsertButton="true" Runat="server" OnItemInserted="dtlMovie_ItemInserted"> <Fields> <asp:BoundField DataField="Title" HeaderText="Title:" /> <asp:BoundField DataField="Director" HeaderText="Director:" /> </Fields> </asp:DetailsView> <asp:SqlDataSource id="srcMovies" ConnectionString="<%$ ConnectionStrings:Movies %>" InsertCommand="INSERT Movies (Title, Director) VALUES (@Title, @Director)" Runat="server" /> </div> </form> </body> </html>
If you need more control over how the ASP.NET Framework caches pages, then you can work directly with the HttpCachePolicy
class. This class is exposed by the Response.Cache
property.
The HttpCachePolicy
class includes properties and methods that enable you to perform programmatically all the tasks that you can perform with the <%@ OutputCache %>
directive. You also can use the methods of this class to manipulate the HTTP cache headers that are sent to proxy servers and browsers.
This class supports the following properties:
VaryByHeaders
—. Gets the list of headers that are used to vary cache output.
VaryByParams
—. Gets the list of query string and form parameters that are used to vary cache output.
The HttpCachePolicy
class also supports the following methods:
AddValidationCallback
—. Enables you to create a method that is called automatically before a page is retrieved from the cache.
AppendCacheExtension
—. Enables you to add custom text to the Cache-Control
HTTP header.
SetAllowResponseInBrowserHistory
—. Enables you to prevent a page from appearing in the browser history cache.
SetCacheability
—. Enables you to set the Cache-Control
header and the server cache.
SetETag
—. Enables you to set the ETag
HTTP header.
SetETagFromFileDependencies
—. Enables you to set the ETag
HTTP header from the time stamps of all files on which the page is dependent.
SetExpires
—. Enables you to set the Expires
HTTP header.
SetLastModified
—. Enables you to set the Last-Modified
HTTP header.
SetLastModifiedFromFileDependencies
—. Enables you to set the Last-Modified
HTTP header from the time stamps of all files on which the page is dependent.
SetMaxAge
—. Enables you to set the Cache-Control:max-age
HTTP header.
SetNoServerCaching
—. Enables you to disable web server caching.
SetNoStore
—. Enables you to send a Cache-Control:no-store
HTTP header.
SetNoTransform
—. Enables you to send a Cache-Control:no-transform
HTTP header.
SetOmitVaryStar
—. Enables you to not send the vary:*
HTTP header.
SetProxyMaxAge
—. Enables you to set the Cache-Control:s-maxage
HTTP header.
SetRevalidation
—. Enables you to set the Cache-Control
HTTP header to either must-revalidation
or proxy-revalidate
.
SetSlidingExpiration
—. Enables you to set a sliding expiration policy.
SetValidUntilExpires
—. Enables you to prevent a page from expiring from the web server cache when a browser sends a Cache-Control
header.
SetVaryByCustom
—. Enables you to set the string passed to the GetVaryByCustomString()
method in the Global.asax
file.
For example, the page in Listing 23.15 programmatically places a page in the output cache. The page is cached on the browser, proxy servers, and web server for 15 seconds.
Example 23.15. ProgramOutputCache.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Sub Page_Load() Response.Cache.SetCacheability(HttpCacheability.Public) Response.Cache.SetExpires(DateTime.Now.AddSeconds(15)) Response.Cache.SetMaxAge(TimeSpan.FromSeconds(15)) Response.Cache.SetValidUntilExpires(True) Response.Cache.SetLastModified(DateTime.Now) Response.Cache.SetOmitVaryStar(True) End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Program OutputCache</title> </head> <body> <form id="form1" runat="server"> <div> <%= DateTime.Now.ToString("T") %> <br /><br /> <a href="ProgramOutputCache.aspx">Request this Page</a> </div> </form> </body> </html>
Clearly, it is more difficult to enable Page Output Caching programmatically than declaratively. You need to call many methods to cache a page in the same way as you can with a single <%@ OutputCache %>
directive. However, programmatically manipulating the cache provides you with fine-grained control over the HTTP headers sent to proxy servers and browsers.
Instead of configuring Page Output Caching for each page in an application, you can configure Page Output Caching in a web configuration file and apply the settings to multiple pages. You can create something called a Cache Profile. Creating Cache Profiles makes your website easier to manage.
For example, the web configuration file in Listing 23.16 contains the definition for a Cache Profile named Cache1Hour
that caches a page for one hour.
The page in Listing 23.17 uses the Cache1Hour
profile. This profile is set with the <%@ OutputCache %>
directive’s CacheProfile
attribute.
Example 23.17. OutputCacheProfile.aspx
<%@ Page Language="VB" %> <%@ OutputCache CacheProfile="Cache1Hour" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Output Cache Profile</title> </head> <body> <form id="form1" runat="server"> <div> <%= DateTime.Now.ToString("T") %> </div> </form> </body> </html>
You can set the same caching properties in a Cache Profile as you can set in an individual page’s <%@ OutputCache %>
directive. For example, you can set varyByParam
, varyByControl
, varyByHeader
, and even varyByCustom
attributes in a Cache Profile.
In the previous section of this chapter, you learned how to cache the entire output of a page. In this section, you learn how to take advantage of Partial Page Caching to cache particular regions of a page.
Partial Page Caching makes sense when a page contains both dynamic and static content. For example, you might want to cache a set of database records displayed in a page, but not cache a random list of news items displayed in the same page.
In this section, you learn about two methods for enabling Partial Page Caching. You can use post-cache substitution to cache an entire page except for a particular region. You can use User Controls to cache particular regions in a page, but not the entire page.
In some cases, you might want to cache an entire page except for one small area. For example, you might want to display the current username dynamically at the top of a page, but cache the remainder of a page. In these cases, you can take advantage of a feature of the ASP.NET Framework called post-cache substitution.
Post-cache substitution is used internally by the AdRotator
control. Even when you use Page Output Caching to cache a page that contains an AdRotator
control, the content rendered by the AdRotator
control is not cached.
You can use post-cache substitution either declaratively or programmatically. If you want to use post-cache substitution declaratively, then you can use the ASP.NET Substitution
control. For example, the page in Listing 23.18 uses the Substitution
control to display the current time on a page that has been output cached (see Figure 23.6).
Example 23.18. SubstitutionControl.aspx
<%@ Page Language="VB" %> <%@ OutputCache Duration="15" VaryByParam="none" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Public Shared Function GetTime(ByVal context As HttpContext) As String Return DateTime.Now.ToString("T") End Function </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Substitution Control</title> </head> <body> <form id="form1" runat="server"> <div> The cached time is: <%= DateTime.Now.ToString("T") %> <hr /> The substitution time is: <asp:Substitution id="Substitution1" MethodName="GetTime" Runat="server" /> </div> </form> </body> </html>
In Listing 23.18, the time is displayed twice. The time displayed in the body of the page is output cached. The time displayed by the Substitution
control is not cached.
The Substitution
control has one important property: MethodName
. The MethodName
property accepts the name of a method defined in the page. The method must be a shared (static) method because an instance of the class is not created when the page is output cached.
Alternatively, you can use post-cache substitution programmatically by using the Response.WriteSubstitution()
method. This method is illustrated in the page in Listing 23.19.
Example 23.19. ShowWriteSubstitution.aspx
<%@ Page Language="VB" %> <%@ OutputCache Duration="15" VaryByParam="none" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Public Shared Function GetTime(ByVal context As HttpContext) As String Return DateTime.Now.ToString("T") End Function </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show WriteSubstitution</title> </head> <body> <form id="form1" runat="server"> <div> The cached time is: <%= DateTime.Now.ToString("T") %> <hr /> The substitution time is: <% Response.WriteSubstitution(AddressOf GetTime)%> </div> </form> </body> </html>
There are two advantages to using the WriteSubstitution()
method. First, the method referenced by the WriteSubstitution()
method does not have to be a method of the current class. The method can be either an instance or shared method on any class.
The second advantage of the WriteSubstitution()
method is that you can use it within a custom control to perform post-cache substitutions. For example, the NewsRotator
control in Listing 23.20 uses the WriteSubstitution()
method when displaying a random news item. If you use this control in a page that has been output cached, the NewsRotator
control continues to display news items randomly.
Example 23.20. NewsRotator.vb
Imports System Imports System.Data Imports System.Web Imports System.Web.UI Imports System.Web.UI.WebControls Imports System.Collections.Generic Namespace myControls Public Class NewsRotator Inherits WebControl Public Shared Function GetNews(ByVal context As HttpContext) As String Dim News As New List(Of String)() News.Add("Martians attack!") News.Add("Moon collides with earth!") News.Add("Life on Jupiter!") Dim rnd As Random = New Random() Return News(rnd.Next(News.Count)) End Function Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter) Context.Response.WriteSubstitution(AddressOf GetNews) End Sub End Class End Namespace
Building custom controls is discussed in detail in Chapter 31, “Building Custom Controls.”
The CD that accompanies this book includes a page named ShowNewsRotator.aspx
. If you open this page, all the content of the page is cached except for the random news item displayed by the NewsRotator
control (see Figure 23.7).
When you use post-cache substitution (declaratively or programmatically) then caching no longer happens beyond the web server. Using post-cache substitution causes a Cache-Control:no-cache
HTTP header to be included in the HTTP response, which disables caching on proxy servers and browsers. This limitation is understandable because the substitution content must be generated dynamically with each page request.
Using post-cache substitution is appropriate only when working with a string of text or HTML. If you need to perform more complex partial page caching, then you should take advantage of User Controls.
You can cache the rendered contents of a User Control in memory in the same way as you can cache an ASP.NET page. When you add an <%@ OutputCache %>
directive to a User Control, the rendered output of the User Control is cached.
When you cache a User Control, the content is cached on the web server and not on any proxy servers or web browsers. When a web browser or proxy server caches a page, it always caches an entire page.
For example, the Movies User Control in Listing 23.21 displays all the rows from the Movies database table. Furthermore, it includes an OutputCache
directive, which causes the contents of the User Control to be cached in memory for a maximum of 10 minutes (600 seconds).
Example 23.21. Movies.ascx
<%@ Control Language="VB" ClassName="Movies" %> <%@ OutputCache Duration="600" VaryByParam="none" %> User Control Time: <%= DateTime.Now.ToString("T") %> <asp:GridView id="grdMovies" DataSourceID="srcMovies" Runat="server" /> <asp:SqlDataSource id="srcMovies" ConnectionString="<%$ ConnectionStrings:Movies %>" SelectCommand="SELECT Title,Director FROM Movies" Runat="server" />
The User Control in Listing 23.21 displays the records from the Movies database table with a GridView
control. It also displays the current time. Because the control includes an OutputCache
directive, the entire rendered output of the control is cached in memory.
The page in Listing 23.22 includes the Movies
User Control in the body of the page. It also displays the current time at the top of the page. When you refresh the page, the time displayed by the Movies
control changes, but not the time displayed in the body of the page (see Figure 23.8).
Example 23.22. ShowUserControlCache.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="user" TagName="Movies" Src="~/Movies.ascx" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show User Control Cache</title> </head> <body> <form id="form1" runat="server"> <div> Page Time: <%= DateTime.Now.ToString("T") %> <hr /> <user:Movies id="Movies1" Runat="server" /> </div> </form> </body> </html>
You can use the following attributes with an <%@ OutputCache %>
directive declared in a User Control:
Duration
—. The amount of time in seconds that the rendered content of the User Control is cached.
Shared
—. Enables you to share the same cached version of the User Control across multiple pages.
VaryByParam
—. Enables you to create different cached versions of a User Control, depending on the values of one or more query string or form parameters. You can specify multiple parameters by supplying a semicolon-delimited list of query string or form parameter names.
VaryByControl
—. Enables you to create different cached versions of a User Control, depending on the value of a control. You can specify multiple controls by supplying a semicolon-delimited list of control IDs.
VaryByCustom
—. Enables you to specify a custom string used by a custom cache policy. (You also can supply the special value browser, which causes different cached versions of the control to be created when the type and major version of the browser differs.)
Because each User Control that you add to a page can have different caching policies, and because you can nest User Controls with different caching policies, you can build pages that have fiendishly complex caching policies. There is nothing wrong with doing this. In fact, you should take advantage of this caching functionality whenever possible to improve the performance of your applications.
Be careful when setting properties of a cached User Control. If you attempt to set the property of a User Control programmatically when the content of the control is served from the cache, you get a NullReference
exception. Before setting a property of a cached control, first check whether the control actually exists like this:
If Not IsNothing(myControl) Then myControl.SomeProperty = "some value" End If
By default, instances of the same User Control located on different pages do not share the same cache. For example, if you add the same Movies
User Control to more than one page, then the contents of each user control is cached separately.
If you want to cache the same User Control content across multiple pages, then you need to include the Shared
attribute when adding the <%@ OutputCache %>
directive to a User Control. For example, the modified Movies
User Control in Listing 23.23 includes the Shared
attribute.
Example 23.23. SharedMovies.ascx
<%@ Control Language="VB" ClassName="SharedMovies" %> <%@ OutputCache Duration="600" VaryByParam="none" Shared="true" %> User Control Time: <%= DateTime.Now.ToString() %> <asp:GridView id="grdMovies" DataSourceID="srcMovies" Runat="server" /> <asp:SqlDataSource id="srcMovies" ConnectionString="<%$ ConnectionStrings:Movies %>" SelectCommand="SELECT Title,Director FROM Movies" Runat="server" />
Using the Shared
attribute is almost always a good idea. You can save a significant amount of server memory by taking advantage of this attribute.
When you include an <%@ OutputCache %>
directive in a User Control, you can modify programmatically how the User Control is cached. The User Control CachePolicy
property exposes an instance of the ControlCachePolicy
class, which supports the following properties:
Cached
—. Enables you to enable or disable caching.
Dependency
—. Enables you to get or set a cache dependency for the User Control.
Duration
—. Enables you to get or set the amount of time (in seconds) that content is cached.
SupportsCaching
—. Enables you to check whether the control supports caching.
VaryByControl
—. Enables you to create different cached versions of the control, depending on the value of a control.
VaryByParams
—. Enables you to create different cached versions of the control, depending on the value of a query string or form parameter.
The ControlCachePolicy
class also supports the following methods:
SetExpires
—. Enables you to set the expiration time for the cache.
SetSlidingExpiration
—. Enables you to set a sliding expiration cache policy.
SetVaryByCustom
—. Enables you to specify a custom string used by a custom cache policy. (You also can supply the special value browser, which causes different cached versions of the control to be created when the type and major version of the browser differs.)
For example, the User Control in Listing 23.24 uses a sliding expiration policy of one minute. When you specify a sliding expiration policy, a User Control is cached just as long as you continue to request the User Control within the specified interval of time.
Example 23.24. SlidingUserCache.ascx
<%@ Control Language="VB" ClassName="SlidingUserCache" %> <%@ OutputCache Duration="10" VaryByParam="none" %> <script runat="server"> Sub Page_Load() CachePolicy.SetSlidingExpiration(true) CachePolicy.Duration = TimeSpan.FromMinutes(1) End Sub </script> User Control Time: <%= DateTime.Now.ToString("T") %>
The CD that accompanies this book includes a page named ShowSlidingUserCache.aspx
, which contains the SlidingUserCache
control. If you keep requesting this page, and do not let more than one minute pass between requests, then the User Control isn’t dropped from the cache.
You can use the CacheControlPolicy.Dependency
property to create a dependency between a cached User Control and a file (or set of files) on the file system. When the file is modified, the User Control is dropped from the cache automatically and reloaded with the next page request.
For example, the User Control in Listing 23.25 displays all the movies from the Movies.xml
file in a GridView
control. Notice that the User Control includes a Page_Load()
handler that creates a dependency on the Movies.xml
file.
Example 23.25. MovieFileDependency.ascx
<%@ Control Language="VB" ClassName="MovieFileDependency" %> <%@ OutputCache Duration="9999" VaryByParam="none" %> <script runat="server"> Sub Page_Load() Dim depend As New CacheDependency(MapPath("~/Movies.xml")) Me.CachePolicy.Dependency = depend End Sub </script> User Control Time: <%= DateTime.Now.ToString("T") %> <hr /> <asp:GridView id="grdMovies" DataSourceID="srcMovies" Runat="server" /> <asp:XmlDataSource id="srcMovies" DataFile="Movies.xml" Runat="server" />
The CD that accompanies this book includes a page named ShowMovieFileDependency
, which displays the MovieFileDependency
User Control (see Figure 23.9). If you open the page, then the User Control is automatically cached until you modify the Movies.xml
file.
You can load a User Control dynamically by using the Page.LoadControl()
method. You can cache dynamically loaded User Controls in the same way that you can cache User Controls declared in a page. If a User Control includes an <%@ OutputCache %>
directive, then the User Control will be cached regardless of whether the control was added to a page declaratively or programmatically.
However, you need to be aware that when a cached User Control is loaded dynamically, the ASP.NET Framework automatically wraps the User Control in an instance of the PartialCachingControl
class. Therefore, you need to cast the control returned by the Page.LoadControl()
method to an instance of the PartialCachingControl
class.
For example, the page in Listing 23.26 dynamically adds the Movies
User Control in its Page_Load()
event handler. The Page_Load()
method overrides the default cache duration specified in the User Control’s <%@ OutputCache %>
directive. The cache duration is changed to 15 seconds (see Figure 23.10).
Example 23.26. ShowDynamicUserControl.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Sub Page_Load() ' Load the control Dim cacheMe As PartialCachingControl = CType(Page.LoadControl("Movies.ascx"), PartialCachingControl) ' Change cache duration to 15 seconds cacheMe.CachePolicy.SetExpires(DateTime.Now.AddSeconds(15)) ' Add control to page PlaceHolder1.Controls.Add(cacheMe) ' Display control cache duration lblCacheDuration.Text = cacheMe.CachePolicy.Duration.ToString() End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show Dynamic User Control</title> </head> <body> <form id="form1" runat="server"> <div> Cache Duration: <asp:Label id="lblCacheDuration" Runat="server" /> <hr /> <asp:PlaceHolder id="PlaceHolder1" Runat="server" /> </div> </form> </body> </html>
In Listing 23.26, the default cache duration is modified by modifying the PartialCachingControl
’s CachePolicy
property. This property returns an instance of the same ControlCachePolicy
class described in the two previous sections of this chapter.
You can refer to the User Control contained with an instance of the PartialCachingControl
class by using the class’s CachedControl
property. Normally, this property returns the value Nothing
(null) because when the User Control is cached, it is never actually created.
Instead of caching at the page or User Control level, you can cache at the level of a DataSource
control. All three of the standard ASP.NET DataSource
controls—the SqlDataSource
, ObjectDataSource
, and XmlDataSource
controls—include properties that enable you to cache the data that the DataSource
control represents.
One advantage of using the DataSource
controls when caching is that the DataSource
controls can reload data automatically when the data is updated. For example, if you use a SqlDataSource
control to both select and update a set of database records, then the SqlDataSource
control is smart enough to reload the cached data after an update.
The DataSource
controls are also smart enough to share the same data across multiple pages. For example, when using the SqlDataSource
control, a unique entry is created in the Cache
object for each combination of the following SqlDataSource
properties: SelectCommand
, SelectParameters
, and ConnectionString
. If these properties are identical for two SqlDataSource
controls located on two different pages, then the two controls share the same cached data.
In this section, you learn how to use the SqlDataSource
, ObjectDataSource
, and XmlDataSource
controls to cache data. You learn how to set either an absolute or sliding expiration policy. Finally, you learn how to create a cache key dependency that you can use to expire the cache programmatically.
When you use an absolute cache expiration policy, the data that a DataSource
represents is cached in memory for a particular duration of time. Using an absolute cache expiration policy is useful when you know that your data does not change that often. For example, if you know that the records contained in a database table are modified only once a day, then there is no reason to keep grabbing the same records each and every time someone requests a web page.
When caching with the SqlDataSource
control, the SqlDataSource
control’s DataSourceMode
property must be set to the value DataSet
(the default value) rather than DataReader
.
The page in Listing 23.27 displays a list of movies that are cached in memory. The page uses a SqlDataSource
control to cache the data.
Example 23.27. DataSourceAbsoluteCache.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>DataSource Absolute Cache</title> </head> <body> <form id="form1" runat="server"> <div> <asp:GridView id="grdMovies" DataSourceID="srcMovies" Runat="server" /> <asp:SqlDataSource id="srcMovies" EnableCaching="True" CacheDuration="3600" SelectCommand="SELECT * FROM Movies" ConnectionString="<%$ ConnectionStrings:Movies %>" Runat="server" /> </div> </form> </body> </html>
In Listing 23.27, two properties of the SqlDataSource
control related to caching are set. First, the EnableCaching
property is set to the value True
. Next, the CacheDuration
property is set to the value 3,600 seconds
(1 hour). The movies are cached in memory for a maximum of one hour. If you don’t supply a value for the CacheDuration
property, the default value is Infinite
.
It is important to understand that there is no guarantee that the SqlDataSource
control will cache data for the amount of time specified by its CacheDuration
property. Behind the scenes, DataSource
controls use the Cache
object for caching. This object supports scavenging. When memory resources become low, the Cache
object automatically removes items from the cache.
You can test whether the page in Listing 23.27 is working by opening the page and temporarily turning off your database server. You can turn off SQL Server Express by opening the SQL Configuration Manager located in the Microsoft SQL Server 2005 program group and stopping the SQL Server service (see Figure 23.11). If you refresh the page, the data is displayed even though the database server is unavailable.
If you need to cache a lot of data, then it makes more sense to use a sliding expiration policy rather than an absolute expiration policy. When you use a sliding expiration policy, data remains in the cache as long as the data continues to be requested within a certain interval.
For example, imagine that you have been asked to rewrite the Amazon website with ASP.NET 2.0. The Amazon website displays information on billions of books. You couldn’t cache all this book information in memory. However, if you use a sliding expiration policy, then you can cache the most frequently requested books automatically.
The page in Listing 23.28 illustrates how you can enable a sliding cache expiration policy. The cache duration is set to 15 seconds. As long as no more than 15 seconds pass before you request the page, the movies are kept cached in memory.
Example 23.28. DataSourceSlidingCache.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Sub srcMovies_Selecting(ByVal sender As Object, ByVal e As SqlDataSourceSelectingEventArgs) lblMessage.Text = "Selecting data from database" End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>DataSource Sliding Cache</title> </head> <body> <form id="form1" runat="server"> <div> <p> <asp:Label id="lblMessage" EnableViewState="false" Runat="server" /> </p> <asp:GridView id="grdMovies" DataSourceID="srcMovies" Runat="server" /> <asp:SqlDataSource id="srcMovies" EnableCaching="True" CacheExpirationPolicy="Sliding" CacheDuration="15" SelectCommand="SELECT * FROM Movies" ConnectionString="<%$ ConnectionStrings:Movies %>" OnSelecting="srcMovies_Selecting" Runat="server" /> </div> </form> </body> </html>
Notice that the page in Listing 23.28 includes a srcMovies_Selecting()
event handler. This handler is called only when the movies are retrieved from the database rather than from memory. In other words, you can use this event handler to detect when the movies are dropped from the cache (see Figure 23.12).
The ObjectDataSource
control supports the same caching properties as the SqlDataSource
control. You can cache the data that an ObjectDataSource
control represents by setting its EnableCaching
, CacheDuration
, and (optionally) CacheExpirationPolicy
properties.
Multiple ObjectDataSource
controls can share the same cached data. To share the same cache, the ObjectDataSource
controls must have identical TypeName
, SelectMethod
, and SelectParameters
properties.
For example, the page in Listing 23.29 uses an ObjectDataSource
control to represent the Movies
database table. The ObjectDataSource
is bound to a component named Movie
that includes a method named GetMovies()
that returns all of the records from the Movies database table.
Example 23.29. ShowObjectDataSourceCaching.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Sub srcMovies_Selecting(ByVal sender As Object, ByVal e As ObjectDataSourceSelectingEventArgs) lblMessage.Text = "Selecting data from component" End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show ObjectDataSource Caching</title> </head> <body> <form id="form1" runat="server"> <div> <asp:Label id="lblMessage" EnableViewState="false" Runat="server" /> <br /><br /> <asp:GridView id="grdMovies" DataSourceID="srcMovies" Runat="server" /> <asp:ObjectDataSource id="srcMovies" EnableCaching="true" CacheDuration="15" TypeName="Movie" SelectMethod="GetMovies" OnSelecting="srcMovies_Selecting" Runat="server" /> </div> </form> </body> </html>
The ObjectDataSource
control in Listing 23.29 includes an event handler for its Selecting
event. The event handler displays a message in a Label
control. Because the Selecting
event is not raised when data is retrieved from the cache, you can use this method to determine when data is retrieved from the cache or the Movie
component.
The Movie
component is contained in Listing 23.30.
Example 23.30. Movie.vb
Imports System Imports System.Data Imports System.Data.SqlClient Imports System.Web.Configuration Public Class Movie Public Shared Function GetMovies() As DataTable Dim conString As String = WebConfigurationManager.ConnectionStrings("Movies").ConnectionString Dim dad As New SqlDataAdapter("SELECT Title,Director FROM Movies", conString) Dim movies As New DataTable() dad.Fill(movies) Return movies End Function End Class
Notice that the GetMovies()
method returns a DataTable
. When using the ObjectDataSource
control, you can cache certain types of data but not others. For example, you can cache data represented with a DataSet
, DataTable
, DataView
, or collection. However, you cannot cache data represented by a DataReader
. If you attempt to bind to a method that returns a DataReader
, then an exception is thrown.
Unlike the SqlDataSource
and ObjectDataSource
controls, the XmlDataSource
control has caching enabled by default. The XmlDataSource
automatically creates a file dependency on the XML file that it represents. If the XML file is modified, the XmlDataSource
control automatically reloads the modified XML file.
For example, the page in Listing 23.31 contains an XmlDataSource
control that represents the Movies.xml
file. If you modify the Movies.xml
file, then the contents of the files are automatically reloaded.
Example 23.31. ShowXmlDataSourceCaching.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show XmlDataSource Caching</title> </head> <body> <form id="form1" runat="server"> <div> <asp:GridView id="grdMovies" DataSourceID="srcMovies" Runat="server" /> <asp:XmlDataSource id="srcMovies" DataFile="Movies.xml" Runat="server" /> </div> </form> </body> </html>
Imagine that your web application has multiple pages that display different sets of records from the Movies database table. However, you have one page that enables a user to enter a new movie. In that case, you need some method of signaling to all your DataSource
controls that the Movies database table has changed.
You can create a key dependency between the DataSource
controls in your application and an item in the cache. That way, if you remove the item from the cache, all the DataSource
controls will reload their data.
The page in Listing 23.32 contains a SqlDataSource
control that displays the contents of the Movies database table. The SqlDataSource
caches its data for an infinite duration.
Example 23.32. DataSourceKeyDependency.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Sub srcMovies_Selecting(ByVal sender As Object, ByVal e As SqlDataSourceSelectingEventArgs) lblMessage.Text = "Selecting data from database" End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>DataSource Key Dependency</title> </head> <body> <form id="form1" runat="server"> <div> <p> <asp:Label id="lblMessage" EnableViewState="false" Runat="server" /> </p> <asp:GridView id="grdMovies" DataSourceID="srcMovies" Runat="server" /> <asp:SqlDataSource id="srcMovies" EnableCaching="True" CacheDuration="Infinite" CacheKeyDependency="MovieKey" SelectCommand="SELECT * FROM Movies" ConnectionString="<%$ ConnectionStrings:Movies %>" OnSelecting="srcMovies_Selecting" Runat="server" /> <br /><br /> <a href="AddMovieDataSourceKeyDependency.aspx">Add Movie</a> </div> </form> </body> </html>
Notice that the SqlDataSource
control in Listing 23.32 includes a CacheKeyDependency
property that has the value MovieKey
. This property creates a dependency between the DataSource
control’s cached data and an item in the cache named MovieKey
.
The Global.asax
file in Listing 23.33 creates the initial MovieKey
cache item. The value of the cache item doesn’t really matter. In Listing 23.33, the MovieKey
cache item is set to the current date and time.
Example 23.33. Global.asax
<%@ Application Language="VB" %> <script runat="server"> Private Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs) Dim context As HttpContext = HttpContext.Current context.Cache.Insert( _ "MovieKey", _ DateTime.Now, _ Nothing, _ DateTime.MaxValue, _ Cache.NoSlidingExpiration, _ CacheItemPriority.NotRemovable, _ Nothing) End Sub </script>
The page in Listing 23.34 contains a DetailsView
control that enables you to insert a new record. Notice that the DetailsView
control’s ItemInserted
event is handled. When you insert a new record, the MovieKey
item is reinserted into the cache and every DataSource
control that is dependent on this key is reloaded automatically.
Example 23.34. AddMovieDataSourceKeyDependency.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Sub dtlMovie_ItemInserted(ByVal sender As Object, ByVal e As DetailsViewInsertedEventArgs) Cache.Insert("MovieKey", DateTime.Now) Response.Redirect("~/DataSourceKeyDependency.aspx") End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Add Movie Key Dependency</title> </head> <body> <form id="form1" runat="server"> <div> <h1>Add Movie</h1> <asp:DetailsView id="dtlMovie" DefaultMode="Insert" DataSourceID="srcMovies" AutoGenerateRows="false" AutoGenerateInsertButton="true" OnItemInserted="dtlMovie_ItemInserted" Runat="server"> <Fields> <asp:BoundField DataField="Title" HeaderText="Title:" /> <asp:BoundField DataField="Director" HeaderText="Director:" /> </Fields> </asp:DetailsView> <asp:SqlDataSource id="srcMovies" ConnectionString="<%$ ConnectionStrings:Movies %>" InsertCommand="INSERT Movies (Title, Director) VALUES (@Title, @Director)" Runat="server" /> </div> </form> </body> </html>
Behind the scenes, all the various caching mechanisms included in the ASP.NET Framework use the Cache
object. In other words, the Cache
object is the fundamental mechanism for all caching in the ASP.NET Framework.
One instance of the Cache
object is created for each ASP.NET application. Any items you add to the cache can be accessed by any other page, control, or component contained in the same application (virtual directory).
In this section, you learn how to use the properties and methods of the Cache
object. You learn how to add items to the cache, set cache expiration policies, and create cache item dependencies.
The Cache
object exposes the main application programming interface for caching. This object supports the following properties:
Count
—. Represents the number of items in the cache.
EffectivePrivateBytesLimit
—. Represents the size of the cache in kilobytes.
The Cache
object also supports the following methods:
Add
—. Enables you to add a new item to the cache. If the item already exists, this method fails.
Get
—. Enables you to return a particular item from the cache.
GetEnumerator
—. Enables you to iterate through all the items in the cache.
Insert
—. Enables you to insert a new item into the cache. If the item already exists, this method replaces it.
Remove
—. Enables you to remove an item from the cache.
For example, the page in Listing 23.35 displays all the items currently contained in the cache (see Figure 23.13).
Example 23.35. EnumerateCache.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Public Class CacheItem Private _key As String Private _value As Object Public ReadOnly Property Key() As String Get Return _key End Get End Property Public ReadOnly Property Value() As String Get Return _value.ToString() End Get End Property Public Sub New(ByVal key As String, ByVal value As Object) _key = key _value = value End Sub End Class Private Sub Page_Load() Dim items As New ArrayList() For Each item As DictionaryEntry In Cache items.Add(New CacheItem(item.Key.ToString(), item.Value)) Next grdCache.DataSource = items grdCache.DataBind() End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> .grid td, .grid th { padding:5px; } </style> <title>Enumerate Cache</title> </head> <body> <form id="form1" runat="server"> <div> <asp:GridView id="grdCache" CssClass="grid" Runat="server" /> </div> </form> </body> </html>
The page in Listing 23.35 displays only items that have been added to the cache by the methods of the Cache
object. For example, it does not display a list of pages that have been output cached. Output cached pages are stored in the internal cache (the secret cache maintained by the ASP.NET framework).
You can add items to the cache by using the Insert()
method. There are several overloaded versions of the Insert()
method. The maximally overloaded version of the Insert()
method accepts the following parameters:
key
—. Enables you to specify the name of the new item.
value
—. Enables you to specify the value of the new item.
dependencies
—. Enables you to specify one or more cache dependencies, such as a file, key, or SQL dependency.
absoluteExpiration
—. Enables you to specify an absolute expiration time for the cached item. If you don’t need to specify a value for this property, use the static field Cache.NoAbsoluteExpiration
.
slidingExpiration
—. Enables you to specify a sliding expiration interval for the cached item. If you don’t need to specify a value for this property, use the static field Cache.NoSlidingExpiration
.
priority
—. Enables you to specify the priority of the cached item. Possible values are AboveNormal
, BelowNormal
, Default
, High
, Low
, Normal
, and NotRemovable
.
onRemoveCallback
—. Enables you to specify a method that is called automatically before the item is removed from the cache.
When using the cache, it is important to understand that items that you add to the cache might not be there when you attempt to retrieve the item in the future. The cache supports scavenging. When memory resources become low, items are automatically evicted from the cache.
Before using any item that you retrieve from the cache, you should always check whether the item is Nothing
(null). If an item has been removed, then you’ll retrieve Nothing
when you attempt to retrieve it from the cache in the future.
You can add almost any object to the cache. For example, you can add custom components, DataSets
, DataTables
, ArrayLists
, and Lists
to the cache.
You shouldn’t add items to the cache that depend on an external resource. For example, it does not make sense to add a SqlDataReader
or a FileStream
to the cache. When using a SqlDataReader
, you need to copy the contents of the SqlDataReader
into a static representation such as an ArrayList
or List
collection.
When you insert items in the cache, you can specify a time when the item will expire. If you want an item to remain in the cache for an extended period of time, then you should always specify an expiration time for the item.
The page in Listing 23.36 illustrates how you can add an item to the cache with an absolute expiration policy. The item is added to the cache for one hour.
Example 23.36. ShowAbsoluteExpiration.aspx
<%@ Page Language="VB" Trace="true" %> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <%@ Import Namespace="System.Web.Configuration" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Private Sub Page_Load() ' Get movies from Cache Dim movies As DataTable = CType(Cache("Movies"), DataTable) ' If movies not in cache, recreate movies If IsNothing(movies) Then movies = GetMoviesFromDB() Cache.Insert("Movies", movies, Nothing, DateTime.Now.AddHours(1), Cache.NoSlidingExpiration) End If grdMovies.DataSource = movies grdMovies.DataBind() End Sub Private Function GetMoviesFromDB() As DataTable Trace.Warn("Getting movies from database") Dim conString As String = WebConfigurationManager.ConnectionStrings("Movies").ConnectionString Dim dad As New SqlDataAdapter("SELECT Title,Director FROM Movies", conString) Dim movies As New DataTable() dad.Fill(movies) Return movies End Function </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show Absolute Expiration</title> </head> <body> <form id="form1" runat="server"> <div> <asp:GridView id="grdMovies" Runat="server" /> </div> </form> </body> </html>
The first time the page in Listing 23.36 is requested, nothing is retrieved from the cache. In that case, a new DataTable
is created that represents the Movies database table. The DataTable
is inserted into the cache. The next time the page is requested, the DataTable
can be retrieved from the cache and there is no need to access the database.
The DataTable
will remain in the cache for one hour, or until memory pressures force the DataTable
to be evicted from the cache. In either case, the logic of the page dictates that the DataTable
will be added back to the cache when the page is next requested.
Tracing is enabled for the page in Listing 23.36 so that you can see when the Movies database table is loaded from the cache and when the table is loaded from the database. The GetMoviesFromDB()
method writes a Trace
message whenever it executes (see Figure 23.14).
When you specify a sliding expiration policy, items remain in the cache just as long as they continue to be requested within a specified interval of time. For example, if you specify a sliding expiration policy of 5 minutes, then the item remains in the Cache just as long as no more than 5 minutes pass without the item being requested.
Using a sliding expiration policy makes sense when you have too many items to add to the cache. A sliding expiration policy keeps the most requested items in memory and the remaining items are dropped from memory automatically.
The page in Listing 23.37 illustrates how you can add a DataSet to the cache with a sliding expiration policy of 5 minutes.
Example 23.37. ShowSlidingExpiration.aspx
<%@ Page Language="VB" Trace="true" %> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <%@ Import Namespace="System.Web.Configuration" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Private Sub Page_Load() ' Get movies from Cache Dim movies As DataSet = CType(Cache("Movies"), DataSet) ' If movies not in cache, re-create movies If IsNothing(movies) Then movies = GetMoviesFromDB() Cache.Insert("Movies", movies, Nothing, Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(5)) End If grdMovies.DataSource = movies grdMovies.DataBind() End Sub Private Function GetMoviesFromDB() As DataSet Trace.Warn("Getting movies from database") Dim conString As String = WebConfigurationManager.ConnectionStrings("Movies").ConnectionString Dim dad As New SqlDataAdapter("SELECT Title,Director FROM Movies", conString) Dim movies As New DataSet() dad.Fill(movies) Return movies End Function </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show Sliding Expiration</title> </head> <body> <form id="form1" runat="server"> <div> <asp:GridView id="grdMovies" Runat="server" /> </div> </form> </body> </html>
In Listing 23.37, when the DataSet
is added to the cache with the Insert()
method, its absoluteExpiration
parameter is set to the value Cache.NoAbsoluteExpiration
and its slidingExpiration
parameter is set to an interval of 5 minutes.
When you add an item to the Cache
object, you can make the item dependent on an external object. If the external object is modified, then the item is automatically dropped from the cache.
The ASP.NET Framework includes three cache dependency classes:
CacheDependency
—. Enables you to create a dependency on a file or other cache key.
SqlCacheDependency
—. Enables you to create a dependency on a Microsoft SQL Server database table or the result of a SQL Server 2005 query.
AggregateCacheDependency
—. Enables you to create a dependency using multiple CacheDependency
objects. For example, you can combine file and SQL dependencies with this object.
The CacheDependency
class is the base class. The other two classes derive from this class. The CacheDependency
class supports the following properties:
HasChanged
—. Enables you to detect when the dependency object has changed.
UtcLastModified
—. Enables you to retrieve the time when the dependency object last changed.
The CacheDependency
object also supports the following method:
GetUniqueID
—. Enables you to retrieve a unique identifier for the dependency object.
You can create a custom cache dependency class by deriving a new class from the base CacheDependency
class.
The SqlCacheDependency
class is discussed in detail in the final section of this chapter. In this section, I want to show you how you can use the base CacheDependency
class to create a file dependency on an XML file.
The page in Listing 23.38 creates a dependency on an XML file named Movies.xml
. If you modify the Movies.xml
file, the cache is reloaded with the modified file automatically.
Example 23.38. ShowFileDependency.aspx
<%@ Page Language="VB" Trace="true" %> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Private Sub Page_Load() Dim movies As DataSet = CType(Cache("Movies"), DataSet) If IsNothing(movies) Then Trace.Warn("Retrieving movies from file system") movies = New DataSet() movies.ReadXml(MapPath("~/Movies.xml")) Dim fileDepend As New CacheDependency(MapPath("~/Movies.xml")) Cache.Insert("Movies", movies, fileDepend) End If grdMovies.DataSource = movies grdMovies.DataBind() End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show File Dependency</title> </head> <body> <form id="form1" runat="server"> <div> <asp:GridView id="grdMovies" Runat="server" /> </div> </form> </body> </html>
When you add an item to the Cache
, you can specify a particular priority for the item. Specifying a priority provides you with some control over when an item gets evicted from the Cache
. For example, you can indicate that one cached item is more important than other cache items so that when memory resources become low, the important item is not evicted as quickly as other items.
You can specify any of the following values of the CacheItemPriority
enumeration to indicate the priority of a cached item:
AboveNormal
BelowNormal
Default
High
Low
Normal
NotRemovable
For example, the following line of code adds an item to the cache with a maximum absolute expiration time and a cache item priority of NotRemovable
:
Cache.Insert("ImportantItem", DateTime.Now, Nothing, DateTime.MaxValue, Cache.NoSlidingExpiration, CacheItemPriority.NotRemovable, Nothing)
You can configure the size of the cache by using the web configuration file. You specify cache settings with the cache element. This element supports the following attributes:
disableMemoryCollection
—. Enables you to prevent items from being removed from the cache when memory resources become low.
disableExpiration
—. Enables you to prevent items from being removed from the cache when the items expire.
privateBytesLimit
—. Enables you to specify the total amount of memory that can be consumed by your application and its cache before items are removed.
percentagePhysicalMemoryUsedLimit
—. Enables you to specify the total percentage of memory that can be consumed by your application and its cache before items are removed.
privateBytesPollTime
—. Enables you to specify the time interval for checking the application’s memory usage.
Notice that you can’t set the size of the cache directly. However, you can specify limits on the overall memory that your application consumes, which indirectly limits the size of the cache.
By default, both the privateBytesLimit
and percentPhysicalMemoryUsedLimit
attributes have the value 0
, which indicates that the ASP.NET Framework should determine the correct values for these attributes automatically.
The web configuration file in Listing 23.39 changes the memory limit of your application to 100,000 kilobytes and disables the expiration of items in the cache.
The page in Listing 23.40 displays your application’s current private bytes limit (see Figure 23.15):
Example 23.40. ShowPrivateBytesLimit.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Sub Page_Load() lblPrivateBytes.Text = Cache.EffectivePrivateBytesLimit.ToString("n0") End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show Private Bytes Limit</title> </head> <body> <form id="form1" runat="server"> <div> Effective Private Bytes Limit: <asp:Label id="lblPrivateBytes" Runat="server" /> </div> </form> </body> </html>
One of the most powerful new features added to the ASP.NET 2.0 Framework is SQL cache dependencies. This feature enables you to reload cached database data automatically whenever the data in the underlying databases changes.
There is a tradeoff when you use either an absolute or sliding cache expiration policy. The tradeoff is between performance and stale data. For example, if you cache data in memory for 20 seconds, then the data that is displayed on your web pages might be 20 seconds out of date.
In the case of most applications, displaying slightly stale data does not really matter. For example, if you are building a discussion forum, then everyone can live with the fact that new posts might not appear immediately.
However, there are certain types of applications in which you cannot afford to display any stale data at all. For example, if you are creating a stock trading website or an auction website, then every second might count.
The ASP.NET Framework’s support for SQL cache dependencies enables you to take advantage of caching but minimize stale data. When you use a SQL cache dependency, you can automatically detect when data has changed in the underlying database and refresh the data in the cache.
The ASP.NET Framework supports two types of SQL cache dependencies: Polling and Push. You can use Polling SQL cache dependencies with any recent version of Microsoft SQL Server, including Microsoft SQL Server 2005 Express, Microsoft SQL Server 2000, and Microsoft SQL Server 7.0. The second type of cache dependency, Push SQL cache dependencies, works with only Microsoft SQL Server 2005 or Microsoft SQL Server 2005 Express because it requires the SQL Server 2005 Service Broker.
You can use either type of SQL cache dependencies with Page Output Caching, DataSource Control Caching, and Data Caching. The following sections examine each scenario.
A Polling SQL cache dependency is the most flexible type of SQL cache dependency, and I recommend that you use Polling rather than Push SQL cache dependencies for most applications. You can use a Polling SQL cache dependency to detect any type of modification to a database table.
Behind the scenes, a Polling SQL cache dependency uses a database trigger. When a table is modified, the trigger fires and a row in a database table named AspNet_SqlCacheTablesForChangeNotification
is updated to record the fact that the table has been changed.
The ASP.NET Framework uses a background thread to poll this database table for changes on a periodic basis. If there has been a change, then any item in the cache that is dependent on the database table is dropped from the cache.
If you use a Polling SQL cache dependency, then you can eliminate the majority of your database traffic. Unless a database table changes, the only traffic between your web server and the database server is the query that checks for changes in the AspNet_SqlCacheTablesForChangeNotification
table.
Because a Polling SQL cache dependency must poll the database for changes, an item cached with a SQL Polling cache dependency won’t be dropped from the cache immediately after there is a change in the database. The polling interval determines the staleness of your cached data. You can configure the polling interval to be any value you need.
Before you can use a Polling SQL cache dependency, you must perform two configuration steps:
You must enable SQL cache dependencies for a database and one or more database tables.
You must configure SQL cache dependencies in your web configuration file.
Let’s examine each of these steps.
You can configure a SQL Server database to support Polling SQL cache dependencies by using a class in the Framework named the SqlCacheDependencyAdmin
class. This class has the following methods:
DisableNotifications
—. Enables you to disable a database for Polling SQL cache dependencies. Removes all tables and stored procedures used by Polling SQL cache dependencies.
DisableTableForNotification
—. Enables you to disable a particular database table for Polling SQL cache dependencies.
EnableNotifications
—. Enables a database for Polling SQL cache dependencies by adding all the necessary database objects.
EnableTableForNotifications
—. Enables a particular database table for Polling SQL cache dependencies.
GetTablesEnabledForNotifications
—. Enables you to retrieve all tables enabled for Polling SQL cache dependencies.
You should not use the SqlCacheDependencyAdmin
class in an ASP.NET page because calling the methods of this class requires database permissions to create tables, stored procedures, and triggers. For security reasons, the ASP.NET process should not be given these permissions. Instead, you should use the SqlCacheDependencyAdmin
class in a command-line tool.
The ASP.NET Framework includes a command-line tool named aspnet_regsql
that enables you to configure a database to support Polling SQL cache dependencies. This tool works with Microsoft SQL Server 7.0, Microsoft SQL Server 2000, and Microsoft SQL Server 2005. Unfortunately, the aspnet_regsql
command-line tool does not work with a local instance of Microsoft SQL Server 2005 (but we’ll fix this limitation in a moment).
The aspnet_regsql
tool is located in the following folder:
c:WindowsMicrosoft.NETFramework[version]
If you open the SDK Command Prompt from the Microsoft .NET Framework SDK Program group, then you do not need to navigate to the Microsoft.NET folder to execute the aspnet_regsql
command-line tool.
Executing the following command enables the Pubs database for SQL cache dependencies:
aspnet_regsql -C "Data Source=localhost;Integrated Security=True; Initial Catalog=Pubs" -ed
This command creates the AspNet_SqlCacheTablesForChangeNotification
database table and adds a set of stored procedures to the database specified in the connection string.
After you enable a database, you can enable a particular table for SQL cache dependencies with the following command:
aspnet_regsql -C "Data Source=localhost;Integrated Security=True; Initial Catalog=Pubs" -et -t Titles
This command enables the Titles database table for SQL cache dependencies. It creates a new trigger for the Titles database table and adds a new entry in the AspNet_SqlCacheTablesForChangeNotification
table.
Unfortunately, you cannot use the standard aspnet_regsql
tool to enable a local SQL Server 2005 Express database for Polling SQL cache dependencies. The aspnet_regsql tool does not allow you to use the AttachDBFileName
parameter in the connection string.
To get around this limitation, I’ve written a custom command-line tool named enableNotifications
that works with a local SQL Express database. This tool is included on the CD that accompanies this book.
To use the enableNotifications
tool, you need to open a command prompt and navigate to the folder that contains your local SQL Express database table. Next, execute the command with the name of the database file and the name of the database table that you want to enable for Polling SQL cache dependencies. For example, the following command enables the Movies database table located in the MyDatabase.mdf
database:
enableNotifications "MyDatabase.mdf" "Movies"
The enableNotifications
tool works only with a local instance of Microsoft SQL Server Express 2005. You cannot use the tool with other versions of Microsoft SQL Server.
After you set up a database to support Polling SQL cache dependencies, you must configure your application to poll the database. You configure Polling SQL cache dependencies with the sqlCacheDependency
sub-element of the caching element in the web configuration file.
For example, the file in Listing 23.41 causes your application to poll the AspNet_SqlCacheTablesForChangeNotification
table every 5 seconds (5000 milliseconds) for changes.
Example 23.41. Web.Config
<?xml version="1.0"?> <configuration> <connectionStrings> <add name="Movies" connectionString="Data Source=.SQLEXPRESS; AttachDbFilename=|DataDirectory|MyDatabase.mdf;Integrated Security=True; User Instance=True" /> </connectionStrings> <system.web> <caching> <sqlCacheDependency enabled="true" pollTime="5000"> <databases> <add name="MyDatabase" connectionStringName="Movies" /> </databases> </sqlCacheDependency> </caching> </system.web> </configuration>
After you configure Polling SQL cache dependencies, you can use a SQL dependency with Page Output Caching. For example, the page in Listing 23.42 is output cached until you modify the Movies database table.
Example 23.42. PollingSQLOutputCache.aspx
<%@ Page Language="VB" %> <%@ OutputCache Duration="9999" VaryByParam="none" SqlDependency="MyDatabase:Movies" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Polling SQL Output Cache</title> </head> <body> <form id="form1" runat="server"> <div> <%= DateTime.Now.ToString("T") %> <hr /> <asp:GridView id="grdMovies" DataSourceID="srcMovies" Runat="server" /> <asp:SqlDataSource id="srcMovies" ConnectionString="<%$ ConnectionStrings:Movies %>" SelectCommand="SELECT Title, Director FROM Movies" Runat="server" /> </div> </form> </body> </html>
The page in Listing 23.42 includes an <%@ OutputCache %>
directive with a SqlDependency
attribute. The value of the SqlDependency
attribute is the name of the database enabled for SQL dependencies in the web configuration file, followed by the name of a database table.
If you open the page in Listing 23.42 in your browser and click your browser’s Reload button multiple times, then you’ll notice that the time displayed does not change. The page is output cached (see Figure 23.16).
However, if you modify the Movies database, then the page is dropped from the cache automatically (within 5 seconds). The next time you click the Reload button, the modified data is displayed.
If you want to make a page dependent on multiple database tables, then you can assign a semicolon-delimited list of database and table names to the SqlDependency
attribute.
You can use Polling SQL cache dependencies with both the SqlDataSource
and ObjectDataSource
controls by setting the SqlCacheDependency
property. For example, the page in Listing 23.43 caches the output of a SqlDataSource
control until the Movies database table is modified.
Example 23.43. PollingSQLDataSourceCache.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Sub srcMovies_Selecting(ByVal sender As Object, ByVal e As SqlDataSourceSelectingEventArgs) lblMessage.Text = "Retrieving data from database" End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Polling SQL DataSource Cache</title> </head> <body> <form id="form1" runat="server"> <div> <asp:Label id="lblMessage" EnableViewState="false" Runat="server" /> <hr /> <asp:GridView id="grdMovies" DataSourceID="srcMovies" Runat="server" /> <asp:SqlDataSource id="srcMovies" ConnectionString="<%$ ConnectionStrings:Movies %>" SelectCommand="SELECT Title, Director FROM Movies" EnableCaching="true" SqlCacheDependency="MyDatabase:Movies" OnSelecting="srcMovies_Selecting" Runat="server" /> </div> </form> </body> </html>
In Listing 23.43, the SqlDataSource
control includes both an EnableCaching
property and a SqlCacheDependency
property. A database name and table name are assigned to the SqlCacheDependency
property. (The database name must correspond to the database name configured in the <sqlCacheDependency>
section of the web configuration file.)
If you need to monitor multiple database tables, then you can assign a semicolon-delimited list of database and table names to the SqlCacheDependency
property.
You also can use Polling SQL cache dependencies when working with the Cache
object. You represent a Polling SQL cache dependency with the SqlCacheDependency
object.
For example, the page in Listing 23.44 creates a SqlCacheDependency
object that represents the Movies database table. When a DataTable
is added to the Cache
object, the DataTable
is added with the SqlCacheDependency
object.
Example 23.44. PollingSQLDataCache.aspx
<%@ Page Language="VB" Trace="true" %> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <%@ Import Namespace="System.Web.Configuration" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Private Sub Page_Load() Dim movies As DataTable = CType(Cache("Movies"), DataTable) If IsNothing(movies) Then movies = GetMoviesFromDB() Dim sqlDepend As New SqlCacheDependency("MyDatabase", "Movies") Cache.Insert("Movies", movies, sqlDepend) End If grdMovies.DataSource = movies grdMovies.DataBind() End Sub Private Function GetMoviesFromDB() As DataTable Trace.Warn("Retrieving data from database") Dim conString As String = WebConfigurationManager.ConnectionStrings("Movies").ConnectionString Dim dad As New SqlDataAdapter("SELECT Title,Director FROM Movies", conString) Dim movies As New DataTable() dad.Fill(movies) Return movies End Function </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Polling SQL Data Cache</title> </head> <body> <form id="form1" runat="server"> <div> <asp:GridView id="grdMovies" Runat="server" /> </div> </form> </body> </html>
In Listing 23.44, an instance of the SqlCacheDependency
class is created. A database name and table name are passed to the constructor for the SqlCacheDependency
class. This class is used as a parameter with the Cache.Insert()
method when the DataTable
is added to the Cache
.
When using Microsoft SQL Server 2005, you have the option of using Push SQL cache dependencies rather than Polling SQL cache dependencies. Microsoft SQL Server 2005 includes a feature called query notifications, which use the Microsoft SQL Server 2005 Service Broker in the background. The Service Broker can automatically send a message to an application when data changes in the database.
You can create two types of databases with SQL Server Express: a Local or a Server database. You should not use Push dependencies with a Local database. You should use Push dependencies only with a Server database.
You cannot create new Server databases when using Visual Web Developer. You can create a Server database by using the full version of Visual Studio .NET 2005 or by downloading Microsoft SQL Server Management Studio Express from the Microsoft MSDN website (msdn.microsoft.com).
The advantage of using Push dependencies rather than Polling dependencies is that your ASP.NET application does not need to continuously poll your database for changes. When a change happens, your database is responsible for notifying your application of the change.
Now the bad news. There are significant limitations on the types of queries that you can use with Push dependencies. Here are some of the more significant limitations:
The query must use two-part table names (for example, dbo.Movies instead of Movies) to refer to tables.
The query must contain an explicit list of column names (you cannot use *).
The query cannot reference a view, derived table, temporary table, or table variable.
The query cannot reference large object types such as Text
, NText
, and Image
columns.
The query cannot contain a subquery, outer join, or self join.
The query cannot use the DISTINCT
, COMPUTE
, COMPUTE BY
, or INSERT
keywords.
The query cannot use many aggregate functions including AVG
, COUNT(*)
, MAX
, and MIN
.
This is not a complete list of query limitations. For the complete list, refer to the Creating a Query for Notification topic in the SQL Server 2005 Books Online or the MSDN website (msdn.Microsoft.com).
For example, the following simple query won’t work:
SELECT * FROM Movies
This query won’t work for two reasons. First, you cannot use the asterisk (*) to represent columns. Second, you must supply a two-part table name. The following query, on the other hand, will work:
SELECT Title, Director FROM dbo.Movies
You can use Push SQL cache dependencies with stored procedures. However, each SELECT
statement in the stored procedure must meet all the requirements just listed.
You must perform two configuration steps to enable Push SQL cache dependencies:
You must configure your database by enabling the SQL Server 2005 Service Broker.
You must configure your application by starting the notification listener.
In this section, you learn how to perform both of these configuration steps.
Unfortunately, when a Push SQL cache dependency fails, it fails silently, without adding an error message to the Event Log. This makes the situation especially difficult to debug. I recommend that after you make the configuration changes discussed in this section that you restart both your web server and database server.
Before you can use Push SQL cache dependencies, you must enable the Microsoft SQL Server 2005 Service Broker. You can check whether the Service Broker is activated for a particular database by executing the following SQL query:
SELECT name, is_broker_enabled FROM sys.databases
If the Service Broker is not enabled for a database, then you can enable it by executing an ALTER DATABASE
command. For example, the following SQL command enables the Service Broker for a database named MyMovies:
ALTER DATABASE MyMovies SET ENABLE_BROKER
Finally, the ASP.NET process must be supplied with adequate permissions to subscribe to query notifications. When an ASP.NET page is served from Internet Information Server, the page executes in the context of the NETWORK SERVICE
account (in the case of Microsoft Windows Server 2003) or the ASPNET
account (in the case of other operating systems such as Windows XP).
Executing the following SQL command provides the local ASPNET
account on a server named YOURSERVER with the required permissions:
GRANT SUBSCRIBE QUERY NOTIFICATIONS TO "YOURSERVERASPNET"
When you request an ASP.NET page when using the Visual Web Developer web server, an ASP.NET page executes in the security context of your current user account. Therefore, when using a file system website, you’ll need to grant SUBSCRIBE QUERY NOTIFICATIONS
permissions to your current account.
Before you can receive change notifications in your application, you must enable the query notification listener. You can enable the listener with the Global.asax
file in Listing 23.45.
Example 23.45. Global.asax
<%@ Application Language="VB" %> <%@ Import Namespace="System.Data.SqlClient" %> <%@ Import Namespace="System.Web.Configuration" %> <script runat="server"> Private Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs) ' Enable Push SQL cache dependencies Dim conString As String = WebConfigurationManager.ConnectionStrings("MyMovies").ConnectionString SqlDependency.Start(conString) End Sub </script>
The Application_Start
handler executes once when your application first starts. In Listing 23.45, the SqlDependency.Start()
method is called with a connection string to a SQL Express server database named MyMovies.
The code in Listing 23.45 is commented out in the Global.asax
file on the CD that accompanies this book so that it won’t interfere with all the previous code samples discussed in this chapter. You’ll need to remove the comments to use the code samples in the following sections.
You can use Push SQL cache dependencies when caching an entire ASP.NET page. If the results of any SQL command contained in the page changes, then the page is dropped automatically from the cache.
The SqlCommand
object includes a property named the NotificationAutoEnlist
property. This property has the value True
by default. When NotificationAutoEnlist
is enabled, a Push cache dependency is created between the page and the command automatically.
For example, the page in Listing 23.46 includes an <%@ OutputCache %>
directive that includes a SqlDependency
attribute. This attribute is set to the special value CommandNotification
.
Example 23.46. PushSQLOutputCache.aspx
<%@ Page Language="C#" %> <%@ OutputCache Duration="9999" VaryByParam="none" SqlDependency="CommandNotification" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Push SQL Output Cache</title> </head> <body> <form id="form1" runat="server"> <div> <%= DateTime.Now.ToString("T") %> <hr /> <asp:GridView id="grdMovies" DataSourceID="srcMovies" Runat="server" /> <asp:SqlDataSource id="srcMovies" ConnectionString="<%$ ConnectionStrings:MyMovies %>" SelectCommand="SELECT Title, Director FROM dbo.Movies" Runat="server" /> </div> </form> </body> </html>
The page in Listing 23.46 includes a SqlDataSource
control that retrieves all the records from the Movies database table. Notice that the SqlDataSource
control uses a SQL query that explicitly lists column names and uses a two-part table name. These are requirements when using Push dependencies.
The page in Listing 23.46 displays the current time. If you request the page in your browser, and refresh the page, the time does not change. The time does not change until you modify the Movies database table.
The page in Listing 23.46 connects to a Server database named MyMovies. You should not use Push dependencies with a Local SQL Express database. The page uses a database table named Movies, which was created with the following SQL command:
CREATE TABLE Movies ( Id int IDENTITY NOT NULL, Title nvarchar(100) NOT NULL, Director nvarchar(50) NOT NULL, EntryDate datetime NOT NULL DEFAULT GetDate() )
You also can use Push SQL cache dependencies with both the SqlDataSource
and ObjectDataSource
controls by setting the SqlCacheDependency
property. When using Push rather than Polling dependencies, you need to set the SqlCacheDependency
property to the value CommandNotification
.
For example, the page in Listing 23.47 contains a SqlDataSource
control that has both its EnableCaching
and SqlDependency
properties set.
Example 23.47. PushSQLDataSourceCache.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Sub srcMovies_Selecting(ByVal sender As Object, ByVal e As SqlDataSourceSelectingEventArgs) lblMessage.Text = "Retrieving data from database" End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Push SQL DataSource Cache</title> </head> <body> <form id="form1" runat="server"> <div> <asp:Label id="lblMessage" EnableViewState="false" Runat="server" /> <hr /> <asp:GridView id="grdMovies" DataSourceID="srcMovies" Runat="server" /> <asp:SqlDataSource id="srcMovies" ConnectionString="<%$ ConnectionStrings:MyMovies %>" SelectCommand="SELECT Title, Director FROM dbo.Movies" EnableCaching="true" SqlCacheDependency="CommandNotification" OnSelecting="srcMovies_Selecting" Runat="server" /> </div> </form> </body> </html>
In Listing 23.47, the SqlDataSource
control includes a Selecting
event handler. Because this event is raised when the data cannot be retrieved from the cache, you can use this event to determine when the data is retrieved from the cache or the database server (see Figure 23.17).
The page in Listing 23.47 connects to a Server database named MyMovies. You should not use Push dependencies with a Local SQL Express database. The page uses a database table named Movies, which was created with the following SQL command:
CREATE TABLE Movies ( Id int IDENTITY NOT NULL, Title nvarchar(100) NOT NULL, Director nvarchar(50) NOT NULL, EntryDate datetime NOT NULL DEFAULT GetDate() )
You can use Push SQL cache dependencies when working with the Cache
object. You represent a Push SQL cache dependency with an instance of the SqlCacheDependency
class.
For example, in the Page_Load()
handler in Listing 23.48, a DataTable
is added to the cache that represents the contents of the Movies database table. The DataTable
is displayed in a GridView
control.
Example 23.48. PushSQLDataCache.aspx
<%@ Page Language="VB" Trace="true" %> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <%@ Import Namespace="System.Web.Configuration" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server"> Private Sub Page_Load() Dim movies As DataTable = CType(Cache("Movies"), DataTable) If IsNothing(movies) Then Trace.Warn("Retrieving data from database") Dim conString As String = WebConfigurationManager.ConnectionStrings("MyMovies").ConnectionString Dim dad As New SqlDataAdapter("SELECT Title,Director FROM dbo.Movies", conString) Dim sqlDepend As New SqlCacheDependency(dad.SelectCommand) movies = New DataTable() dad.Fill(movies) Cache.Insert("Movies", movies, sqlDepend) End If grdMovies.DataSource = movies grdMovies.DataBind() End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Push SQL Data Cache</title> </head> <body> <form id="form1" runat="server"> <div> <asp:GridView id="grdMovies" Runat="server" /> </div> </form> </body> </html>
Notice that an instance of the SqlCacheDependency
class is created. A SqlCommand
object is passed to the constructor for the SqlCacheDependency
class. If the results of the SqlCommand
changes, then the DataTable
will be dropped automatically from the cache.
The order of the commands here is important. You need to create the SqlCacheDependency
object before you execute the command. If you call the Fill()
method before you create the SqlCacheDependency
object, then the dependency is ignored.
The page in Listing 23.48 connects to a Server database named MyMovies. You should not use Push dependencies with a Local SQL Express database. The page uses a database table named Movies, which was created with the following SQL command:
CREATE TABLE Movies ( Id int IDENTITY NOT NULL, Title nvarchar(100) NOT NULL, Director nvarchar(50) NOT NULL, EntryDate datetime NOT NULL DEFAULT GetDate() )
In this chapter, you learned how to improve the performance of your ASP.NET applications by taking advantage of caching. In the first part of this chapter, you learned how to use each of the different types of caching technologies supported by the ASP.NET Framework.
First, you learned how to use Page Output Caching to cache the entire rendered contents of a page. You learned how to create different cached versions of the same page when the page is requested with different parameters, headers, and browsers. You also learned how to remove pages programmatically from the Page Output Cache. Finally, we discussed how you can define Cache Profiles in a web configuration file.
Next, you learned how to use Partial Page Caching to apply different caching policies to different regions in a page. You learned how to use post-cache substitution to dynamically inject content into a page that has been output cached. You also learned how to use User Controls to cache different areas of a page.
We also discussed how you can cache data by using the different DataSource
controls. You learned how to enable caching when working with the SqlDataSource
, ObjectDataSource
, and XmlDataSource
controls.
Next, you learned how to use the Cache
object to cache items programmatically. You learned how to add items to the cache with different expiration policies and dependencies. You also learned how to configure the maximum size of the cache in the web configuration file.
Finally, we discussed SQL cache dependencies. You learned how to use SQL cache dependencies to reload database data in the cache automatically when the data in the underlying database changes. You learned how to use both Polling and Push SQL cache dependencies with Page Output Caching, DataSource Caching, and the Cache
object.