18.3 Object CachingAll the examples in this chapter so far have cached pages or parts of pages wrapped in user controls. But ASP.NET allows you much more caching flexibility. You can use object caching to place any object in the cache. The object can be of almost any type: a data type, a web control, a class, a DataSet, etc. The object cache is stored in server memory. As such, it is a limited resource and the careful developer will husband that resource carefully. That said, it is an easy way to buy significant performance benefits when used wisely, especially since ASP.NET will evict older items if memory becomes scarce. Suppose you are developing a retail shopping catalogue web application. Many of the page requests contain queries against the same database to return a relatively static price list and descriptive data. Instead of your control requerying the database each time the data is requested, the data set is cached, so that subsequent requests for the data will be satisfied from high-speed cache rather than the slow and expensive regeneration of the data. You might want to set the cache to expire every minute, hourly, or daily, depending on the needs of the application and the frequency with which the data is likely to change. Object caching is implemented by the Cache class. One instance of this class is created automatically per application domain when the application starts. The class remains valid for the life of the application. The Cache class uses syntax very similar to that of session and application state. Objects are stored in Cache as key/value pairs in a dictionary object. The object being stored is the value, and the key is a descriptive string. To clarify object caching, look at the code shown in Example 18-12. The web page in this listing will display a data grid containing data from the Bugs database. It will initially query data from the Bugs database, then store it in cache for subsequent requests. Example 18-15 contains the VB.NET source, while Example 18-16 shows the C# source. Example 18-16 omits the HTML, since it is identical to the VB.NET version shown in Example 18-15. Example 18-15. Object caching a data set in VB.NET, vbObjCache-01.aspx<%@ Page Language="vb" %> <%@ Import namespace="System.Data" %> <%@ Import namespace="System.Data.SqlClient" %> <script runat="server"> sub Page_Load(ByVal Sender as Object, _ ByVal e as EventArgs) CreateDataGrid( ) end sub sub btnClear_OnClick(ByVal Sender as Object, _ ByVal e as EventArgs) Cache.Remove("DataGridDataSet") CreateDataGrid( ) end sub sub CreateDataGrid( ) dim dsGrid as DataSet dsGrid = CType(Cache("DataGridDataSet"), DataSet) if dsGrid is Nothing then dsGrid = GetDataSet( ) Cache("DataGridDataSet") = dsGrid lbl.Text = "Data from database." else lbl.Text = "Data from cache." end if dg.DataSource=dsGrid.Tables(0) dg.DataBind( ) end sub public function GetDataSet( ) as DataSet ' connect to the Bugs database dim connectionString as string = "server=MyServer; uid=sa; " & _ "pwd=dan; database=Bugs" ' get records from the Bugs table dim commandString as string = "Select BugID, Description from Bugs" ' create the data set command object and the DataSet dim da as SqlDataAdapter = new SqlDataAdapter(commandString, _ connectionString) dim dsData as DataSet = new DataSet( ) ' fill the data set object da.Fill(dsData,"Bugs") return dsData end function </script> <html> <body> <form runat="server"> <h1>Object Caching</h1> <asp:Label id="lbl" runat="server"/> <br/> <br/> <asp:DataGrid id="dg" runat="server"/> <br/> <asp:Button id="btnClear" Text="Clear Cache" OnClick="btnClear_OnClick" runat="server"/> <asp:Button id="btnPost" Text="Post" runat="server"/> </form> </body> </html> Example 18-16. Object Caching a DataSet in C#, csObjCache-01.aspx<%@ Page Language="C#" %> <%@ Import namespace="System.Data" %> <%@ Import namespace="System.Data.SqlClient" %> <script runat="server"> void Page_Load(Object Source, EventArgs E) { CreateDataGrid( ); } void btnClear_OnClick(Object Source, EventArgs E) { Cache.Remove("DataGridDataSet"); CreateDataGrid( ); } public void CreateDataGrid( ) { DataSet dsGrid; dsGrid = (DataSet)Cache["DataGridDataSet"]; if (dsGrid == null) { dsGrid = GetDataSet( ); Cache["DataGridDataSet"] = dsGrid; lbl.Text = "Data from database."; } else { lbl.Text = "Data from cache."; } dg.DataSource=dsGrid.Tables[0]; dg.DataBind( ); } public DataSet GetDataSet( ) { // connect to the Bugs database string connectionString = "server=MyServer; uid=sa; pwd=dan; " + "database=Bugs"; // get records from the Bugs table string commandString = "Select BugID, Description from Bugs"; // create the data set command object and the DataSet SqlDataAdapter dataAdapter = new SqlDataAdapter(commandString, connectionString); DataSet dsData = new DataSet( ); // fill the data set object dataAdapter.Fill(dsData,"Bugs"); return dsData; } </script> The heart of Example 18-15 and Example 18-16 involves data access. For a complete discussion of data access in ASP.NET, see Chapter 11 and Chapter 12. For now, notice that the directives at the top of the code listing include two Import directives in order to make the classes and methods from the System.Data and System.Data.SqlClient namespaces available to the code. While looking at the page directives, also notice that there is no OutputCache directive, since this example does not use output caching. A method named CreateDataGrid is called every time the data grid needs to be created. Notice that it is called in the Page_Load every time the page is loaded. Looking at the CreateDataGrid method, a DataSet object is instantiated to contain the data that will be bound and displayed by the data grid. In VB.NET, the code is: dim dsGrid as DataSet
In C#, the code is: DataSet dsGrid;
The Cache object with the key DataGridDataSet is then retrieved and assigned to the dsGrid DataSetobject. In VB.NET, the code is: dsGrid = CType(Cache("DataGridDataSet"), DataSet)
In C#, the code is: dsGrid = (DataSet)Cache["DataGridDataSet"];
As with the Session and Application objects seen in Chapter 6, whatever is retrieved from the Cache object must be explicitly cast, or converted, to the correct data type, here DataSet. For this purpose, C# uses an explicit cast, while VB.NET uses the CType function. The dsGrid data set is then tested to see if it actually exists. Although the DataSet object has been instantiated, it is only a placeholder until it actually contains data. If the Cache object with the key DataGridDataSet has not yet been created or has expired, then dsGrid still has no data in it. In VB.NET, this is done using: if dsGrid is Nothing then
In C#, it's: if (dsGrid == null)
If the DataSet object does already contain data, meaning the Cache had been previously filled and was not expired, then the Label control's Text property is set accordingly to convey this to you on the web page. Otherwise, the GetDataSet method is called, the cache is filled with the data set returned by GetDataSet, and the Label control's Text property is set accordingly. In VB.NET, the code is: dsGrid = GetDataSet( ) Cache("DataGridDataSet") = dsGrid lbl.Text = "Data from database." In C#, it's: dsGrid = GetDataSet( ); Cache["DataGridDataSet"] = dsGrid; lbl.Text = "Data from database."; In either case, once the data set is filled, the DataSource property of the DataGrid control on the web page is set to be the data set, and the DataGrid control is data bound. In VB.NET, the code is: dg.DataSource=dsGrid.Tables(0) dg.DataBind( ) In C#, it's: dg.DataSource=dsGrid.Tables[0]; dg.DataBind( ); The result of running the code in Example 18-12 and Example 18-13 is shown in Figure 18-5. Figure 18-5. Results of object cachingThe first time the web page is run, the label just above the DataGrid control will indicate that the data is coming directly from the database. Every subsequent time the form is requested, the label will change to say "Data from cache." There is no way for the cache in this example to expire (i.e., to go away), unless memory becomes scarce on the server and ASP.NET removes it automatically. As you will see shortly, there are several ways to force a cache to expire. In this example, however, even opening a new browser instance on a different machine will cause the data to come from the cache unless the application on the server is restarted. That is because the cache is available to the entire application, just as the Application object is. In this example, a button, called btnClear, is added to the form to empty the cache and refill it. The event handler for this button calls the Cache.Remove method. This method removes the cache record specified by the key named as the parameter to the method. In VB.NET, the code is: Cache.Remove("DataGridDataSet")
In C#, it's: Cache.Remove("DataGridDataSet");
In Example 18-12 and Example 18-13, the button event handler then refills the cache by calling the CreateDataGrid method. As an exercise in observing different behavior, comment out the line that calls CreateDataGrid in the btnClear_OnClick event procedure and observe the different behavior when you repost the page after clicking the Clear Cache button. When the line calling the CreateDataGrid method is not commented out, then the next time a browser is opened after the Clear Cache button is clicked, the data will still come from the cache. But if the line is commented out, the next browser instance will get the data directly from the database. 18.3.1 Cache Class FunctionalityExample 18-15 and Example 18-16 demonstrate how to add values to and retrieve values from the Object cache using a dictionary syntax of key/value pairs. The Cache class exposes much more functionality than this, including the ability to set dependencies, manage expirations, and control how memory used by cached objects can be recovered for more critical operations. All of these features will be covered in detail in the next sections. This additional functionality is exposed through a different syntax for adding objects to the cache that uses the Add and Insert methods of the Cache class. The Add and Insert methods are very similar in effect. The only difference is that the Add method requires parameters for controlling all the exposed functionality, while the Insert method allows you to make some of the parameters optional, using default values for those parameters. The syntax for the Add method in VB.NET is: Cache.Add(KeyName, KeyValue, Dependencies, AbsoluteExpiration, _ SlidingExpiration, Priority, CacheItemRemovedCallback) For C#, it's: Cache.Add(KeyName, KeyValue, Dependencies, AbsoluteExpiration, SlidingExpiration, Priority, CacheItemRemovedCallback); In these syntaxes, KeyName is a string with the name of the key in the Cache dictionary, and KeyValue is the value to be inserted into the Cache. KeyValue is an object of any type. All the other parameters will be described below. While the Add method requires that all the parameters be provided, the Insert method is overloaded to allow several of the parameters to be optional. The syntax for the overloaded Insert methods in VB.NET is described in this list. For C#, the syntax is identical except that there is a terminating semicolon for each statement:
To see this syntax in action, replace a single line from Example 18-12 or Example 18-13. Find the line in the CreateDataGrid method that looks like this in VB.NET: Cache("DataGridDataSet") = dsGrid It looks like this in C#: Cache["DataGridDataSet"] = dsGrid; Replace it with the following line in VB.NET: Cache.Insert("DataGridDataSet", dsGrid) Replace it with the following in C#: Cache.Insert("DataGridDataSet", dsGrid); On running the modified page in a browser, you will see no difference from the prior version. By using the Insert method rather than the Add method, you are only required to provide the key and value, just as with the dictionary syntax. There is much more you can do with these methods. 18.3.2 DependenciesOne very useful feature exposed by the Cache class is dependencies. A dependency is a relationship between a cached item and either a point in time or an external object. If the designated point in time is reached or if the external object changes, then the cached item will be automatically expired and removed from the cache. The external object controlling the dependency can be a file, a directory, an array of files or directories, another item stored in the cache (represented by its key), or an array of items stored in the cache. The designated point in time can be either an absolute time or a relative time. In the following sections, we'll examine each of these dependencies and how they can be used to control the contents of the cache programmatically. 18.3.2.1 File change dependencyWith a file change dependency, a cached item will become expired and be removed from the cache if a specified file has changed. This feature is typically used when a cached data set is derived from an XML file. You do not want the application to get the data set from the cache if the underlying XML file has changed. To generate the XML file:
Example 18-17 shows the contents of an XML file that contains all the records from the Bugs table in the Bugs database. The code in Example 18-17 can be modified to demonstrate a file change dependency. Since the data set will be coming from XML rather than SQL Server, replace the Import directive pointing to System.Data.SqlClient with a directive pointing to System.Xml. Example 18-17. Bugs.xml<?xml version="1.0" encoding="utf-8" ?> <ROOT> <bugs BugID="1" Product="2" Version="0.1" Description="Update bug test" Reporter="3" /> <bugs BugID="2" Product="1" Version="0.1" Description="Does not report correct owner of bug" Reporter="5" /> <bugs BugID="3" Product="1" Version="0.1" Description="Does not show history of previous action" Reporter="6" /> <bugs BugID="4" Product="1" Version="0.1" Description="Fails to reload properly" Reporter="5" /> <bugs BugID="5" Product="2" Version="0.7" Description="Loses data overnight" Reporter="5" /> <bugs BugID="6" Product="2" Version="0.7" Description="HTML is not shown properly" Reporter="6" /> <bugs BugID="31" Product="1" Version="0.3" Description="this is test 7" Reporter="1" /> <bugs BugID="32" Product="2" Version="0.1" Description="New bug test" Reporter="3" /> <bugs BugID="33" Product="2" Version="0.1" Description="Cache test 3" Reporter="3" /> <bugs BugID="34" Product="2" Version="0.1" Description="Object cache test 1" Reporter="3" /> <bugs BugID="35" Product="2" Version="0.1" Description="Obj Cache test 2" Reporter="4" /> </ROOT> Next, modify the CreateDataGrid andGetDataSet methods as shown in Example 18-18 for VB.NET and Example 18-19 for C#, where the highlighted lines of code are different from the code in Example 18-15 and Example 18-16. Example 18-18. Cache file dependency in VB.NET, vbObjCache-02.aspxsub CreateDataGrid( ) dim dsGrid as DataSet dsGrid = CType(Cache("DataGridDataSet"), DataSet) if dsGrid is Nothing then dsGrid = GetDataSet( ) dim fileDepends as new CacheDependency(Server.MapPath("Bugs.xml")) Cache.Insert("DataGridDataSet", dsGrid, fileDepends) lbl.Text = "Data from XML file." else lbl.Text = "Data from cache." end if dg.DataSource=dsGrid.Tables(0) dg.DataBind( ) end sub public function GetDataSet( ) as DataSet dim dsData as new DataSet( ) dim doc as new XmlDataDocument( ) doc.DataSet.ReadXml(Server.MapPath("Bugs.xml")) dsData = doc.DataSet return dsData end function Example 18-19. Cache file dependency in C#, csObjCache-02.aspxpublic void CreateDataGrid( ) { DataSet dsGrid; dsGrid = (DataSet)Cache["DataGridDataSet"]; if (dsGrid == null) { dsGrid = GetDataSet( ); CacheDependency fileDepends = new CacheDependency(Server.MapPath("Bugs.xml")); Cache.Insert("DataGridDataSet", dsGrid, fileDepends); lbl.Text = "Data from XML file."; } else { lbl.Text = "Data from cache."; } dg.DataSource=dsGrid.Tables[0]; dg.DataBind( ); } public DataSet GetDataSet( ) { DataSet dsData = new DataSet( ); XmlDataDocument doc = new XmlDataDocument( ); doc.DataSet.ReadXml(Server.MapPath("Bugs.xml")); dsData = doc.DataSet; return dsData; } You also need to import the System.Data and System.Xml namespaces: <%@ Import namespace="System.Data"%> <%@ Import namespace="System.Xml" %> The goal of the GetDataSet method is still to return a data set. However, the source of the data for the data set is now the XML file called Bugs.xml. Since ASP.NET stores data sets internally as XML, it is very easy to move back and forth between XML and data sets. The XML object equivalent to a data set is the XmlDataDocument. An XmlDataDocument object named doc is instantiated. This XmlDataDocument object is filled using the ReadXml method. The MapPath method maps a virtual path of a file on the server to a physical path. The DataSet object is obtained from the DataSet property of the XmlDataDocument object, then returned to the calling method. In the CreateDataGrid method, only three lines have changed from Example 18-15 and Example 18-16. A CacheDependency object is defined against the source XML file. Again, MapPath is used to map the virtual path to a physical path. The dictionary syntax used in Example 18-15 and Example 18-16 to add the item to the cache is changed to use the Insert method of the Cache class. Using the Insert method allows you to specify a dependency in addition to the key name and value. The text string assigned to the label has been updated to reflect the fact that the data is now coming from an XML file rather than a database. Test this page by running the code from Example 18-18 or Example 18-19 in a browser. You will get something similar to Figure 18-6. Figure 18-6. Object caching from XML fileIf you repost the page by highlighting the URL and pressing Enter, the label at the top of the page will indicate that the data is coming from the cache. Now open the Bugs.xml file in a text editor and make a change to one of the values in one of the records. Remember to save the XML file. When you repost the page in the browser, instead of the data still coming from the cache, it will once again be coming from the XML file. As soon as the XML source file was changed, the cached data set was expired and removed from the cache. The next time the page requested the data set from the server, it had to retrieve it fresh from the XML file. If you want to condition the cache dependency on an array of files or directories, the syntax for the CacheDependency constructor in Example 18-18 and Example 18-19 would take an array of file paths or directories rather than a single filename. So, for example, the single line of code in Example 18-18 and Example 18-19 that defines the CacheDependency object would be preceded by code defining a string array with one or more files or paths, and the CacheDependency constructor itself would take the array as a parameter. In VB.NET, it would look something like this: dim fileDependsArray as string( ) = {Server.MapPath("Bugs.xml"), _
Server.MapPath("People.xml")}
dim fileDepends as new CacheDependency(fileDependsArray)
In C#, it would look like this: string[] fileDependsArray = {Server.MapPath("Bugs.xml"), Server.MapPath("People.xml")}; CacheDependency fileDepends = new CacheDependency(fileDependsArray); 18.3.2.2 Cached item dependencyA cached item can be dependent on other items in the cache. If a cached item is dependent on one or more other cached items, it will be expired and removed from the cache if any of those cached items upon which it depends change. These changes include either removal from the cache or a change in value. To make a cached item dependent on other cached items, the keys of all of the controlling items are put into an array of strings. This array is then passed in to the CacheDependency constructor, along with an array of file paths. (If you do not want to define a dependency on any files or paths, then the array of file paths can be Nothing in VB.NET or null in C#.) This is demonstrated in Example 18-20 and Example 18-21. In the web page in this listing, two buttons have been added to the UI. The first button initializes several other cached items. The second button changes the value of the cached text string in one of the controlling cached items. As with the previous examples, a label near the top of the page indicates if the data was retrieved directly from an XML file or from cache. The Clear Cache and Post buttons are unchanged. The lines of code in Example 18-20 and Example 18-21 that are new or changed from Example 18-18 and Example 18-19 are highlighted. Note that the HTML is not included in the C# listing in Example 18-21, since it is identical to that in the VB.NET listing. Example 18-20. Cache item dependency in VB.NET, vbObjCache-03.aspx<%@ Page Language="vb" %> <%@ Import namespace="System.Data" %> <%@ Import namespace="System.Xml" %> <script runat="server"> sub Page_Load(ByVal Sender as Object, _ ByVal e as EventArgs) CreateDataGrid( ) end sub sub btnClear_OnClick(ByVal Sender as Object, _ ByVal e as EventArgs) Cache.Remove("DataGridDataSet") CreateDataGrid( ) end sub sub btnInit_OnClick(ByVal Sender as Object, _ ByVal e as EventArgs) ' Initialize caches to depend on. Cache("Depend0") = "This is the first dependency." Cache("Depend1") = "This is the 2nd dependency." Cache("Depend2") = "This is the 3rd dependency." end sub sub btnKey0_OnClick(ByVal Sender as Object, _ ByVal e as EventArgs) Cache("Depend0") = "This is a changed first dependency." end sub sub CreateDataGrid( ) dim dsGrid as DataSet dsGrid = CType(Cache("DataGridDataSet"), DataSet) if dsGrid is Nothing then dsGrid = GetDataSet( ) dim fileDependsArray as string( ) = {Server.MapPath("Bugs.xml")} dim cacheDependsArray as string( ) = _ {"Depend0","Depend1", "Depend2"} dim cacheDepends as new CacheDependency( _ fileDependsArray, cacheDependsArray) Cache.Insert("DataGridDataSet", dsGrid, cacheDepends) lbl.Text = "Data from XML file." else lbl.Text = "Data from cache." end if dg.DataSource=dsGrid.Tables(0) dg.DataBind( ) end sub public function GetDataSet( ) as DataSet dim dsData as new DataSet( ) dim doc as new XmlDataDocument( ) doc.DataSet.ReadXml(Server.MapPath("Bugs.xml")) dsData = doc.DataSet return dsData end function </script> <html> <body> <form runat="server"> <h1>Object Caching</h1> <h2>Cache Item Dependency</h2> <asp:Label id="lbl" runat="server"/> <br/> <br/> <asp:DataGrid id="dg" runat="server"/> <br/> <asp:Button id="btnClear" Text="Clear Cache" OnClick="btnClear_OnClick" runat="server"/> <asp:Button id="btnPost" Text="Post" runat="server"/> <br/> <br/> <asp:Button id="btnInit" Text="Initialize Keys" OnClick="btnInit_OnClick" runat="server"/> <asp:Button id="btnKey0" Text="Change Key 0" OnClick="btnKey0_OnClick" runat="server"/> </form> </body> </html> Example 18-21. -Cache item dependency in C#, csObjCache-03.aspx<%@ Page Language="C#" %> <%@ Import namespace="System.Data" %> <%@ Import namespace="System.Xml" %> <script runat="server"> void Page_Load(Object Source, EventArgs E) { CreateDataGrid( ); } void btnClear_OnClick(Object Source, EventArgs E) { Cache.Remove("DataGridDataSet"); CreateDataGrid( ); } void btnInit_OnClick(Object Source, EventArgs E) { // Initialize caches to depend on. Cache["Depend0"] = "This is the first dependency."; Cache["Depend1"] = "This is the 2nd dependency."; Cache["Depend2"] = "This is the 3rd dependency."; } void btnKey0_OnClick(Object Source, EventArgs E) { Cache["Depend0"] = "This is a changed first dependency."; } public void CreateDataGrid( ) { DataSet dsGrid; dsGrid = (DataSet)Cache["DataGridDataSet"]; if (dsGrid == null) { dsGrid = GetDataSet( ); string[] fileDependsArray = {Server.MapPath("Bugs.xml")}; string[] cacheDependsArray = {"Depend0","Depend1", "Depend2"}; CacheDependency cacheDepends = new CacheDependency (fileDependsArray, cacheDependsArray); Cache.Insert("DataGridDataSet", dsGrid, cacheDepends); lbl.Text = "Data from XML file."; } else { lbl.Text = "Data from cache."; } dg.DataSource=dsGrid.Tables[0]; dg.DataBind( ); } public DataSet GetDataSet( ) { DataSet dsData = new DataSet( ); XmlDataDocument doc = new XmlDataDocument( ); doc.DataSet.ReadXml(Server.MapPath("Bugs.xml")); dsData = doc.DataSet; return dsData; } </script> In the btnInit_OnClick event handler, the controlling cache items are created. The values of the cached items are not important for this example, except as something to change when the Change Key 0 button is clicked, as is done in the event handler for that button, btnKey0_OnClick. The real action here occurs in the CreateDataGrid method. Two string arrays are defined, one to hold the file to depend upon, and one to hold the keys of the other cached items to depend upon. The file dependency is exactly as described in the preceding section. If you do not wish to implement any file or directory dependency here, then use Nothing or null for VB.NET or C#, respectively. For example, in VB.NET, the code would be: dim cacheDepends as new CacheDependency(Nothing, cacheDependsArray) In C#, it would be: CacheDependency cacheDepends = new CacheDependency(null, cacheDependsArray); Running the code in Example 18-20 or Example 18-21 brings up the page shown in Figure 18-7. Initially, the label above the data grid will show that the data is from the XML file. Re-entering the URL will cause the data to come from the Cache. Clicking any of the buttons or changing the contents of Bugs.xml will cause the cached data set to expire and the data to be retrieved fresh from the XML file the next time the page is posted. Although this example does not explicitly demonstrate what would happen if one of the controlling cached items was removed from the Cache, that, too, would cause the dependent cached item to expire. Figure 18-7. Cached item dependency18.3.2.3 Time dependencyItems in the Cache can be given a dependency based on time. This is done with two parameters in either the Add or Insert methods of the Cache object. The two parameters that control time dependency are AbsoluteExpiration and SlidingExpiration. Both parameters are required in the Add method and are optional in the Insert method through method overloading. To insert a key/value pair into the Cache with file or cached item dependencies and time-based dependencies, use the following syntax (the same in both VB.NET and C#, except for the closing semicolon): Cache.Insert(KeyName, KeyValue, Dependencies, AbsoluteExpiration, SlidingExpiration) If you don't want any file or cached item dependencies, then the Dependencies parameter should be Nothing in VB.NET or null in C#. If this syntax is used, default values will be used for the scavenging and callback parameters (described in the next sections). The AbsoluteExpiration parameter is of type DateTime. It defines a lifetime for the cached item. The time provided can be an absolute time, such as August 21, 2001 at 1:23:45 P.M. The code to implement that type of absolute expiration would look something like the following (in C#): DateTime expDate = new DateTime(2001,8,21,13,23,45); Cache.Insert("DataGridDataSet", dsGrid, null, expDate, Cache.NoSlidingExpiration); Obviously, this is not very flexible. Of greater utility is an absolute expiration based on the current time, say 30 minutes from now. The syntax for that expiration would be (again in C#—VB.NET is identical except for the trailing semicolon and possibly a line continuation character): Cache.Insert("DataGridDataSet", dsGrid, null, DateTime.Now.AddMinutes(30), Cache.NoSlidingExpiration); This line of code inserts the specified data set into the Cache, then expires that item 30 minutes after it was inserted. This scenario would be useful when accessing a slowly changing database where it was only necessary to be sure that the data presented was no more than 30 minutes old. Suppose that the data was extremely volatile and/or needed to be very current. Then perhaps the data presented must never be more than 10 seconds old. The following line of code implements that scenario: Cache.Insert("DataGridDataSet", dsGrid, null, DateTime.Now.AddSeconds(10), Cache.NoSlidingExpiration); If your web page is receiving hundreds of hits per minute, implementing a 10-second cache would provide a huge performance boost by reducing the number of database queries by a factor of 20 or more. Even a one-second cache can provide a significant performance enhancement to heavily trafficked web servers. The other time-based parameter is SlidingExpiration, of type TimeSpan. This parameter specifies a time interval between when an item is last accessed and when it expires. If the sliding expiration is set for 30 seconds, for example, then the cached item will expire if the cache is not accessed within 30 seconds. If it is accessed within that time period, the clock will be reset, so to speak, and the cached item will persist for at least another 30 seconds. To implement this scenario, use the following line of code (again in C#, with the VB.NET version nearly identical): Cache.Insert("DataGridDataSet", dsGrid, null, Cache.NoAbsoluteExpiration, TimeSpan.FromSeconds(30)); Cache.NoAbsoluteExpiration is used for the AbsoluteExpiration parameter. Alternatively, you could use DateTime.MaxValue. This constant is the largest possible value of DateTime, corresponding to 11:59:59 PM, 12/31/9999. (That's a millennium problem we can live with.) This indicates to ASP.NET that absolute expiration should not be used. If you attempt to use both types of expiration policies at once (absolute and sliding), an error will occur. 18.3.3 ScavengingOne of the features of object caching is scavenging, where ASP.NET automatically removes seldom used items from the Cache object if server memory becomes scarce. This frees up memory to handle a higher volume of page requests. Scavenging is influenced through the Priority parameter of the Add and Insert methods of the Cache class. This parameter is required of the Add method and optional for the Insert method through method overloading. The Priority parameter indicates the cost of the cached item relative to the other items stored in the cache. This parameter is used by the cache when it evicts objects in order to free up system memory when the web server runs low on memory. Cached items with a lower priority are evicted before items with a higher priority. The legal values of the Priority parameter are contained in the CacheItemPriority enumeration, shown in Table 18-2 in descending order of priority.
To implement scavenging, use the following line of code in VB.NET: Cache.Insert("DataGridDataSet", dsGrid, null, _ Cache.NoAbsoluteExpiration, _ Cache.NoSlidingExpiration, _ CacheItemPriority.High, _ Nothing) In C#, use the following code: Cache.Insert("DataGridDataSet", dsGrid, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.High, null); The final parameter in the above lines of code pertain to callback support, which will be covered in the next section. Since these Insert method calls use all seven parameters, you could also use the Add method with the same parameters. 18.3.4 Callback SupportIt may be useful to be informed when an item is removed from the cache for any reason. Perhaps you will want to reinsert the item into the cache, or perhaps you will want to know if you need to install more memory in your web server. Such notification is implemented using the CacheItemRemovedCallback parameter of the Add or Insert methods. This parameter specifies a callback method to be run when the cached item is removed. In the example web page shown in Example 18-22 (VB.NET) and Example 18-23 (C#), support is added for a callback when the cached item is expired or removed from the cache. This callback method, RemovedCallback, makes a log entry in a text file in the root of drive C. The log entry has a timestamp and the reason for the removal. The lines in Example 18-22 and Example 18-23 that are changed or new from the previous example are highlighted. Example 18-22. Cache callbacks in VB.NET, vbObjCache-04.aspx<%@ Page Language="vb" %> <%@ Import namespace="System.Data" %> <%@ Import namespace="System.Xml" %> <script runat="server"> private shared onRemove as CacheItemRemovedCallback = Nothing sub Page_Load(ByVal Sender as Object, _ ByVal e as EventArgs) CreateDataGrid( ) end sub sub btnClear_OnClick(ByVal Sender as Object, _ ByVal e as EventArgs) Cache.Remove("DataGridDataSet") CreateDataGrid( ) end sub sub btnInit_OnClick(ByVal Sender as Object, _ ByVal e as EventArgs) ' Initialize caches to depend on. Cache("Depend0") = "This is the first dependency." Cache("Depend1") = "This is the 2nd dependency." Cache("Depend2") = "This is the 3rd dependency." end sub sub btnKey0_OnClick(ByVal Sender as Object, _ ByVal e as EventArgs) Cache("Depend0") = "This is a changed first dependency." end sub sub CreateDataGrid( ) dim dsGrid as DataSet dsGrid = CType(Cache("DataGridDataSet"), DataSet) onRemove = new CacheItemRemovedCallback( _ AddressOf Me.RemovedCallback) if dsGrid is Nothing then dsGrid = GetDataSet( ) dim fileDependsArray as string( ) = _ {Server.MapPath("Bugs.xml")} dim cacheDependsArray as string( ) = _ {"Depend0","Depend1", "Depend2"} dim cacheDepends as new CacheDependency( _ fileDependsArray, cacheDependsArray) Cache.Insert("DataGridDataSet", dsGrid, cacheDepends, _ DateTime.Now.AddSeconds(10), _ Cache.NoSlidingExpiration, _ CacheItemPriority.Default, _ onRemove) lbl.Text = "Data from XML file." else lbl.Text = "Data from cache." end if dg.DataSource=dsGrid.Tables(0) dg.DataBind( ) end sub public sub RemovedCallback(k As String, _ v As Object, _ r As CacheItemRemovedReason) Call WriteFile("Cache removed for following reason: " & _ r.ToString( )) end sub public sub WriteFile(strText as string) dim writer as System.IO.StreamWriter = new System.IO.StreamWriter( _ "C:\test.txt",true) dim str as string str = DateTime.Now.ToString( ) & " " & strText writer.WriteLine(str) writer.Close( ) end sub public function GetDataSet( ) as DataSet dim dsData as new DataSet( ) dim doc as new XmlDataDocument( ) doc.DataSet.ReadXml(Server.MapPath("Bugs.xml")) dsData = doc.DataSet return dsData end function </script> <html> <body> <form runat="server"> <h1>Object Caching</h1> <h2>Cache Callbacks</h2> <asp:Label id="lbl" runat="server"/> <br/> <br/> <asp:DataGrid id="dg" runat="server"/> <br/> <asp:Button id="btnClear" Text="Clear Cache" OnClick="btnClear_OnClick" runat="server"/> <asp:Button id="btnPost" Text="Post" runat="server"/> <br/> <br/> <asp:Button id="btnInit" Text="Initialize Keys" OnClick="btnInit_OnClick" runat="server"/> <asp:Button id="btnKey0" Text="Change Key 0" OnClick="btnKey0_OnClick" runat="server"/> </form> </body> </html> Example 18-23. Cache callbacks in C#, csObjCache-04.aspx<%@ Page Language="C#" %> <%@ Import namespace="System.Data" %> <%@ Import namespace="System.Xml" %> <script runat="server"> private static CacheItemRemovedCallback onRemove = null; void Page_Load(Object Source, EventArgs E) { CreateDataGrid( ); } void btnClear_OnClick(Object Source, EventArgs E) { Cache.Remove("DataGridDataSet"); CreateDataGrid( ); } void btnInit_OnClick(Object Source, EventArgs E) { // Initialize caches to depend on. Cache["Depend0"] = "This is the first dependency."; Cache["Depend1"] = "This is the 2nd dependency."; Cache["Depend2"] = "This is the 3rd dependency."; } void btnKey0_OnClick(Object Source, EventArgs E) { Cache["Depend0"] = "This is a changed first dependency."; } public void CreateDataGrid( ) { DataSet dsGrid; dsGrid = (DataSet)Cache["DataGridDataSet"]; onRemove = new CacheItemRemovedCallback(this.RemovedCallback); if (dsGrid == null) { dsGrid = GetDataSet( ); string[] fileDependsArray = {Server.MapPath("Bugs.xml")}; string[] cacheDependsArray = {"Depend0","Depend1", "Depend2"}; CacheDependency cacheDepends = new CacheDependency( fileDependsArray, cacheDependsArray); Cache.Insert("DataGridDataSet", dsGrid, cacheDepends, DateTime.Now.AddSeconds(10), Cache.NoSlidingExpiration, CacheItemPriority.Default, onRemove); lbl.Text = "Data from XML file."; } else { lbl.Text = "Data from cache."; } dg.DataSource=dsGrid.Tables[0]; dg.DataBind( ); } public void RemovedCallback(String k, Object v, CacheItemRemovedReason r) { WriteFile("Cache removed for following reason: " + r.ToString( )); } void WriteFile(string strText) { System.IO.StreamWriter writer = new System.IO.StreamWriter( @"C:\test.txt",true); string str; str = DateTime.Now.ToString( ) + " " + strText; writer.WriteLine(str); writer.Close( ); } public DataSet GetDataSet( ) { DataSet dsData = new DataSet( ); XmlDataDocument doc = new XmlDataDocument( ); doc.DataSet.ReadXml(Server.MapPath("Bugs.xml")); dsData = doc.DataSet; return dsData; } </script> Looking at the lines of code that call the Insert method, you can see that one more parameter has been added, onRemove. This is the callback. The callback method is encapsulated within a delegate. A delegate is a reference type that encapsulates a method with a specific signature and return type. The callback method is of the same type and must have the same signature as the CacheItemRemovedCallback delegate. The callback method is declared as a private member of the Page class. In VB.NET, the line of code is: private shared onRemove as CacheItemRemovedCallback = Nothing In C#, it's: private static CacheItemRemovedCallback onRemove = null; Further down, in the CreateDataGrid method, the callback delegate is instantiated, passing in a reference to the appropriate method. In VB.NET, the code is: onRemove = new CacheItemRemovedCallback( _ AddressOf Me.RemovedCallback) In C#, it's: onRemove = new CacheItemRemovedCallback(this.RemovedCallback); This instantiation associates the onRemove delegate with the RemovedCallback method. Notice the use of the AddressOf keyword in VB.NET to create a reference to the method, which is not necessary in C#. The RemovedCallBack method is reproduced here in VB.NET: public sub RemovedCallback(k As String, _ v As Object, _ r As CacheItemRemovedReason) Call WriteFile("Cache removed for following reason: " & _ r.ToString( )) end sub The code in C# is this: public void RemovedCallback(String k, Object v, CacheItemRemovedReason r) { WriteFile("Cache removed for following reason: " + r.ToString( )); } This code has the required signature, which consists of three parameters:
This last parameter, CacheItemRemovedReason, provides the reason that the cached item was removed from the cache. It can have one of the values shown in Table 18-3.
In this example, the only thing the RemovedCallback method does is call WriteFile to make a log entry. It does this by instantiating a StreamWriter on the log file. In VB.NET, the code is: dim writer as System.IO.StreamWriter = new System.IO.StreamWriter( _ "C:\test.txt",true) In C#, it's: System.IO.StreamWriter writer = new System.IO.StreamWriter( @"C:\test.txt",true); The second parameter for the StreamWriter class, the Boolean, specifies to append to the file if it exists, and to create the file if it doesn't exist. If false, it would have overwritten the file if it existed. In order for this to work as written, the account used by the ASP.NET process must have sufficient rights to create files in the root directory. The WriteLine method is then used to write the string to be logged to the log file. |