Cutting Edge

Implement Custom Cache Dependencies in ASP.NET 1.x

Dino Esposito

Code download available at:CuttingEdge0407.exe(128 KB)

Contents

What's a Cache Dependency, Anyway?
Cache Dependencies in ASP.NET 2.0
Custom Cache Dependencies in ASP.NET 1.x
Setting Up the Timer
Creating a Web Service Dependency
Database Dependencies in ASP.NET 1.x
Conclusion

One of the most compelling improvements that ASP.NET brought to ASP programming was the Cache object. The Cache has some similarities to the Application object and is a container of global data (as opposed to session-specific data) that features a fair number of innovative characteristics. At its core, the ASP.NET Cache is a sealed data container class built around a hashtable and defined in the System.Web.Caching namespace. The Cache object is central to the whole ASP.NET infrastructure. Various runtime components use the ASP.NET Cache to store working data. For example, the output of cached pages and controls is stored there. Likewise, applications that keep session state in memory use the ASP.NET Cache to store it.

The Cache object is made up of two independent data stores—a public and a private cache. The private cache is reserved for system components and is accessed through a private API. The public cache is the one that is available to developers. In order to enable you to loop through the public store, the Cache class implements the IEnumerable interface.

Unlike data stored in the Application object, items stored in the Cache object can have a number of additional attributes applied to them, including an expiration policy, a priority, and one or more dependencies. These attributes form the substrate of the advanced Cache features such as the ability to remove least-used items, have items expire at a given time, and set logical connections between a group of items and disk files.

Like many other ASP.NET features, the Cache object has whetted the appetites of developers and given them more ideas for features to request in future versions. The most requested new Cache features are most likely support for database-driven data invalidation and custom dependencies. And if they aren't, they should be. The good news is that these two features will be available in ASP.NET 2.0 through a dazzling new class, SqlCacheDependency, which links cached items to database tables, and through the newly unsealed CacheDependency class.

The SqlCacheDependency class detects changes to tables in SQL Server™ 7.0, SQL Server 2000, and SQL Server 2005 (formerly code-named "Yukon") and invalidates all dependent cache entries. In ASP.NET 2.0, CacheDependency has new virtual members and, more importantly, is unsealed and can be derived from. SqlCacheDependency derives from CacheDependency.

In this column, I won't go through all the new caching features of ASP.NET 2.0, but I'll demonstrate how to implement similar custom cache dependencies in ASP.NET 1.x. A good understanding of cache dependencies is crucial if you are to create custom dependencies. So without further ado, let's start with a quick refresher of what a cache dependency is and how it works.

What's a Cache Dependency, Anyway?

The whole ASP.NET cache dependency mechanism is encapsulated in the CacheDependency class. This class represents a relationship between a cached item and an array of objects like files, directories, and other cached objects. To establish a dependency between a cached item and an external component, you add the item to the ASP.NET Cache object using a specific overload of the Insert method, like so:

CacheDependency dep = new CacheDependency(fileName); cache.Insert(key, value, dep);

In this code snippet, the item is fully identified by the key/value pair and its lifetime is bound to the timestamp of the specified file name. The item will automatically be removed from the Cache when the timestamp of the specified file changes. To add a new item to the Cache you can also use the Add method or the set accessor of the Item property:

Cache[key] = value;

The Add method is not overloaded and it differs from the Insert method in that it throws an exception if the specified item already exists. (Insert, on the other hand, will overwrite the existing item.) You should note that if you use the set accessor of the Item property to add a new item, the item is correctly inserted into the cache, but no dependency is created. As a result of this, the item will be removed only when the application shuts down or if the item is explicitly removed.

A cached item can also be bound to other cached items instead of or in addition to file dependencies. The following code snippet shows how to accomplish this:

CacheDependency dep; dep = new CacheDependency(fileNames, otherKeys); Cache.Insert(key, value, dep);

When either the files or the other cached items change, the recently added cache object is invalidated and then removed. To make an item dependent only upon a cached item, you set the file name parameter to null. A cache dependency can be subordinate to another cache dependency. This feature is particularly useful for implementing cascading changes to stored items. So much for the programming interface of the CacheDependency class; now it's time to take a closer look at its internal implementation and interaction with the Cache data store.

Overall, items are automatically removed from the cache based on time, file, and key dependencies. It is interesting to look at how each type of dependency is handled. Time and key dependencies are managed by the Cache object itself. Items with an expiration are associated with a timer and removed at the end of the specified countdown interval.

Resolving key dependencies is just one of the many tasks important to the update of the cache. Whenever a write operation occurs, an internal update process fires and frees all of the items that have a broken dependency.

The file change notification mechanism keeps track of file dependencies. It's an operating system function that various ASP.NET modules, including the HTTP runtime, use extensively. When a file dependency is created, the ASP.NET Cache starts to monitor that file or directory. Thanks to the capabilities of the OS, any change on a monitored resource results in an event raised to the Cache object. The handler of this internal event takes care of the removal of the linked item.

That's the only way in ASP.NET 1.x to force the Cache to evict stored elements. This means that any form of custom dependency must be implemented in terms of file, time, or, more likely, key dependencies. So what's a custom cache dependency? It is primarily a class that listens to a data source for changes. When a change is detected, the class bubbles that change up to a particular stored cache item so that the item is evicted from the cache. This loose description can be translated into three actions that any custom cache dependency class should take in ASP.NET 1.x.

  • Define the data source to listen to
  • Define a channel with the Cache to bubble changes up
  • Define a custom API

The first two points are common to all versions of ASP.NET. However, only ASP.NET 2.0 provides an API that will create and manage custom dependencies. Let's proceed with a quick look at ASP.NET 2.0 cache dependencies before taking the plunge into my ASP.NET 1.x implementation.

Cache Dependencies in ASP.NET 2.0

As I mentioned, in ASP.NET 2.0 the CacheDependency class is unsealed and therefore inheritable. Subsequently, a custom dependency is simply a class that inherits from CacheDependency and implements a custom algorithm to detect changes in a given data source. Each detected change then causes an appropriate action to invalidate items in the cache. The use of inheritance guarantees that no breaking changes will ever be introduced in code ported from ASP.NET 1.x applications. In addition, there is no risk that your class misbehaves with the Cache object. The base class will handle all the wiring of the dependency object to the ASP.NET Cache object and all the issues surrounding synchronization and disposal. On the other hand, inheritance means that the memory footprint of your dependency class might be bigger than needed because your cache dependency class picks up all base class functionality, whether it needs it or not. Such functionality includes constructors that accept arrays of files or create dependencies on other cached items.

A good example of a custom dependency class is the aforementioned SqlCacheDependency. It implements database dependencies by listening to table changes on SQL Server 7.0, SQL Server 2000, and SQL Server 2005. Figure 1 lists the new members added to the CacheDependency class to support custom dependencies.

Figure 1 New Members of CacheDependency Class

Member Description
DependencyDispose Releases the resources used by the class
FinishInit Informs the base class that it's safe to call DependencyDispose
GetUniqueId Retrieves a unique string identifier for the object
NotifyDependencyChanged Notifies the base class that the dependency represented by this object has changed
SetUtcLastModified Marks the time when a dependency last changed
HasChanged Indicates whether the dependency has changed; this property also exists in version 1.x, but is not virtual
UtcLastModified Gets the time when the dependency was last changed; this property also exists in version 1.x, but is not publicly accessible

A custom dependency class relies on its parent for any needed interaction with the Cache object. The NotifyDependencyChanged method is called by classes that inherit from CacheDependency to tell the base class that the dependent item has changed. In response to this, the base class updates the values of the HasChanged and UtcLastModified properties. Any cleanup code needed when the custom cache dependency object is dismissed should go into the DependencyDispose method.

The structure of a custom dependency object follows the pattern outlined in Figure 2. The class uses a timer to poll the data source at regular intervals and maintains a reference to the current data. When the data downloaded from the source is newer than the current copy, the corresponding item in the cache is invalidated. A call to the new NotifyDependencyChanged method breaks the internal dependency so that the Cache update process can evict the item.

Figure 2 Custom Dependency Object

public class YourCacheDependency: CacheDependency { static System.Threading.Timer _timer; int _pollTime; object _currentValue; public YourCacheDependency(int pollTime) { // Set some internal members using input parameters _pollTime = pollTime; // Get the current value from the source _currentValue = GetCurrentValue(); // Set up the timer for next reads if (_timer == null) { TimerCallback fun = new TimerCallback(CheckDependencyCallback); _timer = new Timer(func, this, pollTime, pollTime); } FinishInit(); } public void CheckDependencyCallback(object sender) { YourCacheDependency dep = (YourCacheDependency) sender; // Get the current value from the source object _value = GetCurrentValue(); // Compare to the current value if (!_value.Equals(_currentValue)) dep.NotifyDependencyChanged(dep, EventArgs.Empty); } private object GetCurrentValue() { // Check the data source for changes } protected override void DependencyDispose() { if (_timer != null) { _timer.Dispose(); _timer = null; } base.DependencyDispose(); } }

Figure 3** SqlCacheDependency with SQL Server 2005 **

Note that in ASP.NET (no matter what the version) there are only two possible models for detecting changes—polling and notification. Polling means that the dependency class will periodically check the data source for changes. The notification mechanism is based on an external component that pushes changes to the dependency class in an asynchronous manner. A good example is the aforementioned file change notification mechanism. In ASP.NET 2.0, an implementation of the notification model is represented by the SqlCacheDependency class when it is connected to a SQL Server 2005 database. The overall picture is shown in Figure 3. The following is the code to set up the dependency:

// data is a DataTable filled using this same command SqlCommand cmd = new SqlCommand( "SELECT * FROM Customers WHERE country='USA'", conn); SqlCacheDependency dep = new SqlCacheDependency(cmd); Cache.Insert("SqlSource", data, dep);

In this case the SqlCacheDependency class listens to data coming from a SQL Server 2005 component—the Notification Delivery Service. The service monitors all the tables involved with the given query and invokes a callback function whenever something happens that modifies the resultset generated by the query. Internally, SqlCacheDependency exploits the new notification feature of SQL Server 2005 and runs code like the following:

// cmd is the SqlCommand object defined above SqlDependency dep = new SqlDependency(cmd); dep.OnChanged += new OnChangedEventHandler(OnDepChanged);

The callback function defined by SqlCacheDependency—the OnDepChanged method—calls NotifyDependencyChanged to invalidate the corresponding cache entry.

Custom Cache Dependencies in ASP.NET 1.x

At its core, a custom cache dependency is a class that incorporates a timer to periodically check the data source or that listens for event notifications pushed to it. You must link an instance of this class to a cached item. Because the CacheDependency class is sealed in ASP.NET 1.x, you can't rely on the Cache.Insert method to establish this link automatically. In addition, an instance of the custom dependency class must be stored in the ASP.NET Cache to survive for the application's lifetime. When the dependency class detects a change on the data source, it must do something to invalidate the cached item. To do so, you can only use one of the base ASP.NET 1.x expiration techniques—time, file, or key. Time just doesn't apply here. File or key dependencies are both acceptable and I don't see a reason to create or edit a temporary file. So I'll associate each item that has a custom dependency with an additional helper entry. By modifying the helper entry on changes to the monitored data source, you'll automatically invalidate the cached item. A new API is needed to transparently create the helper entry upon insertion of the item with a custom dependency:

AmazonBooksCacheDependency dep = new AmazonBooksCacheDependency(key, author); CacheHelper.Insert(key, dataSet, dep);

The AmazonBooksCacheDependency class represents a dependency on the list of books written by a given author. Some of the information from this data source is relatively static (books, titles, publishers); some other data, however, may vary on a regular, even daily basis. Price, sales rank, reviews, and rating are parameters that may change more than once a day. The previous code sample inserts a DataSet with book information into the ASP.NET cache and makes it dependent on the raw information published on the Amazon Web site. Whenever new information is posted, the cached DataSet is invalidated and refreshed.

You can't just pass an instance of the AmazonBooksCacheDependency class to the Cache.Insert method. That's why I have written a helper method: CacheHelper.Insert. As you can see in Figure 4, the overall programming interface is nearly identical to that of ASP.NET 1.x for standard dependencies and to that of ASP.NET 2.0 for custom dependencies.

Figure 4 Creating Custom Cache Dependencies

using System; using System.Web; namespace MsdnMag { // API to create custom cache dependencies public class CacheHelper { internal static string GetHelperKeyName(string key) { return key + ":MsdnMag:CacheDependency"; } public static void Insert(string key, object keyValue, MsdnMag.CacheDependency dep) { // Create the array of cache keys for the CacheDependency ctor string storageKey = GetHelperKeyName(key); // Create a helper cache key HttpContext.Current.Cache.Insert(storageKey, dep); // Create a standard CacheDependency object and // link it to the previously created string[] rgDepKeys = new string[1]; rgDepKeys[0] = storageKey; System.Web.Caching.CacheDependency keyDep; keyDep = new System.Web.Caching.CacheDependency(null, rgDepKeys); // Create a new entry in the cache and make it dependent on a // helper key HttpContext.Current.Cache.Insert(key, keyValue, keyDep); } } }

Figure 4 shows the source code of the helper API to cache items with a custom dependency. CacheHelper is a class with a couple of static methods, the most important of which is Insert:

static void Insert(string key, object value, MsdnMag.CacheDependency dep)

This method wraps the overload of the Cache's Insert method that takes a CacheDependency object. The Insert method accepts a custom dependency object and does some extra work. Each new cache entry is paired with a second key whose name is programmatically built by concatenating the unique name of the entry with a standard (but arbitrary) string:

internal static string GetHelperKeyName(string key) { return key + ":MsdnMag:CacheDependency"; }

The GetHelperKeyName is marked as internal (called friend, in Visual Basic® .NET) because I want it to be accessible from within other classes defined in the same assembly, but I don't want it to be callable from just any client.

The Insert method in the CacheHelper class does two key things. First, it creates a helper key and stores the custom cache dependency object in it. The helper key is needed to convey notification changes to the principal item to which it is linked. Once placed in the cache, the custom dependency object is up and running all the time and can periodically query the data source looking for changes. When a change is detected, the custom dependency object updates its last modified date property in the cache. In doing so, it breaks the standard dependency between the helper item and the base item. As a result, the base item is evicted from the cache based on a change on an external data source.

Figure 5 contains the source code of the MsdnMag.CacheDependency class. The class is built around a timer and an abstract method—HasChanged. The timer is instantiated in the constructor and executes a given callback method at specified intervals whose length is controlled by the Polling protected member. Polling indicates the extent of the interval in seconds, whereas the .NET Framework timers count in terms of milliseconds.

Figure 5 ASP.NET 1.x CacheDependency Class

using System; using System.Web; using System.Threading; namespace MsdnMag { // Base ASP.NET 1.1 class for custom cache dependencies public abstract class CacheDependency { // ************************************************************** // The internal timer used to poll the data source protected Timer InternalTimer; // Seconds to wait between two successive polls protected int Polling; // Name of the dependent cache key protected string DependentStorageKey; // ************************************************************** // Last update public DateTime UtcLastModified; // ************************************************************** // Class constructor public CacheDependency(string cacheKey) { // Store the name of the cache key to evict in case of changes DependentStorageKey = cacheKey; // Poll every 30 seconds by default Polling = 30; // Set the current time UtcLastModified = DateTime.Now; // Set up the timer if (InternalTimer == null) { int ms = Polling*1000; TimerCallback func = new TimerCallback(InternalTimerCallback); InternalTimer = new Timer(func, this, ms, ms); } } // ************************************************************** // Built-in timer callback that fires an event to the caller private void InternalTimerCallback(object sender) { CacheDependency dep = (CacheDependency) sender; if (HasChanged()) NotifyDependencyChanged(dep); } // ************************************************************** // Must-override member that determines if the monitored source // has changed protected abstract bool HasChanged(); // ************************************************************** // Modify the helper key thus breaking the dependency in the Cache protected virtual void NotifyDependencyChanged(CacheDependency dep) { // Get the name of the helper key string key = CacheHelper.GetHelperKeyName(DependentStorageKey); // Modify the date dep.UtcLastModified = DateTime.Now; // Overwrite the helper key to trigger eviction on the linked // item HttpRuntime.Cache.Insert(key, dep); } }

The constructor of the CacheDependency custom class requires the name of the dependent key. This information is essential for establishing a link between the dependency object and the dependent item. With default ASP.NET dependencies (in both 1.1 and 2.0) this link is implicitly created by the Cache.Insert method when a cache entry is created or updated. The name of the linked item is stored in the DependentStorageKey protected property. Finally, the UtcLastModified date member contains the time of the last update to the dependency. When a change in the data source is detected, an update to this property triggers the eviction of the linked item from the cache.

Setting Up the Timer

As mentioned, there are two basic ways for a dependency object to know about changes in a monitored resource—polling and notification. Basically, either the dependency object sets up a timer and executes a method at specified intervals or registers to receive notifications about changes from an external module. The availability of a notification service is specific to the data source you're working with. SQL Server 2005 and Windows® have one for query and file changes, respectively. If you're going to create a dependency based on a message queue (for example, MSMQ), then a notification model might be appropriate to implement. For the sample dependency object covered in this column I'll use the polling model and implement it through a timer:

if (InternalTimer == null) { TimerCallback func = new TimerCallback(InternalTimerCallback); int ms = Polling*1000; InternalTimer = new Timer(func, this, ms, ms); }

The timer is an instance of the System.Threading.Timer class. The core of the timer is the delegate for the method that will be called periodically. In addition, the timer's constructor lets you specify a due time and a period. The due time is the time to wait before the first execution of the method; the period is the time to wait between subsequent executions of the callback method. It is important to note that you must keep a reference to the timer alive for as long as you use the object. Like any other managed object, the timer is subject to garbage collection if it goes out of scope. The timer callback is a TimerCallback delegate:

public delegate void TimerCallback(object state);

The parameter indicates an object containing specific information relevant to the method. The callback doesn't execute in the thread that created the timer; instead it executes in a separate thread that is provided by the system. The following code snippet illustrates the default implementation of the timer callback for a custom dependency object:

private void InternalTimerCallback(object sender) { // CacheDependency is the custom dep object CacheDependency dep = (CacheDependency) sender; if (dep.HasChanged()) NotifyDependencyChanged(dep); }

The callback calls the HasChanged method on the dependency class and based on the return value invokes the NotifyDependencyChanged method to break the dependency with the cache item. The NotifyDependencyChanged method I wrote belongs to the ASP.NET 1.x custom dependency class, but I named it after the equivalent method in ASP.NET 2.0 just to help you get familiar with ASP.NET 2.0 features more quickly. The method retrieves the name of the helper key and modifies its content. The content is just the dependency object modified to reflect the date of the last update:

dep.UtcLastModified = DateTime.Now; HttpRuntime.Cache.Insert(key, dep);

Since this is running on a background thread, in this case you must use the HttpRuntime object to get a valid reference to the ASP.NET Cache. If you try to reach the Cache through the current HttpContext object—what you normally would do from within a codebehind class—you'll run into a null exception:

HttpContext.Current.Cache.Insert(key, dep);

This line of code is equivalent to calling Cache through HttpRuntime but only within the context of a request, which is not necessarily the case in this instance.

The NotifyDependencyChanged method is invoked by the timer callback, and the timer works independently of page requests. Basically, the dependency class, once placed in the ASP.NET Cache, remains active and running as long as the application lives, as does the timer it incorporates. The timer lives in the context of an ASP.NET application, but not in the context of a particular HTTP request. For this reason, HttpContext.Current just returns null. However, this doesn't mean that the Cache object is unavailable or unreachable. You simply have to take the right route to it; you have to use the HttpRuntime.Cache property.

Creating a Web Service Dependency

As you can see, there's a difference between the ASP.NET 2.0 customized dependency objects and my sample class. In ASP.NET 2.0, the base class doesn't incorporate any timer; each derived class can create its own timer if needed. In this ASP.NET 1.x implementation, I tried to reduce the amount of code that characterizes a particular dependency. I've accomplished this by deriving a new class from MsdnMag.CacheDependency and overriding the abstract HasChanged method:

protected abstract bool HasChanged();

The method is expected to check the monitored data source and return true if changes have occurred. Let's put it all together and write a custom dependency that gets broken if the output of a Web service method changes. (Rob Howard presented a similar ASP.NET 2.0 example at the Microsoft PDC 2003. You can find slides and source code on the ASP.NET site at https://www.asp.net/whidbey/downloads/WSV330_rhoward_demos.zip.)

I'll build a dependency class to monitor books available on Amazon. In general, imagine you have an application that needs to work with relatively static data—data that changes, but not as frequently as to justify reading it back at every postback. So you download it the first time and place it in the ASP.NET Cache. How do you deal with changes? Well, if data is known to change often by your standards, you should use a time dependency that guarantees that your data is updated at regular intervals. If the frequency of changes is low, you can opt for a custom dependency—whenever the data changes on the server, your cached snapshot is automatically invalidated. When the cache entry is accessed next, it returns null and you know that it's time for you to get the records from the server. As you'll see in a moment, my sample code goes beyond this and automatically replaces the data in the cache. Figure 6 shows the full source code of the AmazonBooksCacheDependency class.

Figure 6 AmazonBooksCacheDependency Class for ASP.NET 1.x

using System; using System.Data; using System.Net; using System.IO; using System.Web; using System.Configuration; namespace MsdnMag { // Create a cache dependency on the results of a query to the Amazon // Web service for my books public class AmazonBooksCacheDependency : CacheDependency { protected string AuthorName; public AmazonBooksCacheDependency(string key, string author) : base(key) { AuthorName = author; } public AmazonBooksCacheDependency(string key, string author, int pollTime) : base(key) { AuthorName = author; Polling = pollTime; } protected override bool HasChanged() { // Make access to the Web service string response = GetBooksInfo(); // GetBooksInfo reads all the data and compares that to the // cached snapshot. // If you have access to the Web service, you might want to // use a separate, ad-hoc method to check for changes. For // example, a method that simply returns a Boolean value. // Compare data bool hasChanged = (response != (string) HttpRuntime.Cache[DependentStorageKey]); return hasChanged; } private string GetBooksInfo() { // Get the data from the Amazon Web service string url = "https://xml.amazon.com/onca/xml2?t=webservices-20"; url += "&f=xml"; url += "&mode=books"; url += "&type=heavy"; url += "&dev-t=" + ConfigurationSettings.AppSettings["DevToken"]; url += "&KeywordSearch=" + AuthorName; try { WebRequest req = WebRequest.Create(url); WebResponse result = req.GetResponse(); Stream receiveStream = result.GetResponseStream(); StreamReader rs = new StreamReader(receiveStream); string response = rs.ReadToEnd(); rs.Close(); result.Close(): return response; } catch { return null; } } } }

The class inherits CacheDependency and defines two constructors—one takes the author's name, and one takes both a name and a poll time. The author's name is a parameter specific to the task of this dependency. The HasChanged method downloads book information for a given author and compares that to the snapshot currently stored in the cache. If the two don't match, the method returns false and the current cached item is freed:

private void Refresh() { // Is the author cached already? string author = AuthorName.Text; if (!IsAuthorCached(author)) PrepareCacheForAuthor(author); BindData(author); }

When the user clicks the Refresh button, the code verifies that the books of the specified author have been cached. If not, a new entry in the cache is prepared:

private void PrepareCacheForAuthor(string author) { string key = GetAuthorKey(author); AmazonBooksCacheDependency dep; dep = new AmazonBooksCacheDependency(key, author); // Create the cache entry CacheHelper.Insert(key, GetBooksInfo(author), dep); }

GetBooksInfo uses the Amazon Web service to grab up-to-date book information. The data is an XML string that can be easily loaded into a DataSet. It is stored as a string in an ASP.NET Cache item named after the author. As mentioned, CacheHelper.Insert adds two keys to the cache—one with the actual data and one with the dependency object. The dependency object checks the Amazon Web service periodically and invalidates the cached item when changes have been detected. BindData uses a special method to read from the cache, as shown here:

string ReadFromCacheForAuthor(string author) { string key = GetAuthorKey(author); string data = (string) Cache[key]; if (data == null || data == string.Empty) { data = GetBooksInfo(author); Cache[key] = data; } return data; }

If the contents of the cached item is null or empty, the method refills it with a new call to the Web service. Note that in order to develop with the Amazon Web service you must get a developer token, which you can apply for at https://xml.amazon.com. You'll also find licensing and reuse information there.

It is worth noting that the solution outlined so far works, but could be improved upon. The key problem is with the implementation of the HasChanged method. In the Amazon example, book information is retrieved at every timer interval and compared to the contents currently stored in the cache. If a change is detected, the contents of the cache are freed and a second download occurs with the next read from the cache.

As I've learned from sample implementations in ASP.NET 2.0, you should design the Web service to expose information about data changes through a simple Boolean method. A round-trip is still needed, but this trick will significantly reduce the amount of data being moved over the wire. ASP.NET 2.0 applies this pattern to database dependencies. Triggers set on monitored tables write a flag on a small helper table whenever a change is recorded. The helper table has just a few records—one for each monitored table in a database. The timer callback queries the small table looking for all records with the flag set. The result is guaranteed and the performance is optimal.

If you don't have control over the Web service (which I don't have with Amazon), then I suggest a slightly different approach. Modify the HasChanged method so that it replaces data in the cache when changes are detected. In addition, have it always return false to prevent the custom dependency object from breaking the link and invalidating the data. This guarantees that no double read is needed when changes occur.

Database Dependencies in ASP.NET 1.x

Database dependencies are not supported in ASP.NET 1.1, but Jeff Prosise demonstrated how to achieve them in the April 2003 issue of MSDN® Magazine. The idea is that you define a trigger on a SQL Server table and configure it to monitor insertions, deletions, and updates. When fired, the trigger calls an extended stored procedure to modify a disk file. If you set a dependency between a cached item and this file, the ASP.NET Cache will dispose of it whenever the SQL Server table undergoes some changes. This solution can be reworked and optimized a bit in light of the ASP.NET 2.0 code. For example, you can create a helper table to record changes and a second cache entry to invalidate the cached data whenever changes to your data occur.

Conclusion

Once again, I have presented some code you can use now which is a much simplified version of code that will be completely built when ASP.NET 2.0 is released. You might as well get the functionality you want today, and you'll be ahead of the curve in understanding ASP.NET 2.0 tomorrow.

Send your questions and comments for Dino to  cutting@microsoft.com.

Dino Espositois a Wintellect instructor and consultant based in Italy. Author of Programming ASP.NET and the newest Introducing ASP.NET 2.0 (both from Microsoft Press), he spends most of his time teaching classes on ASP.NET and ADO.NET and speaking at conferences. Reach Dino at cutting@microsoft.com or join the blog at https://weblogs.asp.net/despos.