4. Designing a Custom Dependency
Let’s say it up front: writing a custom cache
dependency object is no picnic. You should have a very good reason to do
so, and you should carefully design the new functionality before
proceeding. As mentioned, from ASP.NET 2.0 onward the CacheDependency
class is inheritable—you can derive your own class from it to implement
an external source of events to invalidate cached items.
The base CacheDependency
class handles all the wiring of the new dependency object to the
ASP.NET cache and all the issues surrounding synchronization and
disposal. It also saves you from implementing a start-time feature from
scratch—you inherit that capability from the base class constructors.
(The start-time feature allows you to start tracking dependencies at a
particular time.)
Let’s start reviewing the original limitations of CacheDependency that have led to removing the sealed attribute on the class, making it fully inheritable.
What Cache Dependencies Cannot Do in ASP.NET 1.x
In ASP.NET 1.x, a cached item can be subject to
four types of dependencies: time, files, other items, and other
dependencies. The ASP.NET 1.x Cache
object addresses many developers’ needs and has made building in-memory
collections of frequently accessed data much easier and more effective.
However, this mechanism is not perfect, nor is it extensible.
Let’s briefly consider a real-world scenario.
What type of data do you think a distributed data-driven application
would place in the ASP.NET Cache? In
many cases, it would simply be the results of a database query. But
unless you code it yourself—which can really be tricky—the object
doesn’t support database dependency. A database dependency would
invalidate a cached result set when a certain database table changes. In
ASP.NET 1.x, the CacheDependency
class is sealed and closed to any form of customization that gives
developers a chance to invalidate cached items based on user-defined
conditions.
As far as the Cache
object is concerned, the biggest difference between ASP.NET 1.x and
newer versions is that newer versions support custom dependencies. This
was achieved by making the CacheDependency class inheritable and providing a made-to-measure SqlCacheDependency cache that provides built-in database dependency limited to SQL Server 7.0 and later.
Extensions to the CacheDependency Base Class
To fully support derived classes and to
facilitate their integration into the ASP.NET caching infrastructure, a
bunch of new public and protected members have been added to the CacheDependency class. They are summarized in Table 6.
Table 6. New Members of the CacheDependency Class
Member | Description |
---|
DependencyDispose | Protected method. It releases the resources used by the class. |
GetUniqueID | Public method. It retrieves a unique string identifier for the object. |
NotifyDependencyChanged | Protected method. It notifies the base class that the dependency represented by this object has changed. |
SetUtcLastModified | Protected method. It marks the time when a dependency last changed. |
UtcLastModified | Public
read-only property. It gets the time when the dependency was last
changed. This property also exists in version 1.x, but it is not
publicly accessible. |
As mentioned, a custom dependency class relies on its parent for any interaction with the Cache object. The NotifyDependencyChanged method is called by classes that inherit CacheDependency to tell the base class that the dependent item has changed. In response, 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.
Getting Change Notifications
As you might have noticed, nothing in the public interface of the base CacheDependency class allows you to insert code to check whether a given condition—the heart of the dependency—is met. Why is this? The CacheDependency class was designed to support only a limited set of well-known dependencies—against changes to files or other items.
To detect file changes, the CacheDependency
object internally sets up a file monitor object and receives a call
from it whenever the monitored file or directory changes. The CacheDependency class creates a FileSystemWatcher object and passes it an event handler. A similar approach is used to establish a programmatic link between the CacheDependency object and the Cache object and its items. The Cache object invokes a CacheDependency internal method when one of the monitored items changes. What does this all mean to the developer?
A custom dependency object must be able to
receive notifications from the external data source it is monitoring. In
most cases, this is really complicated if you can’t bind to existing
notification mechanisms (such as file system monitor or SQL Server 2005
notifications). When the notification of a change in the source is
detected, the dependency uses the parent’s infrastructure to notify the
cache of the event. We’ll consider a practical example in a moment.
The AggregateCacheDependency Class
Starting with ASP.NET 2.0, not only can you
create a single dependency on an entry, you can also aggregate
dependencies. For example, you can make a cache entry dependent on both a
disk file and a SQL Server table. The following code snippet shows how
to create a cache entry, named MyData, that is dependent on two different files:
// Creates an array of CacheDependency objects
CacheDependency dep1 = new CacheDependency(fileName1);
CacheDependency dep2 = new CacheDependency(fileName2);
CacheDependency deps[] = {dep1, dep2};
// Creates an aggregate object
AggregateCacheDependency aggDep = new AggregateCacheDependency();
aggDep.Add(deps);
Cache.Insert("MyData", data, aggDep)
Any custom cache dependency object, including SqlCacheDependency, inherits CacheDependency, so the array of dependencies can contain virtually any type of dependency.
The AggregateCacheDependency class is built as a custom cache dependency object and inherits the base CacheDependency class.
5. A Cache Dependency for XML Data
Suppose your application gets some key data from
a custom XML file and you don’t want to access the file on disk for
every request. So you decide to cache the contents of the XML file, but
still you’d love to detect changes to the file that occur while the
application is up and running. Is this possible? You bet. You arrange a
file dependency and you’re done.
In this case, though,
any update to the file that modifies the timestamp is perceived as a
critical change. As a result, the related entry in the cache is
invalidated and you’re left with no choice other than re-reading the XML
data from the disk. The rub here is that you are forced to re-read
everything even if the change is limited to a comment or to a node that
is not relevant to your application.
Because you want the cached data to be
invalidated only when certain nodes change, you create a made-to-measure
cache dependency class to monitor the return value of a given XPath
expression on an XML file.
Note
If the target data source
provides you with a built-in and totally asynchronous notification
mechanism (such as the command notification mechanism of SQL Server
2005), you just use it. Otherwise, to detect changes in the monitored
data source, you can only poll the resource at a reasonable rate. |
Designing the XmlDataCacheDependency Class
To better understand the concept of custom
dependencies, think of the following example. You need to cache the
inner text of a particular node in an XML file. You can define a custom
dependency class that caches the current value upon instantiation and
reads the file periodically to detect changes. When a change is
detected, the cached item bound to the dependency is invalidated.
Note
Admittedly, polling
might not be the right approach for this particular problem. Later on,
in fact, I’ll briefly discuss a more effective implementation. Be aware,
though, that polling is a valid and common technique for custom cache
dependencies. |
A good way to poll a local or remote resource is through a timer callback. Let’s break the procedure into a few steps:
1. | The custom XmlDataCacheDependency
class gets ready for the overall functionality. It initializes some
internal properties and caches the polling rate, file name, and XPath
expression to find the subtree to monitor.
|
2. | After initialization, the dependency object sets up a timer callback to access the file periodically and check contents.
|
3. | In
the callback, the return value of the XPath expression is compared to
the previously stored value. If the two values differ, the linked cache
item is promptly invalidated.
|
There’s no need for the developer to specify details on how the cache dependency is broken or set up. The CacheDependency class in ASP.NET 2.0 takes care of it entirely.
Note
If you’re curious to know how the Cache detects when a dependency is broken, read on. When an item bound to a custom dependency object is added to the Cache, an additional entry is created and linked to the initial item. NotifyDependencyChanged simply dirties this additional element which, in turn, invalidates the original cache item. Figure 3 illustrates the connections.
|
Implementing the Dependency
The following source code shows the core implementation of the custom XmlDataCacheDependency class:
public class XmlDataCacheDependency : CacheDependency
{
// Internal members
static Timer _timer;
int _pollSecs = 10;
string _fileName;
string _xpathExpression;
string _currentValue;
public XmlDataCacheDependency(string file, string xpath, int pollTime)
{
// Set internal members
_fileName = file;
_xpathExpression = xpath;
_pollSecs = pollTime;
// Get the current value
_currentValue = CheckFile();
// Set the timer
if (_timer == null) {
int ms = _pollSecs * 1000;
TimerCallback cb = new TimerCallback(XmlDataCallback);
_timer = new Timer(cb, this, ms, ms);
}
}
public string CurrentValue
{
get { return _currentValue; }
}
public void XmlDataCallback(object sender)
{
// Get a reference to THIS dependency object
XmlDataCacheDependency dep = (XmlDataCacheDependency) sender;
// Check for changes and notify the base class if any are found
string value = CheckFile();
if (!String.Equals(_currentValue, value))
dep.NotifyDependencyChanged(dep, EventArgs.Empty);
}
public string CheckFile()
{
// Evaluates the XPath expression in the file
XmlDocument doc = new XmlDocument();
doc.Load(_fileName);
XmlNode node = doc.SelectSingleNode(_xpathExpression);
return node.InnerText;
}
protected override void DependencyDispose()
{
// Kill the timer and then proceed as usual
_timer.Dispose();
_timer = null;
base.DependencyDispose();
}
}
When the cache dependency is created, the file
is parsed and the value of the XPath expression is stored in an internal
member. At the same time, a timer is started to repeat the operation at
regular intervals. The return value is compared against the value
stored in the constructor code. If the two are different, the NotifyDependencyChanged method is invoked on the base CacheDependency class to invalidate the linked content in the ASP.NET Cache.
Testing the Custom Dependency
How can you use this dependency class in a Web application? It’s as easy as it seems—you just use it in any scenario where a CacheDependency object is acceptable. For example, you create an instance of the class in the Page_Load event and pass it to the Cache.Insert method:
protected const string CacheKeyName = "MyData";
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
// Create a new entry with a custom dependency
XmlDataCacheDependency dep = new XmlDataCacheDependency(
Server.MapPath("employees.xml"),
"MyDataSet/NorthwindEmployees/Employee[employeeid=3]/lastname",
1);
Cache.Insert(CacheKeyName, dep.CurrentValue, dep);
}
// Refresh the UI
Msg.Text = Display();
}
You write the rest of the page as usual, paying close attention to accessing the specified Cache key. The reason for this is that because of the dependency, the key could be null. Here’s an example:
protected string Display()
{
object o = Cache[CacheKeyName];
if (o == null)
return "[No data available--dependency broken]";
else
return (string) o;
}
The XmlDataCacheDependency
object allows you to control changes that occur on a file and decide
which are relevant and might require you to invalidate the cache. The
sample dependency uses XPath expressions to identify a subset of nodes
to monitor for changes. For simplicity, only the first node of the
output of the XPath expression is considered. The sample XPath
expression monitors in the sample employees.xml file the lastname node of the subtree where employeeid=3:
<MyDataSet>
<NorthwindEmployees>
...
<Employee>
<employeeid>3</employeeid>
<lastname>Leverling</lastname>
<firstname>Janet</firstname>
<title>Sales Representative</title>
</Employee>
...
</NorthwindEmployees>
</MyDataSet>
The XML file, the cache dependency object, and the preceding sample page produce the output shown in Figure 4.
The
screen shot at the top is what users see when they first invoke the
page. The page at the bottom is what they get when the cached value is
invalidated because of a change in the monitored node of the XML file.
Note that changes to other nodes, except lastname where employeeid=3, are blissfully ignored and don’t affect the cached value.
Note
I
decided to implement polling in this sample custom dependency because
polling is a pretty common, often mandatory, approach for custom
dependencies. However, in this particular case polling is not the best
option. You could set a FileSystemWatcher
object and watch for changes to the XML file. When a change is
detected, you execute the XPath expression to see whether the change is
relevant for the dependency. Using an asynchronous notifier, if
available, results in much better performance. |