In the previous sections of this chapter, you learned how to cache the entire contents of a page and a particular area of a page. However, in certain situations, caching the data displayed on a page makes more sense than caching the page itself. Imagine that you are creating an intranet application that displays an employee directory for your company. Imagine that you need to display employee information on multiple pages. On one page, you might display a list of employee last names . On another page, you might want to display detailed information about a particular employee. In this situation, you can store all the employee information in a database table and cache a DataSet that contains the database table in memory. On each page in which you need to display employee information, you can retrieve the information from the cached DataSet . Caching database records can significantly improve the performance of your Web site. Instead of retrieving employee information from a database table every time you need to display the information on a page, you can retrieve the information directly from memory. You can cache items in memory by taking advantage of the Cache object, which is an instance of the Cache class. Each ASP.NET application has a single Cache object that remains valid until the application is restarted. ASP CLASSIC NOTE When building pages with Classic ASP, developers often resorted to caching data in the Application object. This object still exists in ASP.NET, and you can still use it to cache data. However, the Cache object provides you with more flexibility than the Application object. The Cache object enables you, for example, to specify expiration policies and dependencies for cached items. Furthermore, the Cache object supports scavenging. When you add items to the Application object, the item remains in memory until explicitly removed or the application is restarted. The Cache object, on the other hand, automatically dumps items from the cache when memory resources get low. In the following sections, you learn different methods of adding and removing items from the Cache object. Adding Items to the CacheYou can add any item to the Cache object by assigning a value to its Item property. For example, the following statement adds a new item named myItem with the value "Hello World!" to the Cache object: Cache.Item( "myItem" ) = "Hello World!" Because Item is the default property of the Cache object, you can also add an item to the cache like this: Cache( "myItem" ) = "Hello World!" You can add more complex objects to the cache, such as DataSets , DataViews , or collections. For example, the following statements add a HashTable to the Cache object: Dim colHashTable As HashTable colStates = New HashTable colStates.Add( "CA", "California" ) colStates.Add( "WA", "Washington" ) Cache( "States" ) = colStates After you add an item to the cache, you can retrieve it like this: myItem = Cache( "myItem" ) You need to understand that when you add items to the Cache object, the items are not guaranteed to remain there. The Cache object supports scavenging; it automatically removes low-priority or infrequently used items from the cache when system resources become low. NOTE See the section "Setting Cache Item Priorities" later in this chapter for more information on how to control when a cached item expires . When you retrieve items from the cache, therefore, you should always check to see whether the item still exists before you do anything with it. If the item has been removed from the Cache object, attempting to retrieve the item from the cache returns the value Nothing . Typically, when you retrieve items from the cache, you use the following logic: myItem = Cache( "myItem" ) If myItem Is Nothing Then ... Recreate item and assign to Cache End If In other words, after you retrieve an item from the cache, you should check whether the item is Nothing . If it is Nothing , you need to reassign the item to the cache. If you want to explicitly remove items from the cache, you can use the Cache object's Remove method. For example, the following statement removes the item named myItem from the cache: Cache.Remove( "myItem" ) The Cache object contains more than just the information you have added. The object also is used internally to cache system information between requests . You can view all the current items in the cache by using the page in Listing 17.14 (see Figure 17.5). Listing 17.14 ShowCached.aspx<Script Runat="Server"> Sub Page_Load Dim objItem As DictionaryEntry For each objItem in Cache lblContents.Text &= "<li>" & objItem.Key lblContents.Text &= "=" & objItem.Value.ToString Next End Sub </Script> <html> <head><title>ShowCached.aspx</title></head> <body> <h2>Cached Items</h2> <asp:Label ID="lblContents" EnableViewState="False" Runat="Server" /> </body> </html> The C# version of this code can be found on the CD-ROM. Figure 17.5. List of cached items.
In the Page_Load subroutine in Listing 17.14, a FOR...EACH loop displays the key and value for each item in the Cache object. The items are displayed in a Label control. Adding Cache File DependenciesWhen you add an item to the cache, you can associate the item with a file. If the file changes, the item is automatically removed from the cache. To create a file dependency, use the Insert method of the Cache object like this: Cache.Insert( "myItem", "Hello!", _ New CacheDependency( MapPath( "myFile.txt" ) ) ) This statement adds an item named myItem with the value Hello! to the cache. It also creates a file dependency between the item and a file named myFile.txt. If myFile.txt is modified, myItem is automatically dumped from the cache. You also can create a file dependency between a cached item and multiple files. For example, the following statements make the cached item named myItem dependent on three files named File1.txt, File2.txt, and File3.txt: Dim myFiles() As String = _ { MapPath( "File1.txt" ), _ MapPath( "File2.txt" ), _ MapPath( "File3.txt" ) } Cache.Insert( "myItem", "Hello", _ New CacheDependency( myFiles ) ) A string array containing a list of three files is passed to the constructor of the CacheDependency class to create the file dependencies. If any of the three files are modified, myItem is dropped from the cache. NOTE The MapPath method of the Page class translates a virtual path , such as /myFile.txt , into a physical path, such as c:\inetpub\ wwwroot \myFile.txt . Creating file dependencies is useful when you're working with custom configuration files. Imagine, for example, that your application depends on a custom XML file for configuration information. You use the XML file to specify format information for your pages, such as the background color and font size. To improve the performance of your application, you can cache the XML file in the Cache object. However, if someone updates the XML configuration file, you want the cached version of the configuration file to be automatically updated as well. You can add this functionality by creating a file dependency between the cached XML file and the XML file on the hard drive. The page in Listing 17.15 illustrates how you can create a file dependency between a cached item named FormatInfo and an XML file named FormatInfo.xml . Listing 17.15 FileDependency.aspx<%@ Import Namespace="System.Data" %> <Script Runat="Server"> Dim strBgcolor As String Dim strFontface As String Dim strFontsize As String Sub Page_Load Dim dstFormat As DataSet Dim drowFormat As DataRow dstFormat = Cache( "FormatInfo" ) If dstFormat Is Nothing Then dstFormat = GetFormatInfo() Cache.Insert( "FormatInfo", dstFormat, _ New CacheDependency( MapPath( "FormatInfo.xml" ) ) ) End If drowFormat = dstFormat.Tables( 0 ).Rows( 0 ) strBgcolor = drowFormat( "bgcolor" ).Trim strFontface = drowFormat( "fontface" ).Trim strFontsize = drowFormat( "fontsize" ).Trim End Sub Function GetFormatInfo() As DataSet Dim dstFormat As DataSet dstFormat = New DataSet() dstFormat.ReadXML( MapPath( "FormatInfo.xml" ) ) Return dstFormat End Function </Script> <html> <head> <basefont size="<%=strFontsize%>" face="<%=strFontface%>"> <title>FileDependency.aspx</title> </head> <body bgcolor="<%=strBgcolor%>"> Hello, welcome to our Web site! </body> </html> The C# version of this code can be found on the CD-ROM. In the Page_Load subroutine in Listing 17.15, an item named FormatInfo is retrieved from the Cache object. The FormatInfo item represents the FormatInfo.xml file as a DataSet . The background color, base font face, and base font size of the page are assigned values from the FormatInfo DataSet . If the FormatInfo item does not exist in the cache, the GetFormatInfo() function is called to retrieve the FormatInfo.xml file from the hard drive and assign it to the cache. When the FormatInfo item is added to the cache, it's added with a file dependency on the FormatInfo.xml file, which is included in Listing 17.16. Listing 17.16 FormatInfo.xml<format> <bgcolor> Green </bgcolor> <fontsize> 6 </fontsize> <fontface> Arial </fontface> </format> The C# version of this code can be found on the CD-ROM. If you modify the FormatInfo.xml file, the FormatInfo DataSet is automatically updated in the cache. For example, if you change the bgcolor element in the FormatInfo.xml file from Green to Red , the background color of the page is automatically updated. Adding Cache Trigger DependenciesIt would be nice if you could update an item in the cache whenever a change is made to a record in a database table. That way, you could cache a database table in memory and dramatically improve the performance of your application without worrying about the data becoming outdated . Unfortunately, however, the Cache object does not directly support this type of dependency. To make an item in the cache dependent on a database table, you need to do a little work. Microsoft SQL Server includes support for a special type of stored procedure called a trigger . A trigger is a stored procedure that executes whenever an update , delete , or insert statement is executed against a table or view. In other words, you can associate a trigger with a table in such a way that it fires whenever a change is made to the contents of the table. Microsoft SQL Server also includes an extended stored procedure named xp_cmdshell that executes a string as an operating system command. You can use the xp_cmdshell stored procedure to write a file to a directory. To create an item in the cache that is dependent on a database table, therefore, you can create a trigger that writes a file to a directory. You can then use a normal file cache dependency to update the item in the cache. To illustrate how this scenario works, you can add a DataSet that represents the Products table to the cache. Whenever a change to the Products table is made, the Products table is automatically updated. The first thing you need to do is create the trigger. The proper trigger is contained in Listing 17.17. Listing 17.17 UpdateCache.sqlCREATE TRIGGER UpdateCache ON Products FOR UPDATE, DELETE, INSERT AS DECLARE @cmd Varchar( 200 ) SELECT @cmd = 'echo ' + Cast( getDATE() As Varchar( 50 ) ) + ' > c:\ leChange.txt' EXEC master..xp_cmdshell @cmd, no_output The C# version of this code can be found on the CD-ROM. The UpdateCache trigger is fired when an update , delete , or insert statement is executed against the Products table. The trigger uses the xp_cmdshell stored procedure to write a file, named tableChange.txt, to the file system. The file contains the current date and time. Next, you need to create an ASP.NET page that adds the Products database table to the Cache object. The page in Listing 17.18 adds a DataSet representing the Products table to the cache. Listing 17.18 DatabaseDependency.aspx<%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <Script Runat="Server"> Sub Page_Load Dim dstProducts As DataSet dstProducts = Cache( "productsDS" ) If dstProducts Is Nothing Then dstProducts = GetProducts() Cache.Insert( "Products", dstProducts, _ New CacheDependency( "c:\ leChange.txt" ) ) End If dgrdProducts.DataSource = dstProducts dgrdProducts.DataBind() End Sub Function GetProducts() As DataSet Dim conNorthwind As SqlConnection Dim strSelect As String Dim dadProducts As SqlDataAdapter Dim dstProducts As DataSet conNorthwind = New SqlConnection( "Server=Localhost;UID=sa;PWD=secret;Database=Northwind" ) strSelect = "Select TOP 20 * From Products ORDER BY ProductID" dadProducts = New SqlDataAdapter( strSelect, conNorthwind ) dstProducts = New DataSet() dadProducts.Fill( dstProducts, "ProductsDS" ) Return dstProducts End Function </Script> <html> <head><title>DatabaseDependency.aspx</title> </head> <body> <asp:DataGrid ID="dgrdProducts" Runat="Server" /> </body> </html> The C# version of this code can be found on the CD-ROM. When you request the page in Listing 17.18, a DataSet representing the Products database table is retrieved from the cache. If the item doesn't exist in the cache, the DataSet is created and added to the cache with a file dependency on the tableChange.txt file. The Products database table remains cached in memory until a change is made to the underlying Products database table. If the Products table is modified, the UpdateCache trigger executes and writes a file to the hard drive. This file, in turn , causes the current cached copy of the Products table to be dropped and a new copy of the table to be added to the cache. Adding Cache Key DependenciesYou can make one cached item dependent on another cached item by creating a key dependency . For example, imagine that you have two database tables cached in memory. One table contains a list of products, and the other table contains a list of product categories. If you make a change to the cached product categories table, you also want to update the cached copy of the products table. You can do so by creating a key dependency. To create a key dependency between one cached item and another, you use the Cache object's Insert method like this: Dim arrKeyDepends() As String = { "item2" } Cache.Insert( "item1", "hello!", _ New CacheDependency( Nothing, arrKeyDepends ) ) This statement makes item1 dependent on item2 . If item2 is modified, item1 is dumped from the Cache object. The first statement creates a string array of key names. In the second statement, this array is passed to the CacheDependency constructor to create the dependencies. You can make one cached item dependent on multiple cached items. To do so, simply list multiple cached item keys in the array: Dim arrKeyDepends() As String = { "item2", "item3", "item4" } Cache.Insert( "item1", "hello!", _ New CacheDependency( Nothing, arrKeyDepends ) ) These statements make item1 dependent on item2 , item3 , and item4 . If any of these cached items are modified, item1 is automatically deleted from the cache. The page in Listing 17.19 illustrates how to make one cached item dependent on another. Listing 17.19 KeyDependency.aspx<Script Runat="Server"> Sub UpdateItem1( s As Object, e As EventArgs ) Cache( "item1" ) = txtNewValue.Text End Sub Sub UpdateItem2( s As Object, e As EventArgs ) Dim arrKeyDepends() As String = { "item1" } Cache.Insert( "item2", txtNewValue.Text, _ New CacheDependency( Nothing, arrKeyDepends ) ) End Sub </Script> <html> <head><title>KeyDependency.aspx</title></head> <body> <form Runat="Server"> <asp:TextBox ID="txtNewValue" Runat="Server" /> <p> <asp:Button Text="Update Item1" OnClick="UpdateItem1" Runat="Server" /> <asp:Button Text="Update Item2" OnClick="UpdateItem2" Runat="Server" /> <hr> Item1 = <%=Cache( "item1" )%> <br> Item2 = <%=Cache( "item2" )%> </form> </body> </html> The C# version of this code can be found on the CD-ROM. The page in Listing 17.19 contains one TextBox and two Button controls. If you click the first button, the value of item1 in the cache is updated. If you click the second button, the value of item2 in the cache is updated. A key dependency exists between item1 and item2 . If you update item1 , item2 is automatically dropped from the cache. You can test this functionality by entering a value in the TextBox control and clicking the Update Item1 button when Item2 has a value. NOTE Key dependencies cascade. If you make a chain of key dependencies, a change in one item in the chain causes the other items in the chain to be dropped. You also can make key dependencies circular. In that case, a modification to any item in the circle drops all the cached items in the circle. Creating an Absolute Expiration PolicyWhen you add items to the cache, you can specify an absolute expiration policy. Items that are inserted with an absolute expiration policy are automatically dropped from the cache after a preset period of time. The following statement, for example, adds to the cache an item that will automatically expire in one minute: Cache.Insert( "myItem", "Hello!", Nothing, _ DateTime.Now.AddMinutes( 1 ), Cache.NoSlidingExpiration ) This statement adds an item named myItem with the value Hello! to the cache. The item is inserted with no file or key dependencies, and an absolute expiration date and time of one minute in the future. (I discuss the Cache.NoSlidingExpiration parameter in the next section.) The page in Listing 17.20 illustrates how to create an item with an absolute expiration policy within an ASP.NET page. Listing 17.20 AbsoluteExpiration.aspx<Script Runat="Server"> Sub Page_Load Dim strTime As String strTime = Cache( "Time" ) If strTime Is Nothing Then strTime = DateTime.Now.ToString( "T" ) Cache.Insert("Time", strTime, Nothing, _ DateTime.Now.AddMinutes( 1 ), Cache.NoSlidingExpiration ) End If lblMessage.Text = strTime End Sub </Script> <html> <head><title>AbsoluteExpiration.aspx</title></head> <body> Data last cached: <asp:Label ID="lblMessage" Font-Bold="True" ForeColor="Blue" Runat="Server" /> </body> </html> The C# version of this code can be found on the CD-ROM. In the Page_Load subroutine in Listing 17.20, an item named Time is pulled from the cache. If Time is Nothing , the item has expired . In that case, the item is added to the cache again with an absolute expiration policy of one minute in the future. Creating a Sliding Expiration PolicyInstead of setting an absolute expiration policy for an item stored in the cache, you can set a sliding policy. When an item is cached with a sliding expiration policy, the item is removed from the cache a certain interval of time after it was last accessed. The following statement adds an item that will expire one minute after it is last accessed: Cache.Insert( "myItem", "Hello!", Nothing, _ Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes( 1 ) ) This statement inserts into the cache an item that won't expire unless it is not accessed for more than one minute. If never more than a minute goes by without the item being accessed, the item will never be deleted from the cache (unless server resources get low). Listing 17.21 illustrates how you can add an item with a sliding expiration policy in an ASP.NET page. Listing 17.21 DataSlide.aspx<Script Runat="Server"> Sub Page_Load Dim strTime As String strTime = Cache( "Time" ) If strTime Is Nothing Then strTime = DateTime.Now.ToString( "T" ) Cache.Insert("Time", strTime, Nothing, _ Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes( 1 ) ) End If lblMessage.Text = strTime End Sub </Script> <html> <head><title>DataSlide.aspx</title></head> <body> Data last cached: <asp:Label ID="lblMessage" Font-Bold="True" ForeColor="Blue" Runat="Server" /> </body> </html> The C# version of this code can be found on the CD-ROM. In the Page_Load subroutine in Listing 17.21, an item is inserted into the cache with a sliding expiration of 1 minute. If you repeatedly refresh the page, the item is never removed from the cache. NOTE You cannot add an item to the cache with both an absolute and sliding expiration policy. Setting Cache Item PrioritiesWhen you insert an item into the cache, you can insert the item with a certain priority. The priority provides the Cache object with a hint about the relative importance of an item. All things being equal, when times are tough and server resources become low, the cache starts dropping lower priority items before higher priority items. You indicate the relative priority of an item by using one of the following values from the CacheItemPriority enumeration:
To insert an item into the cache with a High priority, for example, you would use the following statement: Cache.Insert( "myItem", _ "Hello", _ Nothing, _ Cache.NoAbsoluteExpiration, _ Cache.NoSlidingExpiration, _ CacheItemPriority.High, _ Nothing ) This statement provides a hint to the Cache object to weigh this cache item higher than other cache items. Creating a Cache Callback MethodWhen you insert an item into the cache, you can associate a callback method with the item. If, for whatever reason, the item is removed from the cache, the callback method automatically executes. A callback method is useful in several applications. The most obvious use for a callback method is for automatically reloading an item into the cache when it expires. You also can create more complicated applications for a callback method. Imagine, for example, that you are displaying banner advertisements at your Web site, and you want the list of banner advertisements to be automatically reloaded from a database table every hour . In that case, you could add an absolute expiration policy to the cached item and create a callback method that reloads the banner advertisements. You also can use the callback method to track the behavior of the Cache object. You can write a callback method that writes to a log file every time an item is dropped from memory. To create a callback method, you need to add a subroutine to an ASP.NET page that accepts three parameters: String , Object , and CacheItemRemovedReason . The callback method should look like this: Sub ItemRemoved( itemKey As String, _ itemValue As Object, _ removedReason As CacheItemRemovedReason ) End Sub The first two parameters passed to the callback method represent the name and value of the item removed from the cache. The last parameter represents a value from the CacheItemRemovedReason enumeration. This enumeration, which indicates the reason the item was removed from the cache, can have the following values:
After you create the callback method, you need to create an instance of the CacheItemRemovedCallback class and initialize the class with the callback method. For example, the following statements create an instance of the CacheItemRemovedCallback class named onRemove : Dim Shared onRemove As CacheItemRemovedCallback onRemove = New CacheItemRemovedCallback( AddressOf ItemRemoved ) Finally, you need to refer to the onRemove object as follows when you insert an item into the cache: Cache.Insert( "myItem", _ "Hello!", _ Nothing, _ Cache.NoAbsoluteExpiration, _ Cache.NoSlidingExpiration, _ CacheItemPriority.High, _ onRemove ) Notice that you specify the callback method with the last parameter passed to the Insert method. In this case, you have created an item that does not have any file or key dependencies, or an absolute or sliding expiration date. The page in Listing 17.22 demonstrates how you can use a callback method in an ASP.NET page. Figure 17.6. Using a cache callback method.
Listing 17.22 CacheCallback.aspx<Script Runat="Server"> Public Shared onRemove As CacheItemRemovedCallback Sub ItemRemoved( _ strItemKey As String, _ objItemValue As Object, _ objRemovedReason As CacheItemRemovedReason ) Dim strLogEntry As String strLogEntry = "Item with value " & objItemValue.ToString() strLogEntry &= " removed at " & Now.ToString( "T" ) strLogEntry &= " because of " & objRemovedReason.ToString() If Application( "CacheLog" ) Is Nothing Then Application( "CacheLog" ) = New ArrayList End If Application( "CacheLog" ).Add( strLogEntry ) Beep End Sub Sub btnAddCache_Click( s As Object, e As EventArgs ) onRemove = New CacheItemRemovedCallback( AddressOf ItemRemoved ) Cache.Insert( "myItem", _ txtNewValue.Text, _ Nothing, _ Now.AddSeconds( 10 ), _ Cache.NoSlidingExpiration, _ CacheItemPriority.High, _ onRemove ) End Sub Sub btnRemoveCache_Click( s As Object, e As EventArgs ) Cache.Remove( "myItem" ) End Sub Sub Page_PreRender( s As Object, e As EventArgs ) dgrdCacheLog.DataSource = Application( "CacheLog" ) dgrdCacheLog.Databind() End Sub </Script> <html> <head><title>CacheCallback.aspx</title></head> <body> <form Runat="Server"> <h2>Cache Log</h2> <asp:DataGrid ID="dgrdCacheLog" CellPadding="8" Runat="Server" /> <p> <asp:TextBox id="txtNewValue" Runat="Server" /> <p> <asp:Button id="btnAddCache" Text="Add To Cache!" OnClick="btnAddCache_Click" Runat="Server" /> <asp:Button id="btnRemoveCache" Text="Remove From Cache!" OnClick="btnRemoveCache_Click" Runat="Server" /> <asp:Button Text="Refresh Page!" Runat="Server" /> </form> </body> </html> The C# version of this code can be found on the CD-ROM. The page in Listing 17.22 contains a form that enables you to add and remove items to the cache. When you add an item, the item is added with an absolute expiration policy of 10 seconds and associated with a callback method named itemRemoved . This callback method does two things:
The page contains a DataGrid that displays the contents of the cache log. The DataGrid displays the reason that each item was removed from the cache. NOTE It would not be a good idea to use the Beep statement in a production application. I've used the statement here so you can detect when an item is expired from the cache. You see two types of entries in the cache log. If the item expires from the cache, the log entry contains the word Expired . If you explicitly remove an item by clicking the Remove From Cache! button, the log entry contains the word Removed . |