Using the ASP.NET Application Cache to Make Your Applications Scream

CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

by Jeff Prosise of Wintellect

The surest way to write a sluggish Web application is to perform file or database accesses on every request. Every developer knows that reading and writing memory is orders of magnitude faster than reading and writing files and databases. Yet it’s surprising how many of the same developers build ASP and ASP.NET pages that hit a file or database on each and every page access.

Take, for example, the ASP.NET page in Figure 1. Each time the page is fetched, its Page_Load method opens a text file named Quotes.txt, reads it line by line, and stores the lines in an ArrayList. It then selects a random item from the ArrayList and writes it to the page using a Label control. Because Quotes.txt contains a collection of famous (and not-so-famous) quotations, each page refresh displays a randomly selected quotation on the page, as shown in Figure 2.

Figure 1: DumbQuotes.aspx

<%@ Import Namespace="System.IO" %>

<html>
  <body>
    <asp:Label ID="Output" RunAt="server" />
  </body>
</html>

<script language="C#" runat="server">
  void Page_Load (Object sender, EventArgs e)
  {
     ArrayList quotes = new ArrayList ();
     StreamReader reader = null;

     try {
        reader = new StreamReader (Server.MapPath ("Quotes.txt"));

        for (string line = reader.ReadLine (); line != null;
           line = reader.ReadLine ())
           quotes.Add (line);

        Random rand = new Random ();
        int index = rand.Next (0, quotes.Count);
        Output.Text = (string) quotes[index];
     }
     finally {
        if (reader !=  null)
            reader.Close ();
     }
  }
</script>

Figure 2: DumbQuotes.aspx in action

So what’s wrong with this picture? Nothing-unless, that is, you value performance. Each time DumbQuotes.aspx is requested, it reads a text file from beginning to end. Consequently, each and every request results in a physical file access.

Simple as it is, DumbQuotes.aspx can benefit greatly from the ASP.NET application cache. The ASP.NET application cache is a smart in-memory repository for data. It’s smart because it allows items entrusted to it to be assigned expiration policies, and also because it fires notifications when it removes items from the cache, affording applications the opportunity to refresh the cache by replacing old data with new. It’s also flexible. It’s capable of caching instances of any type that derives from System.Object, from simple integers to complex types that you define yourself.

Syntactically, using the ASP.NET application cache couldn’t be simpler. Pages access it through the Cache property that they inherit from System.Web.UI.Page; Global.asax files access it through the Cache property of the Context property inherited from System.Web.HttpApplication. The statement

  Context.Cache.Insert ("MyData", ds);

in Global.asax adds a DataSet named ds to the application cache and keys it with the string “MyData.” The statement

  Cache.Insert ("MyData", ds);

does the same in an ASPX file.

An item placed in the application cache this way remains there indefinitely. Other forms of the Insert method support more advanced cache usage. The following statement places an item in the cache and specifies that the item is to be automatically removed after 5 minutes:

  Cache.Insert ("MyData", ds, null,
      DateTime.Now.AddMinutes (5), Cache.NoSlidingExpiration);

As an alternative, you can assign a sliding expiration by passing a TimeSpan value in Insert‘s fifth parameter and Cache.NoAbsoluteExpiration in the fourth:

  Cache.Insert ("MyData", ds, null,
      Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes (5));

A sliding expiration causes an item to expire when it has not been accessed (retrieved from the cache) for a specified period of time.

A third option is to use Insert‘s third parameter to establish a dependency between an item added to the cache and an object in the file system. When a file or directory that is the target of a dependency changes-when the file is modified, for example-ASP.NET removes the item from the cache. The following example initializes a DataSet from an XML file, adds the DataSet to the application cache, and creates a dependency between the DataSet and the XML file so that the DataSet is automatically removed from the cache if someone modifies the XML file:

DataSet ds = new DataSet ();
ds.ReadXml (Server.MapPath ("MyFile.xml"));
Cache.Insert ( "MyData",
               ds,
               new CacheDependency (Server.MapPath ("MyFile.xml")));

Used this way, a CacheDependency object defines a dependency between a cached item and a file. You can also use CacheDependency to create a dependency between two cached items. Simply pass an array of key names identifying the item or items on which your item depends in the second parameter to CacheDependency’s constructor. If you don’t want to establish a file or directory dependency also, pass null in the constructor’s first parameter.

Cache.Insert also lets you assign priorities to items added to the application cache. When memory grows short, ASP.NET uses these priorities to determine which items to remove first. If you don’t specify otherwise, an item’s priority is CacheItemPriority.Normal. Other valid CacheItemPriority values, in order of lowest to highest priorities, are Low, BelowNormal, AboveNormal, High, and NotRemovable. Priority values are specified in Insert‘s sixth parameter. The following statement inserts a DataSet named ds into the application cache, sets it to expire 1 hour after the last access, and assigns it a relatively high priority so that items with default or lower priority will be purged first in low-memory situations:

  Cache.Insert ("MyData", ds, null,
      Cache.NoAbsoluteExpiration, TimeSpan.FromHours (1),
      CacheItemPriority.AboveNormal, null);

Specifying a CacheItemPriority value equal to NotRemovable is the only way to ensure that an item added to the cache will still be there when you go to retrieve it. That’s important, because it means code that retrieves an item from the application cache should always verify that the reference returned isn’t null-unless, of course, the item was marked NotRemovable.

But perhaps the ASP.NET application cache’s most important feature is its support for cache removal callbacks. If you’d like to be notified when an item is removed, pass a CacheItemRemovedCallback delegate to Insert identifying the method you want ASP.NET to call when it removes the item from the cache, as demonstrated here:

  DataSet ds = new DataSet ();
  ds.ReadXml (Server.MapPath ("MyFile.xml"));
  Cache.Insert ("MyData", ds,
      new CacheDependency (Server.MapPath ("MyFile.xml")),
      Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration,
      CacheItemPriority.Default,
      new CacheItemRemovedCallback (RefreshDataSet));

This example initializes a DataSet from an XML file, inserts the DataSet into the application cache, configures the DataSet to expire when the XML file changes, and registers RefreshDataSet to be called if and when the DataSet expires. Presumably, RefreshDataSet would create a brand new DataSet containing updated data and insert it into the application cache.

So how could the application cache benefit DumbQuotes.aspx? Suppose that instead of reading Quotes.txt in every request, DumbQuotes.aspx read it once-at application startup-and stored it in the application cache. Rather than read Quotes.txt, Page_Load could retrieve a quotation directly from the cache. Furthermore, the cached data could be linked to Quotes.txt and automatically deleted if the file changes, and you could register a callback method that refreshes the cache when the data is removed. That way, the application would incur just one physical file access at startup and would never access the file again unless the contents of the file change.

That’s exactly what happens in SmartQuotes.aspx, shown in Figure 3. On the outside, SmartQuotes.aspx and DumbQuotes.aspx look identical, producing exactly the same output. On the inside, they’re very different. Rather than fetch quotations from Quotes.txt, SmartQuotes.aspx retrieves them from the application cache. Global.asax’s Application_Start method (Figure 4) reads Quotes.txt and primes the cache on application startup, while RefreshQuotes refreshes the cache if Quotes.txt changes. RefreshQuotes is a static method. It must be if ASP.NET is to call it after the current request-the one that registers RefreshQuotes for cache removal callbacks-has finished processing.

Figure 3: SmartQuotes.aspx

<html>
  <body>
    <asp:Label ID="Output" RunAt="server" />
  </body>
</html>

<script language="C#" runat="server">
  void Page_Load (Object sender, EventArgs e)
  {
      ArrayList quotes = (ArrayList) Cache["Quotes"];

      if (quotes != null) {
          Random rand = new Random ();
          int index = rand.Next (0, quotes.Count);
          Output.Text = (string) quotes[index];
      }
      else {
          // If quotes is null, this request arrived after the
          // ArrayList was removed from the cache and before a new
          // ArrayList was inserted. Tell the user the server is
          // busy; a page refresh should solve the problem.
          Output.Text = "Server busy";
      }
  }
</script>

Figure 4: Global.asax

<%@ Import NameSpace="System.IO" %>

<script language="C#" runat="server">
  static Cache _cache = null;
  static string _path = null;

  void Application_Start ()
  {
      _cache = Context.Cache;
      _path = Server.MapPath ("Quotes.txt");

      ArrayList quotes = ReadQuotes ();

      _cache.Insert ("Quotes", quotes, new CacheDependency (_path),
          Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration,
          CacheItemPriority.Default,
          new CacheItemRemovedCallback (RefreshQuotes));
  }

  static void RefreshQuotes (String key, Object item,
      CacheItemRemovedReason reason)
  {
      ArrayList quotes = ReadQuotes ();

      _cache.Insert ("Quotes", quotes, new CacheDependency (_path),
          Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration,
          CacheItemPriority.Default,
          new CacheItemRemovedCallback (RefreshQuotes));
  }

  static ArrayList ReadQuotes ()
  {
      ArrayList quotes = new ArrayList ();
      StreamReader reader = null;

      try {
          reader = new StreamReader (_path);
          for (string line = reader.ReadLine (); line != null;
              line = reader.ReadLine ())
              quotes.Add (line);
      }
      finally {
          if (reader != null)
              reader.Close ();
      }
      return quotes;
  }
</script>

Because high volume Web servers are often required to process hundreds (or thousands) of requests per second, every millisecond counts when designing for performance. The ASP.NET application cache is a fine tool for maximizing performance by minimizing time-consuming file and database I/O. Use it early and use it often!

About the Author…

Jeff Prosise makes his living programming Windows and Microsoft .NET teaching others how to do the same. His latest book, Programming Microsoft .NET, was published by Microsoft Press in May 2002. Today Jeff travels the world teaching .NET programming and speaking at developer conferences. He works closely with Microsoft developers in Redmond, WA, to track the development of the .NET Framework and to teach Microsoft’s own developers the art of .NET programming.

Quotes.txt file used in demos.

# # #

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read