The Application Cache

Now that you know about application state, forget that it even exists. The ASP.NET application cache does everything application state does and more, and it’s loaded with features that make it bigger and better than application state in every way.

What is the ASP.NET application cache? It’s a per-application, in-memory data store that, like application state, can store instances of any managed type, including complex types such as DataSet and Hashtable, and key them with strings. Unlike items placed in application state, items placed in the application cache can be assigned expiration policies. If you want an item to expire 15 minutes after it’s placed in the cache or when the file it was initialized from changes, for example, you simply say so and ASP.NET automatically removes the item at the prescribed time. If that’s still not enough to convince you to forget about application state, consider this: ASP.NET will optionally call the callback method of your choice when it removes an item from the cache. You can refresh the cache by writing callback methods that replace deleted items. Finally, when memory grows short, ASP.NET discards items in the application cache based on usage patterns (such as which items have been accessed the least recently) or on priorities that you assign.

Adding and Removing Items

The application cache is represented by instances of System.Web.Caching.Cache. Like application state, the application cache is exposed to program code through properties in ASP.NET base classes. Pages access the application cache through Page.Cache; Global.asax files access it through HttpApplication.Context.Cache. The following statements add three items—once more, decimal stock prices keyed by stock symbols—to the application cache from Global.asax:

Context.Cache["AMZN"]?=?10.00m;
Context.Cache["INTC"]?=?20.00m;
Context.Cache["MSFT"]?=?30.00m;

ASPX files add items to the application cache without using Context as an intermediary:

Cache["AMZN"]?=?10.00m;
Cache["INTC"]?=?20.00m;
Cache["MSFT"]?=?30.00m;

Items can also be added to the application cache with Cache.Insert:

Cache.Insert?("AMZN",?10.00m);
Cache.Insert?("INTC",?20.00m);
Cache.Insert?("MSFT",?30.00m);

Both Insert and [] replace an existing item if an existing key is specified. In other words, the following statements add just one item to the cache but modify its value three times:

Cache["AMZN"]?=?10.00m;
Cache["AMZN"]?=?11.00m;
Cache.Insert?("AMZN",?12.00m);
Cache.Insert?("AMZN",?13.00m);

Here’s how a Web form retrieves items from the application cache:

decimal?amzn?=?(decimal)?Cache["AMZN"];
decimal?intc?=?(decimal)?Cache["INTC"];
decimal?msft?=?(decimal)?Cache["MSFT"];

As with application state, the cast is necessary to let the compiler know what kind of items were retrieved from the cache. To remove an item from the application cache, call Cache.Remove.

Locking and Unlocking

The application cache doesn’t have Lock and UnLock methods as application state does. But that doesn’t mean locking isn’t necessary; it means you have to come up with your own mechanism for doing it. System.Threading.ReaderWriterLock is the perfect tool for the job. Assuming rwlock is an instance of ReaderWriterLock, here’s how you’d lock the application cache during an update:

rwlock.AcquireWriterLock?(Timeout.Infinite);
try?{
????Cache["ItemsSold"]?=?(int)?Cache["ItemsSold"]?+?1;
????Cache["ItemsLeft"]?=?(int)?Cache["ItemsLeft"]?-?1;
}
finally?{
????rwlock.ReleaseWriterLock?();
}

And here’s how you’d read “ItemsSold” and “ItemsLeft” values from the cache:

rwlock.AcquireReaderLock?(Timeout.Infinite);
try?{
????int?sold?=?(int)?Cache["ItemsSold"];
????int?left?=?(int)?Cache["ItemsLeft"];
}
finally?{
????rwlock.ReleaseReaderLock?();
}

As with application state, locking the application cache is necessary only when performing multistep updates that are to be treated as atomic operations.

Expiration Policies

If you use the application cache as shown above—that is, if you do nothing more than add static items and later retrieve them—then the application cache is little better than application state. The real power of the cache comes into play when you assign items expiration policies and process the callbacks that fire when the items expire. The following example, which is taken from a Global.asax file, initializes the application cache with a Hashtable containing three stock prices when the application starts up. It also sets the item to expire 5 minutes after it’s added to the cache:

<script?language="C#" runat="server">
??void?Application_Start?()
??{
??????Hashtable?stocks?=?new?Hashtable?();
??????stocks.Add?("AMZN",?10.00m);
??????stocks.Add?("INTC",?20.00m);
??????stocks.Add?("MSFT",?30.00m);

??????Context.Cache.Insert?("Stocks",?stocks,?null,
??????????DateTime.Now.AddMinutes?(5),?Cache.NoSlidingExpiration);
??}
</script>

Insert’s fourth parameter—a DateTime value specifying a time 5 minutes hence—tells ASP.NET to remove the item from the cache in 5 minutes. That’s called an absolute expiration. As an alternative, you can assign the item a sliding expiration by passing a TimeSpan value in the fifth parameter and Cache.NoAbsoluteExpiration in the fourth. A sliding expiration configures the item to expire when it has not been accessed (retrieved from the cache) for a specified length of time.

Absolute expirations and sliding expirations are one way to define expiration policies. Another option is to use Insert’s third parameter to establish a dependency between an item added to the cache and one or more files or directories. When the file or directory changes—when the file is modified, for example—ASP.NET removes the item from the cache. The following code sample 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?("Stocks.xml"));
Context.Cache.Insert?("Stocks",?ds,
????new?CacheDependency?(Server.MapPath?("Stocks.xml")));

Used this way, a CacheDependency object defines a dependency between a cached item and a file or directory. You can also use CacheDependency to set an item to expire when another item in the cache expires. 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:

Context.Cache.Insert?("Stocks",?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 retrieve it. That’s important, because it means code that retrieves an item from the application cache should always verify that the reference to the item returned by the cache isn’t null—unless, of course, the item was marked NotRemovable.

Cache Removal Callbacks

All items except those marked NotRemovable are subject to removal from the cache at any time if ASP.NET needs the memory for other purposes. If you’d like to be notified when an item is removed, you can pass a CacheItemRemovedCallback delegate to Insert identifying the method you want ASP.NET to call if and when it removes the item from the cache. The following example extends one of the examples in the previous section by adding a DataSet to the application cache, configuring it to expire when the XML file it’s initialized from changes, and automatically replacing the old DataSet with a new one using a callback method:

<%@?Import?Namespace="System.Data" %>

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

??void?Application_Start?()
??{
??????_cache?=?Context.Cache;
??????_path?=?Context.Server.MapPath?("Stocks.xml");

??????DataSet?ds?=?new?DataSet?();
??????ds.ReadXml?(_path);

??????_cache.Insert?("Stocks",?ds,?new?CacheDependency?(_path),
??????????Cache.NoAbsoluteExpiration,?Cache.NoSlidingExpiration,
??????????CacheItemPriority.Default,
??????????new?CacheItemRemovedCallback?(RefreshDataSet));
??}

??static?void?RefreshDataSet?(String?key,?Object?item,
??????CacheItemRemovedReason?reason)
??{
??????DataSet?ds?=?new?DataSet?();
??????ds.ReadXml?(_path);

??????_cache.Insert?("Stocks",?ds,?new?CacheDependency?(_path),
??????????Cache.NoAbsoluteExpiration,?Cache.NoSlidingExpiration,
??????????CacheItemPriority.Default,
??????????new?CacheItemRemovedCallback?(RefreshDataSet));
??}
</script>

When RefreshDataSet (or any other CacheItemRemovedCallback method) is called, the first parameter identifies the string that the item was keyed with, the second identifies the item itself, and the third specifies why the item was removed. This example is simple enough that no callback parameters need to be examined. In a more complex application that stores multiple items in the application cache, however, you’d probably use at least the first of the three parameters to determine which item needs refreshing.

The Cache.Add Method

Earlier, I showed you how to add items to the application cache using Insert and []. You can also add items with Cache.Add. Unlike Insert, however, Add isn’t overloaded to support simplified usage; when you call it, you have to provide seven different parameters, as shown here:

Context.Cache.Add?("Stocks",?ds,?null,
????Cache.NoAbsoluteExpiration,?Cache.NoSlidingExpiration,
????CacheItemPriority.Default,?null);

Add doesn’t behave exactly like Insert. Add adds an item to the cache, but only if the key you specify in Add’s first parameter doesn’t already exist. By contrast, Insert always adds an item to the cache. If the key specified in Insert’s first parameter already exists, Insert simply replaces the old item with the new one.

The SmartQuotes Application

For an example of how the ASP.NET application cache might be used to improve performance, consider the following ASPX file:

<%@?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?=?new?StreamReader?(Server.MapPath?("Quotes.txt"));

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

??????reader.Close?();

??????Random?rand?=?new?Random?();
??????int?index?=?rand.Next?(0,?quotes.Count?-?1);
??????Output.Text?=?(string)?quotes[index];
??}
</script>

Each time this ASPX file is requested, it opens a text file named Quotes.txt, reads its contents, and displays a randomly selected line. If Quotes.txt contains a collection of famous quotations, a randomly selected quotation appears each time the page is refreshed.

So what’s wrong with this picture? Nothing—unless, that is, you value performance. Each time the page is requested, it opens and reads the text file. Consequently, each and every request results in a physical file access. File accesses impede performance because file I/O is a relatively time-consuming undertaking.

This ASPX file, simple as it is, can benefit greatly from the ASP.NET application cache. Suppose that instead of reading Quotes.txt every time the page is requested, you read it once—when the application starts up—and store it in the application cache. Rather than physically access the file, the ASPX file could then retrieve a line directly from the application cache. Furthermore, the cached data could be configured so that it’s deleted from the cache if the file that it comes from changes. You could write 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.

Figure 9-5 shows what the output looks like for a Web application that fits the description in the previous paragraph. Figure 9-6 contains the source code. Global.asax’s Application_Start method reads the contents of Quotes.txt into an ArrayList and inserts the ArrayList into the application cache. It also establishes a dependency between the ArrayList and Quotes.txt so that if the latter changes, the ArrayList is removed from the cache and Global.asax’s RefreshQuotes method is called. RefreshQuotes refreshes the cache by rereading the file and placing the resulting ArrayList in the cache. The ASPX file—SmartQuotes.aspx—retrieves the ArrayList and displays a randomly selected line. And just in case it hits the cache after the ArrayList is deleted and before the new one is added, SmartQuotes.aspx displays a “Server busy” message if the cache read returns a null reference. Refreshing the page again should replace “Server busy” with a famous quotation. To try the application for yourself, copy the source code files to wwwroot or the virtual directory of your choice, open SmartQuotes.aspx in your browser, and refresh the page a few times.

Figure 9-5
The SmartQuotes Web page in action.
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?();

??????if?(quotes?!=?null)?{
??????????_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?();

??????if?(quotes?!=?null)?{
??????????_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);
??????}
??????catch?(IOException)?{
??????????return?null;
??????}
??????finally?{
??????????if?(reader?!=?null)
??????????????reader.Close?();
??????}
??????return?quotes;
??}
</script>
Figure 9-6
The SmartQuotes source code.
SmartQuotes.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?=?(ArrayList)?Cache["Quotes"];

??????if?(quotes?!=?null)?{
??????????Random?rand?=?new?Random?();
??????????int?index?=?rand.Next?(0,?quotes.Count?-?1);
??????????Output.Text?=?(string)?quotes[index];
??????}
??????else?{
??????????Output.Text?= "Server?busy";
??????}
??}
</script>
Quotes.txt
<h3>Give?me?chastity?and?continence,?but?not?yet.</h3>
<i>Saint?Augustine</i>
<h3>The?use?of?COBOL?cripples?the?mind;?its?teaching?should?
??therefore?be?regarded?as?a?criminal?offense.</h3><i>Edsger?
??Dijkstra</i>
<h3>C?makes?it?easy?to?shoot?yourself?in?the?foot;?C++?makes?it?
??harder,?but?when?you?do,?it?blows?away?your?whole?leg.</h3>
??<i>Bjarne?Stroustrup</i>
<h3>A?programmer?is?a?device?for?turning?coffee?into?code.</h3>
??<i>Jeff?Prosise?(with?an?assist?from?Paul?Erdos)</i>
<h3>I?have?not?failed.?I've?just?found?10,000?ways?that?won't?
??work.</h3><i>Thomas?Edison</i>
<h3>Blessed?is?the?man?who,?having?nothing?to?say,?abstains?from?
??giving?wordy?evidence?of?the?fact.</h3><i>George?Eliot</i>
<h3>I?think?there?is?a?world?market?for?maybe?five?computers.</h3>
??<i>Thomas?Watson</i>
<h3>Computers?in?the?future?may?weigh?no?more?than?1.5?tons.</h3>
??<i>Popular?Mechanics</i>
<h3>I?have?traveled?the?length?and?breadth?of?this?country?and?talked?
??with?the?best?people,?and?I?can?assure?you?that?data?processing?is?a?
??fad?that?won't?last?out?the?year.</h3><i>Prentice-Hall?business?books
??editor</i>
<h3>640K?ought?to?be?enough?for?anybody.</h3><i>Bill?Gates</i>