ASP.NET

Managing the Cache

The last example cached items in the most nave way possible. They were simply placed in the cache and given an index. However, at times you may need a bit more control over the items in the cache. For example, what if the physical source backing one of the items you cache changes? If getting accurate information out to your users is important, you may want to know about the change so you can handle it (perhaps by reloading the new information into the cache). As another example, what if you knew that the data in your cache would become invalid after a certain period of time, or on a certain date? You'd want to make sure the data in the cache is invalidated and appropriately refreshed with new data.

In addition to placing items in the cache using the indexer, the Cache object implements a parameterized method named Insert that allows you control over many aspects of the cached item. The ways in which you may control cache entries include the following:

  • Setting up an absolute expiration time

  • Setting up a sliding expiration time

  • Setting up dependencies between cached items and their backing sources (for example database, file, directory dependencies, or dependencies upon other cache entries)

  • Managing a relative priority of cached items

  • Setting up callback functions to be called when items are removed

The Cache's insert method includes four overloads. Table 14-1 enumerates them.

Table 14-1 Overloads for the Cache.Insert method

Insert Overload

Description

Insert (String, Object)

Directly corresponds to the indexer version. Blindly places the object in the Cache using the string key in the first parameter.

Insert (String, Object, CacheDependency)

Inserts an object into the Cache and associates it with a dependency.

Insert (String, Object, CacheDependency, DateTime, TimeSpan)

Inserts an object into the Cache, associating it with a dependency and an expiration policy.

Insert (String, Object, CacheDependency, DateTime, TimeSpan, CacheItemPriority, CacheItemRemovedCallback)

Inserts an object into the Cache. Associates a dependency and expiration and priority policies. Also associates the Cache entry with a delegate for a callback to notify the application when the item is removed from the cache.

The following example illustrates some of these settings and how they work. In addition, the forthcoming examples illustrate another way to get DataTables and DataSets. You may actually create them programmatically. The next few examples use a DataTable that is created in memory rather than being fetched from a database. While the impact of caching isn't quite as dramatic when using the in-memory DataTable, it is still appreciable-and you can see this other approach to managing data. We'll also see how the DataTable serializes as XML as well (which will be useful for examining cached items with file dependencies).

DataSets in Memory

In Tutorial 11, we looked at making a round-trip to the database to gather data suitable to bind to a control. Then we looked at maintaining data between requests by using the Session object. The Session object holds any .NET CLR object-even a DataReader. However, it's not a good idea to hold on to a DataReader for long periods of time as that means holding a connection open. Having too many open connections will ultimately slow your site to a crawl. A better approach is to make single round-trips to the database and hold on to a DataTable or a DataSet.

In addition to fetching them from databases, a DataTable may be synthesized programmatically. Doing so involves constructing a DataTable and adding DataRows to describe the schema. After constructing a DataTable, you may use it to create columns with the correct "shape," populate them, and then add them to the table's columns collection. Listing 14-2 shows an example of creating a DataTable in memory. The table is a collection of famous quotes and their originators that will be useful in the next examples.

Listing 14-2

  public class QuotesCollection : DataTable
   {
      public QuotesCollection()
      {
            //
            // TODO: Add constructor logic here
            //
      }

    public void Synthesize()
      {
            // Be sure to give a name so that it will serialize as XML
            this.TableName = "Quotations";
            DataRow dr;

            Columns.Add(new DataColumn("Quote", typeof(string)));
            Columns.Add(new DataColumn("OriginatorLastName", typeof(strin)));
            Columns.Add(new DataColumn("OriginatorFirstName",
            typeof(string)));

            dr = this.NewRow();
            dr[0] = "Imagination is more important than knowledge.";
            dr[1] = "Einsten";
            dr[2] = "Albert";
            Rows.Add(dr);

            dr = this.NewRow();
            dr[0] = "Assume a virtue, if you have it not";
            dr[1] = "Shakespeare";
            dr[2] = "William";
            this.Rows.Add(dr);

            dr = this.NewRow();
            dr[0] = @"A banker is a fellow who lends you his umbrella
                  when the sun is shining, but wants it back the
                  minute it begins to rain.";
            dr[1] = "Twain";
            dr[2] = "Mark";
            this.Rows.Add(dr);

            dr = this.NewRow();
            dr[0] = "A man cannot be comfortable without his own approval.";
            dr[1] = "Twain";
            dr[2] = "Mark";
            this.Rows.Add(dr);

            dr = this.NewRow();
            dr[0] = "Beware the young doctor and the old barber";
            dr[1] = "Franklin";
            dr[2] = "Benjamin";
            this.Rows.Add(dr);

            dr = this.NewRow();
            dr[0] = @"Reality is merely an illusion, albeit a
                       very persistent one.";
            dr[1] = "Einstein";
            dr[2] = "Albert";
            this.Rows.Add(dr);

            dr = this.NewRow();
            dr[0] = "Beer has food value, but food has no beer value";
            dr[1] = "Sticker";
            dr[2] = "Bumper";
            this.Rows.Add(dr);

            dr = this.NewRow();
            dr[0] = @"Research is what I'm doing when I don't know
                          what I'm doing";
            dr[1] = "Von Braun";
            dr[2] = "Wernher";
            this.Rows.Add(dr);

            dr = this.NewRow();
            dr[0] = "Whatever is begun in anger ends in shame";
            dr[1] = "Franklin";
            dr[2] = "Benjamin";
            this.Rows.Add(dr);

            dr = this.NewRow();
            dr[0] = "We think in generalities, but we live in details";
            dr[1] = "Whitehead";
            dr[2] = "Alfred North";
            this.Rows.Add(dr);

            dr = this.NewRow();
            dr[0] = "Every really new idea looks crazy at first.";
            dr[1] = "Whitehead";
            dr[2] = "Alfred North";
            this.Rows.Add(dr);

            dr = this.NewRow();
            dr[0] = @"The illiterate of the 21st century will not be
                  those who cannot read and write, but those who cannot learn,
                  unlearn, and relearn.";
            dr[1] = "Whitehead"";
            dr[2] = "Alfred North";
            this.Rows.Add(dr);

    }
   }

Building a DataTable in memory is straightforward-it's mostly a matter of defining the column schema and adding rows to the table.

Now let's take a look at managing items within the cache.

Cache Expirations

The first way of managing cached items is to give them expiration thresholds. In some cases, you may be aware of certain aspects of your cached data that allow you to place expiration times on it. The Cache supports both absolute expirations and sliding expirations.

Absolute Expiration

  1. To try out absolute expirations, add a new page to the UseDataCaching site named CacheExpirations.

  2. Use the Website | Add Existing Item to bring the QuoteCollection.cs file .

  3. Drag a GridView onto the CacheExpirations page. Don't bind it to a data source yet. We'll handle that in the Page_Load method.

    Graphic
  4. In the Page_Load method of the CacheExpirations page, check the cache to see if there's already an instance of the QuoteCollections object (just as in the previous example). If the data's not available from the cache, create an instance of the QuoteCollections class and call the Synthesize method to populate the table. Finally, add it to the cache using the overloaded Insert method. You can use the DataTime class to generate an absolute expiration. Bind the QuotesCollection object to the GridView. The caching policy should be Cache.NoSlidingExpiration. Set up some trace statements so you may see how the expiration times affect the lifetime of the cached object.

        protected void Page_Load(object sender, EventArgs e)
        {
             QuotesCollection quotesCollection;
    
             DateTime dtCurrent = DateTime.Now;
             Trace.Warn("Page_Load",
    "Testing cache at: " +
    dtCurrent.ToString());
             quotesCollection = (QuotesCollection)Cache["QuotesCollection"];
    
             if (quotesCollection == null)
             {
    
                   quotesCollection = new QuotesCollection();
                   quotesCollection.Synthesize();
    
                   DateTime dtExpires = new DateTime(2005, 12, 31, 23, 59, 59);
                   dtCurrent = DateTime.Now;
    
                   Trace.Warn("Page_Load",
    "Caching at: " +
    dtCurrent.ToString());
                   Trace.Warn("Page_Load",
    "This entry will expire at: " +
    dtExpires);
                   Cache.Insert("QuotesCollection",
                            quotesCollection,
                            null,
                            dtExpires,
                            Cache.NoSlidingExpiration,
                            CacheItemPriority.Default,
                               null);
    }
    
             this.GridView1.DataSource = quotesCollection;
             this.DataBind();
    }
    
  5. Experiment with changing the dates and times to see how setting the expiration time forces a reload of the cache.

    An absolute expiration time applied to the cached item tells ASP.NET to flush the item from the cache at a certain time. Now let's try using a different kind of expiration technique-the sliding expiration. Using a sliding expiration tells ASP.NET to keep the data in the cache as long as it has been accessed within a certain period of time. Items that have not been accessed within that time frame are subject to expiration.

Sliding Expirations

  1. Now try setting a sliding expiration for the cached data. Modify the Page_Load method in the CacheExpirations page. Getting a sliding expiration to work is simply a matter of changing the parameters of the Insert method. Make up a time span after which you want the cached items to expire. Pass DateTime.MaxValue as the absolute expiration date and the timespan as the final parameter like so:

        protected void Page_Load(object sender, EventArgs e)
        {
             QuotesCollection quotesCollection;
    
             DateTime dtCurrent = DateTime.Now;
             Trace.Warn("Page_Load",
                   "Testing cache: " + dtCurrent.ToString());
              quotesCollection =
                   (QuotesCollection)Cache["QuotesCollection"];
    
             if (quotesCollection == null)
             {
                   quotesCollection = new QuotesCollection();
                   quotesCollection.Synthesize();
    
                   TimeSpan tsExpires = new TimeSpan(0, 0, 15);
                   dtCurrent = DateTime.Now;
    
                   Trace.Warn("Page_Load",
                         "Caching at: " + dtCurrent.ToString());
                   Trace.Warn("Page_Load",
                         "This entry will expire in: " +
                         tsExpires.ToString());
                   Cache.Insert("QuotesCollection",
                         quotesCollection,
                            null,
                            DateTime.MaxValue,
                            tsExpires);
             }
    
             this.GridView1.DataSource = quotesCollection;
             this.DataBind();
       }
    
  2. Surf to the page. You should see the cache reloading if you haven't accessed the cached item within the designated time frame.

    Cache dependencies represent another way to manage cached items. Let's take a look at how they work.

Cache Dependencies

In addition to allowing objects in the cache to expire by duration, you may set up dependencies for the cached items. For example, imagine our program loads some data from a file and places it into the cache. The backing file (that is, the source of the cached information) may change, making the data in the cache invalid. ASP.NET supports setting up a dependency between the cached item and the file so that changing the file invalidates the cached item. The conditions under which the cached items may be flushed include when a file changes, a directory changes, another cache entry is removed, or data in a table in an SQL Server changes (this is an often requested feature finally available in ASP.NET 2.0!).

Here's an example that illustrates setting up cache dependencies.

Setting Cache Dependencies

  1. Add a new page to the UseDataCache site. Name it CacheDependencies.aspx.

  2. Place a button on the page that you may use to post a request to the page to generate an XML file from the QuotationsCollection. Also, drag a GridView onto the page like so:

    Graphic
  3. Double-click the button to generate a handler for the button that will save the XML Schema and the XML from the DataTable to .XML and .XSD files in the App_Data directory.

  4. Within the handler, instantiate a QuotesCollection object and call Synthesize to generate the data. Within the page, you have a reference to the Server object. Call the MapPath method in the Server object to get the physical path for saving the file. Then use that path to create an XML file and a schema file. The DataTable will do this for you automatically by calling the WriteXmlSchema and WriteXml methods, respectively.

    protected void ButtonSaveAsXML_Click(object sender, EventArgs e)
    {
    
       QuotesCollection quotesCollection = new QuotesCollection();
       quotesCollection.Synthesize();
       String strFilePathXml =
       Server.MapPath(Request.ApplicationPath +
       "\\app_data\\QuotesCollection.xml");
       String strFilePathSchema =
       Server.MapPath(Request.ApplicationPath +
       "\\app_data\\QuotesCollection.xsd");
       quotesCollection.WriteXmlSchema(strFilePathSchema);
       quotesCollection.WriteXml(strFilePathXml);
    
    }
    
  5. Now write a method to load the XML into the QuotationsCollection object and cache the data. You can use the file path to the XML file to create a dependency on the file. When it changes, ASP.NET will empty the cache. Turn off the absolute expiration and the sliding expiration by passing in Cache.NoAbsoluteExpiration and Cache.NoSlidingExpiration. If you put trace statements in, you can see the effect of updating the file after it's been loaded in the cache. Finally, make sure to bind the GridView to the QuotationCollection.

    protected void CacheWithFileDependency()
    {
       QuotesCollection quotesCollection;
    
       Trace.Warn("Page_Load", "Testing cache ");
       quotesCollection = (QuotesCollection)Cache["QuotesCollection"];
    
       if (quotesCollection == null)
       {
             Trace.Warn("Page_Load", "Not found in cache");
             quotesCollection = new QuotesCollection();
    
             String strFilePathXml =
                   Server.MapPath(Request.ApplicationPath +
                   "\\app_data\\QuotesCollection.xml");
             String strFilePathSchema =
                   Server.MapPath(Request.ApplicationPath +
                   "\\app_data\\QuotesCollection.xsd");
    
             quotesCollection.ReadXmlSchema(strFilePathSchema);
             quotesCollection.ReadXml(strFilePathXml);
    
             CacheDependency cacheDependency =
                     new CacheDependency(strFilePathXml);
    
             Cache.Insert("QuotesCollection",
                             quotesCollection,
                             cacheDependency,
                             Cache.NoAbsoluteExpiration,
                             Cache.NoSlidingExpiration,
                             CacheItemPriority.Default,
                                null);
       }
    
       this.GridView1.DataSource = quotesCollection;
       this.DataBind();
    }
    
  6. Call the CacheWithFileDependency() within the Page_Load method.

    protected void Page_Load(object sender, EventArgs e)
    {
       CacheWithFileDependency();
    }
    
  7. Now run the page. It should load the XML and schema into the QuotesCollection, save the QuotesCollection in the cache, and then show the data in the grid. Clicking the Save As XML button will refresh the XML file (upon which a cache dependency was made). Because the file on the disk changes, ASP.NET will flush the cache. Next time you load the page, the cache will need to be reloaded.

Now let's look at the final cache dependency: the SQL Server dependency.

The SQL Server Dependency

ASP.NET 1.0 had a huge gap in its cache dependency functionality. The most useful type of dependency was completely missing-that is, a dependency between a cached item coming from SQL Server and the physical database. Because so many sites use data provided by SQL Server to back their DataGrids and other controls, establishing this dependency is definitely a most useful way to manage cached data.

For the SQL Server dependency to work, you first configure SQL Server using the program aspnet_regsql.exe. The dependency is described in the configuration file, whose name is passed into the SqlCacheDependency constructor. The SqlCacheDependency class monitors the table. When something causes the table to change, ASP.NET will remove the item from the Cache.

Listing 14-3 shows a configuration file with a dependency upon SQL Server. Listing 14-4 shows an ASP.NET page that loads the data from the SQL Server database and establishes a dependency between the database and the cached item.

Listing 14-3


 <caching>
    <sqlCacheDependency enabled="true" >
      <databases >
         <add name="DBName" pollTime="500"
             connectionStringName="connectionString"/>
      </databases>
    </sqlCacheDependency>
   </caching>

Listing 14-4

<%@ Page Language="C#" %>
<script runat="server">
    protected void Page_Load(Object sender, EventArgs e)
    {
        DataSet ds = null;
        ds = (DataSet)Cache["SomeData"];
        if (ds == null)
        {
            string cconnectionString =
               ConfigurationSettings.ConnectionStrings["connectionString"].
               ConnectionString;
               SqlDataAdapter da =
                  new SqlDataAdapter("select * from DBName.table",
                  connectionString);
               ds = new DataSet();
               da.Fill(ds);
               SqlCacheDependency sqlCacheDependency =
                  new SqlCacheDependency("DBName", "table");
              Cache.Insert("SomeData",
                              ds,
                              sqlCacheDependency);
        }
        GridView1.DataSource = ds;
        DataBind();
    }
</script>
<html><body>
    <form id="form1" runat="server">
          <asp:GridView ID="GridView1" Runat="server">
        </asp:GridView>
    </form>
</body></html>

Once items are in the cache and their lifetimes are established through expirations and cached item dependencies, one other way to manage the cache remains-reacting when items are removed.

Clearing the Cache

As you can see from the previous examples, ASP.NET clears the cache on several occasions by

  • removing items explicitly by calling Cache.Remove

  • removing low-priority items due to memory consumption

  • removing items that have expired

One of the parameters to one of the Insert overloaded methods is a callback delegate so that ASP.NET can tell you that something's been removed from the cache. To receive callbacks, you simply need to implement a method that matches the signature, wrap it in a delegate, and then pass it when calling the Insert method. When the object is removed, ASP.NET will call the method you supply.

The next example illustrates setting up a removal callback function.

Removal Callback

  1. One of the main tricks to getting the removal callback to work is finding an appropriate place to put the callback. What happens if you make the callback a member of your Page class? It won't work. The callback will become disconnected after the first page has come and gone. The callback has to live in a place that sticks around. The perfect class for establishing the callback is in the global application area. We'll see the application services in more detail in Tutorial 17. For now, add a global application object to your application. Select Website | Add New Item. Find the Global Application template and insert it into the project. Visual Studio will add a new file named Global.asax to your application.

  2. Global.asax will include a server-side script block. Write a method to handle the callback within the Global.asax file. In this case, the response will be to set a flag indicating the cache is dirty. Then the code will simply place the data back into the cache during the Applicationi_BeginRequest handler. The code for doing so will look very much like the code in the CacheWithFileDependency method shown earlier. You can get a reference to the cache through the current HttpContext.

    <%@ Application Language="C#" %>
    
    <script runat="server">
    
       bool  _bReloadQuotations = false;
    
       public void OnRemoveQuotesCollection(string key, object val,
             CacheItemRemovedReason r)
        {
            // Do something about the dependency Change
            if (r == CacheItemRemovedReason.DependencyChanged)
          {
                _bReloadQuotations = true;
          }
        }
    
        protected void Application_BeginRequest(object sender, EventArgs e)
        {
            if (_bReloadQuotations == true)
          {
                ReloadQuotations();
                _bReloadQuotations = false;
          }
        }
    
       protected void ReloadQuuotations()
       {
            QuotesCollection quotesCollection = new QuotesCollection();
    
          String strFilePathXml =
                Server.MapPath(HttpContext.Current.Request.ApplicationPath +
                "\\app_data\\QuotesCollection.xml");
          String strFilePathSchema =
                Server.MapPath(HttpContext.Current.Request.ApplicationPath +
                "\\app_data\\QuotesCollection.xsd");
    
          quotesCollection.ReadXmlSchema(strFilePathSchema);
          quotesCollection.ReadXml(strFilePathXml);
    
          System.Web.Caching.CacheDependency
              cacheDependency =
              new System.Web.Caching.CacheDependency(strFilePathXml);
    
          HttpContext.Current.Cache.Insert("QuotesCollection",
              quotesCollection,
              cacheDependency,
              Cache.NoAbsoluteExpiration,
              Cache.NoSlidingExpiration,
              CacheItemPriority.Default,
    
              this.OnRemoveQuotesCollection);
                }
         </script>
    
  3. Update the CacheWithFileDepenedency method to use the callback method when establishing the QuotesServer in the cache. You may access the callback method through the page's Application member.

       protected void CacheWithFileDependency()
       {
    
             QuotesCollection quotesCollection;
    
             Trace.Warn("Page_Load", "Testing cache ");
             quotesCollection = (QuotesCollection)Cache["QuotesCollection"];
    
             if (quotesCollection == null)
             {
                   Trace.Warn("Page_Load", "Not found in cache");
                   quotesCollection = new QuotesCollection();
    
                   String strFilePathXml =
                         Server.MapPath(Request.ApplicationPath +
                         "\\app_data\\QuotesCollection.xml");
                   String strFilePathSchema =
                         Server.MapPath(Request.ApplicationPath +
                         "\\app_data\\QuotesCollection.xsd");
    
                   quotesCollection.ReadXmlSchema(strFilePathSchema);
                   quotesCollection.ReadXml(strFilePathXml);
                   CacheDependency cacheDependency =
                        new CacheDependency(strFilePathXml);
    
                  Cache.Insert("QuotesCollection",
                           quotesCollection,
                           cacheDependency,
                           Cache.NoAbsoluteExpiration,
                           Cache.NoSlidingExpiration,
                           CacheItemPriority.Default,
                        this.ApplicationInstance.OnRemoveQuuotesCollection);
             }
    
             this.GridView1.DataSource = quotesCollection;
             this.DataBind();
    }
    

When you surf to the page, you should never see the Page_Load method refreshing the cache. That's because when the XML file is overwritten, ASP.NET immediately calls the ReloadQuotations method-which loads the cache again.