Caching Application Data

 

Caching Application Data

Centered around the Cache object, the ASP.NET caching API is much more than simply a container of global data shared by all sessions, like the Application object that I briefly discussed in the previous chapter. Preserved for backward compatibility with classic ASP applications, the Application intrinsic object presents itself as a global container of data with an indexer property and a user-callable locking mechanism. The Cache object is a smarter and thread-safe container that can automatically remove unused items, support various forms of dependencies, and optionally provide removal callbacks and priorities.

The Application object is maintained for backward compatibility with legacy applications; new ASP.NET applications should use the Cache object.

The Cache Class

The Cache class is exposed by the System.Web.Caching namespace and is a new entry in the set of tools that provide state management in ASP.NET. The Cache class works like an application-wide repository for data and objects, but this is the only aspect that it has in common with the HttpApplicationState class, as we'll see in a moment.

An instance of the Cache class is created on a per-AppDomain basis and remains valid until that AppDomain is up and running. The current instance of the application's ASP.NET cache is returned by the Cache property of the HttpContext object or the Cache property of the Page object.

Cache and Other State Objects

In spite of their common goal to serve as a global data repository for ASP.NET applications Cache and HttpApplicationState classes are quite different. Cache is a thread-safe object and does not require you to explicitly lock and unlock before access. All critical sections in the internal code are adequately protected using synchronization constructs. Another key difference with the HttpApplicationState class is that data stored in Cache doesn't necessarily live as long as the application does. The Cache object lets you associate a duration as well as a priority with any of the items you store.

Any cached item can be configured to expire after a specified number of seconds, freeing up some memory. By setting a priority on items, you help the Cache object to select which items can be safely disposed of in case of memory shortage. Items can be associated with various types of dependencies, such as the timestamp of one or more files and directories, changes on other cached items, database table changes, and external events. When something happens to break the link, the cached item is invalidated and is no longer accessible by the application.

Both Cache and HttpApplicationState are globally visible classes and span all active sessions. However, neither works in a Web farm or Web garden scenario; in general, they don't work outside the current AppDomain.

Note 

When more than one AppDomain is involved (for example, in a Web farm), presumably all AppDomains would contain the same cached data, assuming that the cached information is not dynamic. Unlike with session state, this isn't too troubling because the assumption is that application-wide static values can be read upon initialization and cache timeout. If the cached information is dynamic, that's a different story. In that case, you should consider a global cross-machine container, as we'll discuss shortly.

The Cache object is unique in its capability to automatically scavenge the memory and get rid of unused items. Aside from that, it provides the same dictionary-based and familiar programming interface as Application and Session. Unlike Session, the Cache class does not store data on a per-user basis. Furthermore, when the session state is managed in-process, all currently running sessions are stored as distinct items in the ASP.NET Cache.

Note 

If you're looking for a global repository object that, like Session, works across a Web farm or Web garden architecture, you might become frustrated. No such object exists in the Microsoft .NET Framework. To build a cross-machine container, you need to resort to a shared and remote resource, such as an external service or perhaps an installation of Microsoft SQL Server or another database. This means that each access to data will require serialization and is subject to network latency. In general, this scheme is complex enough to invalidate most of the advantages you get from data caching. As far as caching is involved, the tradeoff to evaluate is accessing ready-made data versus running the query to fetch a fresh copy of desired data. ASP.NET provides an effective infrastructure for caching data locally because that is what you need most of the time. Adding to the infrastructure to cover Web farms is up to you.

Properties of the Cache Class

The Cache class provides a couple of properties and public fields. The properties let you count and access the various items. The public fields are internal constants used to configure the expiration policy of the cache items. Table 14-1 lists and describes them all.

Table 14-1: Cache Class Properties and Public Fields

Property

Description

Count

Gets the number of items stored in the cache

Item

An indexer property that provides access to the cache item identified by the specified key

NoAbsoluteExpiration

A static constant that indicates a given item will never expire

NoSlidingExpiration

A static constant that indicates sliding expiration is disabled for a given item

The NoAbsoluteExpiration field is of the DateTime type and is set to the DateTime.MaxValue date that is, the largest possible date defined in the .NET Framework. The NoSlidingExpiration field is of the TimeSpan type and is set to TimeSpan.Zero, meaning that sliding expiration is disabled. We'll say more about sliding expiration shortly.

The Item property is a read/write property that can also be used to add new items to the cache. If the key specified as the argument of the Item property does not exist, a new entry is created. Otherwise, the existing entry is overwritten:

Cache["MyItem"] = value; 

The data stored in the cache is generically considered to be of type object, whereas the key must be a case-sensitive string. When you insert a new item in the cache using the Item property, a number of default attributes are assumed. In particular, the item is given no expiration policy, no remove callback, and a normal priority. As a result, the item will stay in the cache indefinitely, until programmatically removed or until the application terminates. To specify any extra arguments and exercise closer control on the item, use the Insert method of the Cache class instead.

Methods of the Cache Class

The methods of the Cache class let you add, remove, and enumerate the items stored. Methods of the Cache class are listed and described in Table 14-2.

Table 14-2: Cache Class Methods

Method

Description

Add

Adds the specified item to the cache. It allows you to specify dependencies, expiration and priority policies, and a remove callback. The call fails if an item with the same key already exists. The method returns the object that represents the newly added item.

Get

Retrieves the value of the specified n item from the cache. The item is identified by key. The method returns null if no item with that key is found. (This method is used to implement the get accessor of the Item property.)

GetEnumerator

Returns a dictionary enumerator object to iterate through all the valid items stored in the casche.

Insert

Inserts the specified item into the cache. Insert provides several overloads and allows you to specify dependencies, expiration and priority policies, and a remove callback. The method is void and, unlike Add, overwrites an existing item having the same key as the item being inserted. (This method is used to implement the set accessor of the Item property.)

Remove

Removes the specified item from the cache. The item is identified by the key. The method returns the instance of the object being removed or null if no item with that key is found.

Both the Add and Insert methods don't accept null values as the key or the value of an item to cache. If null values are used, an exception is thrown. You can configure sliding expiration for an item for no longer than one year. Otherwise, an exception will be raised. Finally, bear in mind that you cannot set both sliding and absolute expirations on the same cached item.

Note 

Add and Insert work in much the same way, but a couple of differences make it worth-while to have both on board. Add fails (but no exception is raised) if the item already exists, whereas Insert overwrites the existing item. In addition, Add has just one signature, while Insert provides several different overloads.

An Interior View

The Cache class inherits from Object and implements the IEnumerable interface. It is a wrapper around an internal class that acts as the true container of the stored data. The real class used to implement the ASP.NET cache varies depending on the number of affinitized CPUs. If only one CPU is available, the class is CacheSingle; otherwise, it is CacheMultiple. In both cases, items are stored in a hashtable and there will be a distinct hashtable for each CPU. It turns out that CacheMultiple manages an array of hashtables. Figure 14-1 illustrates the architecture of the Cache object.

image from book
Figure 14-1: The internal structure of the ASP.NET cache.

The hashtable is divided into two parts public and private elements. In the public portion of the hashtable are placed all items visible to user applications. System-level data, on the other hand, goes in the private section. The cache is a resource extensively used by the ASP.NET runtime itself; system items, though, are neatly separated by application data, and there's no way an application can access a private element on the cache.

The Cache object is mostly a way to restrict applications to read from, and write to, the public segment of the data store. Get and set methods on internal cache classes accept a flag to denote the public attribute of the item. When called from the Cache class, these internal methods always default to the flag that selects public items.

The hashtable containing data is then enhanced and surrounded by other internal components to provide a rich set of programming features. The list includes the implementation of a least recently used (LRU) algorithm to ensure that items can be removed if the system runs short of memory, dependencies, and removal callbacks.

Caution 

On a multiprocessor machine with more than one CPU affinitized with the ASP.NET worker process, each processor ends up getting its own cache object. The various cache objects are not synchronized. In a Web garden configuration, you can't assume that users will return to the same CPU (and worker process) on subsequent requests. So the status of the ASP.NET cache is not guaranteed to be aligned with what the same page did last time.

Working with the ASP.NET Cache

An instance of the Cache object is associated with each running application and shares the associated application's lifetime. The cache holds references to data and proactively verifies validity and expiration. When the system runs short of memory, the Cache object automatically removes some little-used items and frees valuable server resources. Each item when stored into the cache can be given special attributes that determine a priority and an expiration policy. All these are system-provided tools to help programmers control the scavenging mechanism of the ASP.NET cache.

Inserting New Items in the Cache

A cache item is characterized by a handful of attributes that can be specified as input arguments of both Add and Insert. In particular, an item stored in the ASP.NET Cache object can have the following properties:

There are basically three ways to add new items to the ASP.NET Cache object the set accessor of Item property, the Add method, and the Insert method. The Item property allows you to indicate only the key and the value. The Add method has only one signature that includes all the aforementioned arguments. The Insert method is the most flexible of all options and provides the following four overloads:

public void Insert(string, object); public void Insert(string, object, CacheDependency); public void Insert(string, object, CacheDependency, DateTime, TimeSpan); public void Insert(string, object, CacheDependency, DateTime, TimeSpan,     CacheItemPriority, CacheItemRemovedCallback); 

The following code snippet shows the typical call that is performed under the hood when the Item set accessor is used:

Insert(key, value, null, Cache.NoAbsoluteExpiration,     Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); 

If you use the Add method to insert an item whose key matches that of an existing item, no exception is raised, nothing happens, and the method returns null.

Removing Items from the Cache

All items marked with an expiration policy, or a dependency, are automatically removed from the cache when something happens in the system to invalidate them. To programmatically remove an item, on the other hand, you resort to the Remove method. Notice that this method removes any item, including those marked with the highest level of priority (NotRemovable). The following code snippet shows how to call the Remove method:

object oldValue = Cache.Remove("MyItem"); 

Normally, the method returns the value just removed from the cache. However, if the specified key is not found, the method fails and null is returned, but no exception is ever raised.

When items with an associated callback function are removed from the cache, a value from the CacheItemRemovedReason enumeration is passed on to the function to justify the operation. The enumeration includes the values listed in Table 14-3.

Table 14-3: The CacheItemRemovedReason Enumeration

Reason

Description

DependencyChanged

Removed because the associated dependency changed.

Expired

Removed because expired.

Removed

Programmatically removed from the cache using Remove. Notice that a Removed event might also be fired if an existing item is replaced either through Insert or the Item property.

Underused

Removed by the system to free memory.

If the item being removed is associated with a callback, the function is executed immediately after having removed the item.

Tracking Item Dependencies

Items added to the cache through the Add or Insert method can be linked to an array of files and directories as well as to an array of existing cache items, database tables, or external events. The link between the new item and its cache dependency is maintained using an instance of the CacheDependency class. The CacheDependency object can represent a single file or directory or an array of files and directories. In addition, it can also represent an array of cache keys that is, keys of other items stored in the Cache and other custom dependency objects to monitor for example, database tables or external events.

The CacheDependency class has quite a long list of constructors that provide for the possibilities listed in Table 14-4.

Table 14-4: The CacheDependency Constructor List

Constructor

Description

String

A file path that is, a URL to a file or a directory name

String[]

An array of file paths

String, DateTime

A file path monitored starting at the specified time

String[], DateTime

An array of file paths monitored starting at the specified time

String[], String[]

An array of file paths, and an array of cache keys

String[], String[], CacheDependency

An array of file paths, an array of cache keys, and a separate CacheDependency object

String[], String[], DateTime

An array of file paths and an array of cache keys monitored starting at the specified time

String[], String[], CacheDependency, DateTime

An array of file paths, an array of cache keys, and a separate instance of the CacheDependency class monitored starting at the specified time

Any change in any of the monitored objects invalidates the current item. It's interesting to note that you can set a time to start monitoring for changes. By default, monitoring begins right after the item is stored in the cache. A CacheDependency object can be made dependent on another instance of the same class. In this case, any change detected on the items controlled by the separate object results in a broken dependency and the subsequent invalidation of the present item.

Note 

Cache dependencies underwent some significant changes and improvements in ASP.NET 2.0. In the previous version, the CacheDependency class was sealed and not further inheritable. As a result, the only dependency objects you could work with were those linking to files, directories, or other cached items. In ASP.NET 2.0, the CacheDependency class is inheritable and can be used as a base to build custom dependencies. ASP.NET 2.0 comes with a built-in class to monitor database tables for changes. We'll examine custom dependencies shortly.

In the following code snippet, the item is associated with the timestamp of a file. The net effect is that any change made to the file that affects the timestamp invalidates the item, which will then be removed from the cache.

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

Bear in mind that the CacheDependency object needs to take file and directory names expressed through absolute file system paths.

Defining a Removal Callback

Item removal is an event independent from the application's behavior and control. The difficulty with item removal is that because the application is oblivious to what has happened, it attempts to access the removed item later and gets only a null value back. To work around this issue, you can either check for the item's existence before access is attempted or, if you think you need to know about removal in a timely manner, register a callback and reload the item if it's invalidated. This approach makes particularly good sense if the cached item just represents the content of a tracked file or query.

The following code-behind class demonstrates how to read the contents of a Web server's file and cache it with a key named MyData. The item is inserted with a removal callback. The callback simply re-reads and reloads the file if the removal reason is DependencyChanged.

void Load_Click(object sender, EventArgs e) {    AddFileContentsToCache("data.xml"); } void Read_Click(object sender, EventArgs e) {    object data = Cache["MyData"];    if (data == null)    {        contents.Text = "[No data available]";        return;    }    contents.Text = (string) data; } void AddFileContentsToCache(string fileName) {    string file = Server.MapPath(fileName);    StreamReader reader = new StreamReader(file);    string buf = reader.ReadToEnd();    reader.Close();    // Create and display the contents    CreateAndCacheItem(buf, file);    contents.Text = Cache["MyData"].ToString(); } void CreateAndCacheItem(object buf, string file) {    CacheItemRemovedCallback removal;    removal = new CacheItemRemovedCallback(ReloadItemRemoved);    CacheDependency dep = new CacheDependency(file);    Cache.Insert("MyData", buf, dep, Cache.NoAbsoluteExpiration,          Cache.NoSlidingExpiration, CacheItemPriority.Normal, removal); } void ReloadItemRemoved(string key, object value,        CacheItemRemovedReason reason) {    if (reason == CacheItemRemovedReason.DependencyChanged)    {       // At this time the item has been removed. We get fresh data and       // re-insert the item       if (key == "MyData")          AddFileContentsToCache("data.xml");       // This code runs asynchronously with respect to the application,       // as soon as the dependency gets broken. To test it, add here       // some code to trace the event    } } void Remove_Click(object sender, EventArgs e) {     Cache.Remove("MyData"); } 

Figure 14-2 shows a sample page to test the behavior of the caching API when dependencies are used. If the underlying file has changed, the dependency-changed event is notified and the new contents are automatically loaded. So the next time you read from the cache you get fresh data. If the cached item is removed, any successive attempt to read returns null.

image from book
Figure 14-2: A sample page to test the behavior of removal callbacks in the ASP.NET cache.

Note that the item removal callback is a piece of code defined by a user page but automatically run by the Cache object as soon as the removal event is fired. The code contained in the removal callback runs asynchronously with respect to the page. If the removal event is related to a broken dependency, the Cache object will execute the callback as soon as the notification is detected.

If you add an object to the Cache and make it dependent on a file, directory, or key that doesn't exist, the item is regularly cached and marked with a dependency as usual. If the file, directory, or key is created later, the dependency is broken and the cached item is invalidated. In other words, if the dependency item doesn't exist, it's virtually created with a null timestamp or empty content.

Note 

Once an item bound to one or more dependencies is removed from the cache, it stops monitoring for changes. Further changes to, say, the underlying file won't be caught just because the item is no longer in the cache. You can verify this behavior by loading some data, as shown in Figure 14-2. Next, you click Remove to dispose of the item and modify the underlying file. Later, if you try to re-read the item, it'll return null because the element is no longer in the cache.

To define a removal callback, you first declare a variable of type CacheRemovedItemCallback. Next, you instantiate this member with a new delegate object with the right signature:

CacheItemRemovedCallback removal; removal = new CacheItemRemovedCallback(ReloadItemRemoved); 

The CacheDependency object is simply passed the removal delegate member, which executes the actual function code for the Cache object to call back.

Tip 

If you define a removal callback function through a static method, you avoid an instance of the class that contains the method to be kept in memory all the time to support the call back. Static methods (that is, Shared methods according to the Microsoft Visual Basic .NET jargon) are callable on a class even when no instance of the class has been created. Note, though, that this choice raises other issues as far as trying to use the callback to re-insert a removed item. In this case, therefore, you reasonably need to access a method on the page class, which is not permitted from within a static member. To work around this issue, you create a static field, say ThisPage, and set it to the page object (the this keyword in C# or Me in Visual Basic .NET) during the Page_Init event. You then invoke any object-specific method through the static ThisPage member, even from within a static method.

Setting the Items Priority

Each item in the cache is given a priority that is, a value picked up from the CacheItemPriority enumeration. A priority is a value ranging from Low (lowest) to NotRemovable (highest), with the default set to Normal. The priority is supposed to determine the importance of the item for the Cache object. The higher the priority is, the more chances the item has to stay in memory even when the system resources are going dangerously down.

If you want to give a particular priority level to an item being added to the cache, you have to use either the Add or Insert method. The priority can be any value listed in Table 14-5.

Table 14-5: Priority Levels in the Cache Object

Priority

Value

Description

Low

1

Items with this level of priority are the first items to be deleted from the cache as the server frees system memory.

BelowNormal

2

Intermediate level of priority between Normal and Low.

Normal

3

Default priority level. It is assigned to all items added using the Item property.

Default

3

Same as Normal.

AboveNormal

4

Intermediate level of priority between Normal and High.

High

5

Items with this level of priority are the last items to be removed from the cache as the server frees memory.

NotRemovable

6

Items with this level of priority are never removed from the cache. Use this level with extreme care.

The Cache object is designed with two goals in mind. First, it has to be efficient and built for easy programmatical access to the global repository of application data. Second, it has to be smart enough to detect when the system is running low on memory resources and to clear elements to free memory. This trait clearly differentiates the Cache object from HttpApplication-State, which maintains its objects until the end of the application (unless the application itself frees those items). The technique used to eliminate low-priority and seldom-used objects is known as scavenging.

Controlling Data Expiration

Priority level and changed dependencies are two of the causes that could lead a cached item to be automatically garbage-collected from the Cache. Another possible cause for a premature removal from the Cache is infrequent use associated with an expiration policy. By default, all items added to the cache have no expiration date, neither absolute nor relative. If you add items by using either the Add or Insert method, you can choose between two mutually exclusive expiration policies: absolute and sliding expiration.

Absolute expiration is when a cached item is associated with a DateTime value and is removed from the cache as the specified time is reached. The DateTime.MaxValue field, and its more general alias NoAbsoluteExpiration, can be used to indicate the last date value supported by the .NET Framework and to subsequently indicate that the item will never expire.

Sliding expiration implements a sort of relative expiration policy. The idea is that the object expires after a certain interval. In this case, though, the interval is automatically renewed after each access to the item. Sliding expiration is rendered through a TimeSpan object a type that in the .NET Framework represents an interval of time. The TimeSpan.Zero field represents the empty interval and is also the value associated with the NoSlidingExpiration static field on the Cache class. When you cache an item with a sliding expiration of 10 minutes, you use the following code:

Insert(key, value, null, Cache.NoAbsoluteExpiration,     TimeSpan.FromMinutes(10), CacheItemPriority.Normal, null); 

Internally, the item is cached with an absolute expiration date given by the current time plus the specified TimeSpan value. In light of this, the preceding code could have been rewritten as follows:

Insert(key, value, null, DateTime.Now.AddMinutes(10),     Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); 

However, a subtle difference still exists between the two code snippets. In the former case that is, when sliding expiration is explicitly turned on each access to the item resets the absolute expiration date to the time of the last access plus the time span. In the latter case, because sliding expiration is explicitly turned off, any access to the item doesn't change the absolute expiration time.

image from book
Statistics About Memory Usage

Immediately after initialization, the Cache collects statistical information about the memory in the system and the current status of the system resources. Next, it registers a timer to invoke a callback function at one-second intervals. The callback function periodically updates and reviews the memory statistics and, if needed, activates the scavenging module. Memory statistics are collected using a bunch of Win32 API functions to obtain information about the system's current usage of both physical and virtual memory.

The Cache object classifies the status of the system resources in terms of low and high pressure. Each value corresponds to a different percentage of occupied memory. Typically, low pressure is in the range of 15 percent to 40 percent, while high pressure is measured from 45 percent to 65 percent of memory occupation. When the memory pressure exceeds the guard level, seldom-used objects are the first to be removed according to their priority.

Practical Issues

Caching is a critical factor for the success of a Web application. Caching mostly relates to getting quick access to prefetched data that saves you roundtrips, queries, and any other sort of heavy operations. Caching is important also for writing, especially in systems with a high volume of data to be written. By posting requests for writing to a kind of intermediate memory structure, you decouple the main body of the application from the service in charge of writing. Some people call this batch update, but in the end it is nothing more than a form of caching for data to write.

The caching API provides you with the necessary tools to build a bulletproof caching strategy. When it comes to this, though, a few practical issues arise.

Should I Cache or Should I Fetch?

There's just one possible answer to this question it depends. It depends on the characteristics of the application and the expected goals. For an application that must optimize through put and serve requests in the shortest possible amount of time, caching is essential. The quantity of data you cache and the amount of time you cache it are the two parameters you need to play with to arrive at a good solution.

Caching is about reusing data so data that is not often used in the lifetime of the application is not a good candidate for the cache. In addition to being frequently used, cacheable data is also general-purpose data rather than data specific to a request or a session. If your application manages data with these characteristics, cache them with no fear.

Caching is about memory, and memory is relatively cheap. However, a bad application design can easily drive the application to unpleasant out-of-memory errors regardless of the cost of a memory chip. On the other hand, caching can boost the performance just enough to ease your pain and give you more time to devise a serious refactoring.

Sometimes you face users who claim an absolute need for live data. Sure, data parked in the cache is static, unaffected by concurrent events, and not fully participating in the life of the application. Can your users afford data that has not been updated for a few seconds? With a few exceptions, the answer is, "Sure, they can." In a canonical Web application, there's virtually no data that can't be cached at least for a second or two. No matter what end users claim, caching can realistically be applied to the vast majority of scenarios. Real-time systems and systems with a high degree of concurrency (for example, a booking application) are certainly an exception, but most of the time a slight delay of one or two seconds can make the application run faster under stress conditions without affecting the quality of the service.

In the end, you should be considering caching all the time and filter it out in favor of direct data access only in very special situations. As a practical rule, when users claim they need live data, you should try with a counterexample to prove to them that a few seconds of delay are still acceptable and maximize hardware and software investments.

Fetching to get the real data is an option, but it's usually the most expensive one. If you choose that option, make sure you really need it. Accessing cached data is faster if the data you get in this way makes sense to the application. On the other hand, be aware that caching requires memory. If abused, it can lead to out-of-memory errors and performance hits.

Building a Wrapper Cache Object

As mentioned, no data stored in the ASP.NET cache is guaranteed to stay there when a piece of code attempts to read it. For the safety of the application, you should never rely on the value returned by the Get method or the Item property. The following pattern keeps you on the safe side:

object data = Cache["MyData"]; if (data != null) {    // The data is here, process it    ... } 

The code snippet deliberately omits the else branch. What should you do if the requested item is null? You can abort the ongoing operation and display a friendly message to the user, or you can perhaps reload the data with a new fetch. Whatever approach you opt for, it will unlikely fit for just any piece of data you can have in the cache.

When it comes to building a cache layer, you're better off thinking in a domain-based way. You should avoid caching data as individual elements, with the key being the only clue to retrieve the element later. You can build a helper class with domain-specific properties bound to cache entries. Here's an example:

public static class MyCache {    protected static class MyCacheEntries    {       public const string Customers = "Customers";    }    public static CustomerCollection Customers    {       get       {           object o = HttpContext.Current.Cache[MyCacheEntries.Customers];           if (o == null)           {               HttpContext.Current.Trace.Warn("Empty cache--reloading...");               LoadCustomers();           }           return (CustomerCollection) o;       }    }    protected static void LoadCustomers()    {       // Get data       CustomerCollection coll = ProAspNet20.DAL.Customers.LoadAll();       // Set the item (5 seconds duration)       HttpContext.Current.Cache.Insert(MyCacheEntries.Customers, coll,           null, DateTime.Now.AddSeconds(5), Cache.NoSlidingExpiration);     } } 

The MyCache class defines a property named Customers of type CustomerCollection. The contents of this property comes from the sample Data Access Layer (DAL) we discussed in Chapter 9, and it's stored in the cache for a duration of 5 seconds. The Customers property hides all the details of the cache management and ensures the availability of valid data to host pages. If the cached item is not there because it has expired (or it has been removed), the get accessor of the property takes care of reloading the data.

Note 

If you move the preceding code to a non-code-behind class, you can't access the ASP.NET cache object using the plain Cache keyword. Unlike classic ASP, ASP.NET has no intrinsic objects, meaning that all objects you invoke must be public or reachable properties on the current class or its parent. Just as we did in the previous example of the MyCache class, you need to qualify the cache using the static property HttpContext.Current.

A caller page needs only the following code to populate a grid with the results in the cache:

CustomerCollection data = MyCache.Customers; CustomerList.DataTextField = "CompanyName"; CustomerList.DataValueField = "ID"; CustomerList.DataSource = data; CustomerList.DataBind(); 

By writing a wrapper class around the specific data you put into the cache, you can more easily implement a safe pattern for data access that prevents null references and treats each piece of data appropriately. In addition, the resulting code is more readable and easy to maintain.

Note 

This approach is potentially more powerful than using the built-in cache capabilities of data source controls. First and foremost, such a wrapper class encapsulates all the data you need to keep in the cache and not just the data bound to a control. Second, it gives you more control over the implementation you can set priority and removal callback, implement complex dependencies, and choose the name of the entry. Next, it works with any data and not just with ADO.NET objects, as is the case with SqlDataSource and ObjectDataSource. You can use this approach instead while building your own DAL so that you come up with a bunch of classes that support caching to bind to data source controls. If your pages are quite simple (for example, some data bound to a grid or other data-bound controls) and you're using only DataSet or DataTable, the caching infrastructure of data source controls will probably suit your needs.

Enumerating Items in the Cache

Although most of the time you simply access cached items by name, you might find it useful to know how to enumerate the contents of the cache to list all stored public items. As mentioned, the Cache class is a sort of collection that is instantiated during the application's startup. Being a collection, its contents can be easily enumerated using a for?each statement. The following code shows how to copy the current contents of the ASP.NET cache to a newly created DataTable object:

private DataTable CacheToDataTable() {     DataTable dt = CreateDataTable();     foreach(DictionaryEntry elem in HttpContext.Current.Cache)         AddItemToTable(dt, elem);     return dt; } private DataTable CreateDataTable() {     DataTable dt = new DataTable();     dt.Columns.Add("Key", typeof(string));     dt.Columns.Add("Value", typeof(string));     return dt; } private void AddItemToTable(DataTable dt, DictionaryEntry elem) {     DataRow row = dt.NewRow();     row["Key"] = elem.Key.ToString();     row["Value"] = elem.Value.ToString();     dt.Rows.Add(row); } 

The DataTable contains two columns, one for the key and one for the value of the item stored. The value is rendered using the ToString method, meaning that the string and numbers will be loyally rendered but objects will typically be rendered through their class names.

Important 

When you enumerate the items in the cache, only two pieces of information are available the key and the value. From a client page, there's no way to read the priority of a given item or perhaps its expiration policy. When you enumerate the contents of the Cache object, a generic DictionaryEntry object is returned with no property or method pointing to more specific information. To get more information, you should consider using the .NET Reflection API.

Also note that because the Cache object stores data internally using a hashtable, the enumerator returns contained items in an apparently weird order that is neither alphabetical nor time-based. The order in which items are returned, instead, is based on the internal hash code used to index items.

Clearing the Cache

The .NET Framework provides no method on the Cache class to programmatically clear all the content. The following code snippet shows how to build one:

public void Clear() {     foreach(DictionaryEntry elem in Cache)     {         string s = elem.Key.ToString();         Cache.Remove(s);     } } 

Even though the ASP.NET cache is implemented to maintain a neat separation between the application's and system's items, it is preferable that you delete items in the cache individually. If you have several items to maintain, you might want to build your own wrapper class and expose one single method to clear all the cached data.

Cache Synchronization

Whenever you read or write an individual cache item, from a threading perspective you're absolutely safe. The ASP.NET Cache object guarantees that no other concurrently running threads can ever interfere with what you're doing. If you need to ensure that multiple operations on the Cache object occur atomically, that's a different story. Consider the following code snippet:

int counter = -1; object o = Cache["Counter"]; if (o == null) {     // Retrieve the last good known value from a database     // or return a default value     counter = RetrieveLastKnownValue(); } else {     counter = (int) Cache["Counter"];     counter ++;     Cache["Counter"] = counter; } 

The Cache object is accessed repeatedly in the context of an atomic operation incrementing a counter. While individual accesses to Cache are thread-safe, there's no guarantee that other threads won't kick in between the various calls. If there's potential contention on the cached value, you should consider using additional locking constructs, such as the C# lock statement (SyncLock in Microsoft Visual Basic .NET).

Important 

Where should you put the lock? If you directly lock the Cache object, you might run into trouble. ASP.NET uses the Cache object extensively, and directly locking the Cache object might have a serious impact on the overall performance of the application. However, most of the time ASP.NET doesn't access the cache via the Cache object, but rather it accesses the direct data container, that is the CacheSingle or CacheMultiple class. In this regard, a lock on the Cache object probably won't affect many ASP.NET components, but it's a risk that personally I wouldn't like to take. By locking the Cache object, you also risk blocking HTTP modules and handlers active in the pipeline, as well as other pages and sessions in the application that need to use cache entries different from the ones you want to serialize access to.

The best way out seems to be using a synchronizer, that is, an intermediate but global object that you lock before entering in a piece of code sensitive to concurrency:

lock(yourSynchronizer) {     // Access the Cache here. This pattern must be replicated for     // each access to the cache that requires serialization. } 

The synchronizer object must be global to the application. For example, it can be a static member defined in the global.asax file.

Per-Request Caching

Although you normally tend to cache only global data and data of general interest, to squeeze out every little bit of performance you can also cache per-request data that is long-lived even though it's used only by a particular page. You place this information in the Cache object.

Another form of per-request caching is possible to improve performance. Working information shared by all controls and components participating in the processing of a request can be stored in a global container for the duration of the request. In this case, though, you might want to use the Items collection on the HttpContext class (discussed in Chapter 12) to park the data because it is automatically freed up at the end of the request and doesn't involve implicit or explicit locking like Cache.

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, in ASP.NET 2.0 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 limitations of CacheDependency in ASP.NET 1.x that have led to removing the sealed attribute on the class, making it fully inheritable in ASP.NET 2.0.

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 ASP.NET 2.0 is that version 2.0 supports 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 14-6.

Table 14-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 Cache-Dependency 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

In ASP.NET 2.0, not only can you create a single dependency on an entry, but 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.

In ASP.NET 2.0, the AggregateCacheDependency class is built as a custom cache-dependency object and inherits the base CacheDependency class.

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 about 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 14-3 illustrates the connections.

image from book
Figure 14-3: Custom dependencies use helper cache entries to invalidate any items under their control.

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 14-4.

image from book
Figure 14-4: The custom dependency object in action in a sample page.

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.

SQL Server Cache Dependency

Many ASP.NET applications query some data out of a database, cache it, and then manage to serve a report to the user. Binding the report to the data in the cache will both reduce the time required to load each report and minimize traffic to and from the database. So what's the problem? With a report built from the cache, if the data displayed is modified in the database, users will get an out-of-date report. If updates occur at a known or predictable rate, you can set an appropriate duration for the cached data so that the report gets automatically refreshed at regular intervals. However, this contrivance just doesn't work if serving live data is critical for the application or if changes occur rarely and, worse yet, randomly. In the latter case, whatever duration you set might hit the application in one way or the other. A too-long duration creates the risk of serving outdated reports to end users which, in some cases, could undermine the business; a too-short duration burdens the application with unnecessary queries.

A database dependency is a special case of custom dependency that consists of the automatic invalidation of some cached data when the contents of the source database table changes. Not directly supported by the framework in ASP.NET 1.x, database dependencies are a native feature in ASP.NET 2.0. In ASP.NET 2.0, you find an ad hoc class SqlCacheDependency that inherits CacheDependency and supports dependencies on SQL Server tables. More precisely, the class is compatible with MSDE, SQL Server 7.0, and subsequent SQL Server versions (including SQL Server 2005).

Under the Hood of Database Dependencies

With the notable exception of SQL Server 2005, no database today offers listening features to detect when relevant changes occur. This means that on SQL Server 7.0, SQL Server 2000, and non-SQL Server databases you must create a database-level infrastructure that makes table changes emerge, allows them to be captured, and notifies the ASP.NET cache of the changes.

In the past several years, a few techniques have been investigated by the ASP.NET team and developers in the community. None of the techniques is perfect, but all are worth a look if you plan to implement database cache invalidation in ASP.NET 1.x applications.

A database cache invalidation system is based on two key elements a mechanism to detect changes on the database, and a mechanism to notify ASP.NET of the changes. To detect changes, triggers are the most commonly used technique. You need to have a trigger active on each table in a database that you want to monitor for changes. The trigger captures insertions, deletions, and updates on a table and does something with them. What exactly it does depends on the second mechanism you implement. Various approaches have been tested over time:

Although some people don't particularly like the use of triggers, as I see things the real issue here is the use of extended stored procedures. They have to be written in C++ and be deployed manually to SQL Server. Furthermore, they require administrative permissions because they run external programs and introduce potentially serious blocking issues. The extended stored procedure can't return until the HTTP call or the file modification is complete. What if the Web server takes a long time to respond? What if the file is locked? In the end, the database will be affected and the flow of information from it or to it might be slowed down. These solutions might work great for small applications with no scalability concerns, but they are not ideal for large, real-world sites.

Tip 

If you don't like triggers, you might want to try T-SQL checksum functions. The following query returns a value that varies when changes are made to a table record:

SELECT CHECKSUM_AGG(BINARY_CHECKSUM(*)) FROM Customers 

Checksum functions are reasonably fast but don't work with reference columns such as text and image. The advantage of checksum functions is that all you need to deploy on the data base is a stored procedure to wrap the query just shown.

Extended stored procedures implement a push model, where the database backend pushes changes to the ASP.NET application. The reverse approach is also possible a pull model based on polling which is the foundation of the ASP.NET 2.0 implementation of database cache invalidation.

The database to be monitored is equipped with triggers and a helper table with one record for each monitored table. Triggers update the helper table whenever the corresponding table is modified. A custom component placed in the ASP.NET cache polls this helper table looking for changes and because it's a very small table, results are likely to be paged in SQL Server's memory for the fastest performance. When the polling component detects a change in the table, it will invalidate the linked cache item with the data used by the application.

Note 

To implement database dependencies in ASP.NET 1.x, you start by creating a custom CacheDependency class along the lines of the CacheDependency class in ASP.NET 2.0. This abstract class will start a timer in the constructor and call an overridable method for example, HasChanged to check for changes. The user-defined DatabaseCacheDependency class inherits from CacheDependency and overrides HasChanged to query against the helper table of change notifications (or checksums). To insert data in the cache bound to this dependency object, you resort to a helper method that extends the Insert method of the native Cache object. Basically, your helper insert method will create a pair of cache entries one for the real data, and one for the dependency object polling for data. The two entries are linked so that changes to the entry with the dependency invalidates the one with real data. Details and sample code are available at http://msdn.microsoft.com/msdnmag/issues/04/07/CuttingEdge.

Enabling Database Dependencies in ASP.NET 2.0

In ASP.NET 2.0, database dependencies are implemented through the SqlCacheDependency class. The class works with SQL Server 7.0, SQL Server 2000, and the newer SQL Server 2005. To make it work with SQL Server 2005, much less setup work is required. Let's tackle SQL Server 7.0 and SQL Server 2000 first.

For the SqlCacheDependency class to work correctly, any tables that you want to monitor for changes must have notifications enabled. Enabling notifications entails administrative changes to the database that must be accomplished before the application is published.

Changes include creating ad hoc triggers and stored procedures to handle any incoming UPDATE, INSERT, or DELETE statements.

You use the command-line tool aspnet_regsql to do any required offline work. You first enable notifications on the database, and next do the same on one or more of the database tables. Run the following command to enable notifications on the Northwind database for the local installation of SQL Server:

aspnet_regsql.exe -S (local) -U YourUserName -P YourPassword                   -d Northwind -ed 

Run the following command to enable notification on the Customers table:

aspnet_regsql.exe -S (local) -U YourUserName -P YourPassword                   -d Northwind -et -t Customers 

The first command adds a new table to the database whose name is AspNet_SqlCacheTables-ForChangeNotification. In addition, a bunch of stored procedures and triggers are added. Note that you need to specify a login with enough permissions to perform all the operations.

The second command adds a trigger to the specified table and a new record to the AspNet_SqlCacheTablesForChangeNotification table that refers to the specified table. Here's the trigger:

CREATE TRIGGER dbo.[Customers_AspNet_SqlCacheNotification_Trigger] ON [Customers] FOR INSERT, UPDATE, DELETE AS BEGIN   SET NOCOUNT ON   EXEC dbo.AspNet_SqlCacheUpdateChangeIdStoredProcedure N'Customers' END 

Figure 14-5 provides a view of the structure of the change notification table. This table contains one record for each monitored table.


Figure 14-5: The structure of the AspNet_SqlCacheTablesForChangeNotification table.

The trigger executes the following stored procedure whenever a change occurs on the monitored table. As a result, the changeId column for the table is modified.

BEGIN   UPDATE dbo.AspNet_SqlCacheTablesForChangeNotification WITH (ROWLOCK)     SET changeId = changeId + 1     WHERE tableName = @tableName END 

To finalize the setup of SqlCacheDependency, you need to add the following script to the application's web.config file:

<system.web>   <caching>     <sqlCacheDependency enabled="true" pollTime="1000" >       <databases>         <add name="Northwind" connectionStringName="LocalNWind" />       </databases>     </sqlCacheDependency>   </caching> </system.web> 

The pollTime attribute indicates (in milliseconds) the interval of the polling. In the preceding sample, any monitored table will be checked every second. Under the <databases> node, you find a reference to monitored databases. The name attribute is used only to name the dependency. The connectionStringName attribute points to an entry in the <connectionStrings> section of the web.config file and denotes the connection string to access the database.

Note 

In addition to using the aspnet_regsql command-line tool, you can use a programming interface to create the run-time environment that allows database cache dependencies for SQL Server 7 and SQL Server 2000. The following code enables the Northwind database for notifications:

SqlCacheDependencyAdmin.EnableNotifications("Northwind"); 

You add a table to the list of monitored tables with the following code:

SqlCacheDependencyAdmin.EnableTableForNotifications(     "Northwind", "Employees"); 

The SqlCacheDependencyAdmin class also counts methods to disable previously enabled dependencies.

Let's see now how to create and use a SqlCacheDependency object.

Taking Advantage of SQL Server Dependencies

The SqlCacheDependency class has two constructors. The first takes a SqlCommand object, and the second accepts two strings the database name and the table name. The constructor that accepts a SqlCommand is intended for use only with SQL Server 2005; the other is designed for dependencies that involve older versions of SQL Server.

The following code creates a SQL Server dependency and binds it to a cache key:

protected void AddToCache(object data) {     string database = "Northwind";     string table = "Customers";     SqlCacheDependency dep = new SqlCacheDependency(database, table);     Cache.Insert("MyData", data, dep); } protected void Page_Load(object sender, EventArgs e) {     if (!IsPostBack)     {         // Get some data to cache         CustomerCollection data = Customers.LoadByCountry("USA");         // Cache with a dependency on Customers         AddToCache(data);     } } 

The data in the cache can be linked to any data-bound control, as follows:

CustomerCollection data = null; object o = Cache["MyData"]; if (o != null)     data = (CustomerCollection)o; else     Trace.Warn("Null data"); CustomerList.DataTextField = "CompanyName"; CustomerList.DataSource = data; CustomerList.DataBind(); 

When the database is updated, the MyData entry is invalidated and, as for the sample implementation provided here, the listbox displays empty.

Important 

You get notification based on changes in the table as a whole. In the preceding code, we're displaying a data set that results from the following:

SELECT * FROM customers WHERE country='USA' 

If, say, a new record is added to the Customers table, you get a notification no matter what the value in the country column is. The same happens if a record is modified or deleted where the country column is not USA.

SQL Server 2005 offers a finer level of control and can notify applications only of changes to the database that modify the output of a specific command.

Once you are set up for table notifications, pages that use a SqlDataSource control can implement a smarter form of caching that monitors the bound table for changes and reloads data in case of changes:

<asp:SqlDataSource  runat="server"      ConnectionString="<%$ ConnectionStrings:LocalNWind %>"      SelectCommand="SELECT * FROM Customers"      EnableCaching="true"      SqlCacheDependency="Northwind:Customers"> </asp:SqlDataSource> 

You set the SqlCacheDependency property to a string of the form Database:Table. The first token is the name of the database dependency as set in the <databases> section. The second token is the name of the table to monitor. You can also define multiple dependencies by separating each pair with a semicolon:

<asp:SqlDataSource  runat="server"      EnableCaching="true"      SqlCacheDependency="Northwind:Customers;Pubs:Authors"      ... /> 

Note that caching must be enabled for the feature to work.

Note 

Although I've mentioned only SqlDataSource, the SqlCacheDependency property also works with ObjectDataSource as long as ObjectDataSource returns data through ADO.NET objects.

Cache Dependencies in SQL Server 2005

As mentioned, the SqlCacheDependency class has two constructors, one of which takes a Sql-Command object as its sole argument. This constructor is used to create SqlCacheDependency objects for SQL Server 2005 databases. Here's how to use it:

protected void AddToCache() {    SqlConnection conn = new SqlConnection(       ConfigurationManager.ConnectionStrings["NWind05"].ConnectionString);    SqlCommand cmd = new SqlCommand(        "SELECT * FROM Customers WHERE country='USA'",        conn);    SqlDataAdapter adapter = new SqlDataAdapter(cmd);    DataTable data = new DataTable();    adapter.Fill(data);    SqlCacheDependency dep = new SqlCacheDependency(cmd);    Cache.Insert("MyData", data, dep); } 

Note that with SQL Server 2005 no setup work is needed and no external objects must be added to the database. No triggers, stored procedures, and notification tables are needed.

SQL Server 2005 incorporates a made-to-measure component that monitors changes at a finer level than was possible in earlier versions. This component takes a command object and tracks all the ongoing changes to detect whether something happened to modify the result set returned by the command. When this happens, the component pushes new information to the listening object. This mechanism relies on the ADO.NET SqlDependency class that we discussed back in Chapter 7.

So when you instantiate a SqlCacheDependency object and have it guard a given command, a SqlDependency object wraps the command and fires an event when a change is detected. In turn, the SqlCacheDependency catches the event and invalidates the cached item. Figure 14-6 illustrates the data flow.

image from book
Figure 14-6: The internal implementation of SqlCacheDependency when used with SQL Server 2005.

Note 

The SQL Server 2005 implementation of database cache invalidation is clearly the best possible because it leverages a new infrastructure built in the database. ASP.NET applications receive an asynchronous notification of changes, which is good for performance and poses no blocking issues. Also, no setup is necessary for the feature to work. The SQL Server 7.0 and SQL Server 2000 implementation of the same feature relies on polling and requires some setup work. The ASP.NET team made the SQL dependency setup as smooth as possible, but it still requires an administrative login to get into the database to create triggers, tables, and stored procedures. This might be a problem if you're not allowed full access to the database or if you're working in an ISP scenario. Is polling the best possible option for detecting changes? All things considered, polling is a necessity when there's no built-in notification mechanism in the database. And polling a small table is more efficient than repeatedly running a query. Finally, polling doesn't suffer from blocking issues as do approaches built on extended stored procedures, and it also works great in Web farms and Web gardens. In the end, the ASP.NET team determined that polling was the best option for the broadest number of ASP.NET applications willing to support database cache invalidation.

 


Programming Microsoft ASP. Net 2.0 Core Reference
Programming Microsoft ASP.NET 2.0 Core Reference
ISBN: 0735621764
EAN: 2147483647
Year: 2004
Pages: 112
Authors: Dino Esposito
BUY ON AMAZON

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net