
Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]
.pdf
408 C H A P T E R 1 1 ■ C A C H I N G
In this example, each time you select a city, a separate query is performed to get just the matching employees in that city. The query is used to fill a DataSet, which is then cached. If you select a different city, the process repeats, and the new DataSet is cached separately. However, if you pick a city that you or another user has already requested, the appropriate DataSet is fetched from the cache (provided it hasn’t yet expired).
■Note SqlDataSource caching works only when the DataSourceMode property is set to DataSet (the default). That’s because the DataReader object can’t be efficiently cached, because it represents a live connection to the database.
Caching separate results for different parameter values works well if some parameter values are used much more frequently than others. For example, if the results for London are requested much more often than the results for Redmond, this ensures that the London results stick around in the cache even when the Redmond DataSet has been released. Assuming the full set of results is extremely large, this may be the most efficient approach.
On the other hand, if the parameter values are all used with similar frequency, this approach isn’t as suitable. One of the problems it imposes is that when the items in the cache expire, you’ll need multiple database queries to repopulate the cache (one for each parameter value), which isn’t as efficient as getting the combined results with a single query.
If you fall into the second situation, you can change the SqlDataSource so that it retrieves a DataSet with all the employee records and caches that. The SqlDataSource can then extract just the records it needs to satisfy each request from the DataSet. This way, a single DataSet with all the records is cached, which can satisfy any parameter value.
To use this technique, you need to rewrite your SqlDataSource to use filtering. First, the select query should return all the rows and not use any SELECT parameters:
<asp:SqlDataSource ID="sourceEmployees" runat="server"
SelectCommand="SELECT EmployeeID, FirstName, LastName, Title, City FROM Employees"
...> </asp:SqlDataSource>
Second, you need to define the filter expression. This is the portion that goes in the WHERE clause of a typical SQL query, and you write it in the same way as you used the DataView.RowFilter property in Chapter 9. (In fact, the SqlDataSource uses the DataView’s row filtering abilities behind the scenes.) However, this has a catch—if you’re supplying the filter value from another source (such as a control), you need to define one or more placeholders, using the syntax {0} for the first placeholder, {1} for the second, and so on. You then supply the filter values using the <FilterParameters> section, in much the same way you supplied the select parameters in the first version.
Here’s the completed SqlDataSource tag:
<asp:SqlDataSource ID="sourceEmployees" runat="server" ProviderName="System.Data.SqlClient" ConnectionString="<%$ ConnectionStrings:Northwind %>"
SelectCommand="SELECT EmployeeID, FirstName, LastName, Title, City FROM Employees" FilterExpression="City='{0}'" EnableCaching="True">
<FilterParameters>
<asp:ControlParameter ControlID="lstCities" Name="City" PropertyName="SelectedValue" />
</FilterParameters>
</asp:SqlDataSource>

C H A P T E R 1 1 ■ C A C H I N G |
409 |
■Tip Don’t use filtering unless you are using caching. If you use filtering without caching, you are essentially retrieving the full result set each time and then extracting a portion of its records. This combines the worst of both worlds—you have to repeat the query with each postback, and you fetch far more data than you need each time.
Caching with ObjectDataSource
The ObjectDataSource caching works on the data object returned from the SelectMethod. If you are using a parameterized query, the ObjectDataSource distinguishes between requests with different parameter values and caches them separately. Unfortunately, the ObjectDataSource caching has a significant limitation—it works only when the select method returns a DataSet or DataTable. If you return any other type of object, you’ll receive a NotSupportedException.
This limitation is unfortunate, because there’s no technical reason you can’t cache custom objects in the data cache. If you want this feature, you’ll need to implement data caching inside your method, by manually insert your objects into the data cache and retrieving them later. In fact, caching inside your method can be more effective, because you have the ability to share the same cached object in multiple methods. For example, you could cache a DataTable with a list of product categories and use that cached item in both the GetProductCategories() and GetProductsByCategory() methods.
■Tip The only consideration you should keep in mind is to make sure you use unique cache key names that aren’t likely to collide with the names of cached items that the page might use. This isn’t a problem when using the built-in data source caching, because it always stores its information in a hidden slot in the cache.
If your custom class returns a DataSet or DataTable, and you do decide to use the built-in ObjectDataSource caching, you can also use filtering as discussed with the SqlDataSource control. Just instruct your ObjectDataSource to call a method that gets the full set of data, and set the FilterExpression to retrieve just those items that match the current view.
Cache Dependencies
As time passes, the data source may change in response to other actions. However, if your code uses caching, you may remain unaware of the changes and continue using out-of-date information from the cache. To help mitigate this problem, ASP.NET supports cache dependencies. Cache dependencies allow you to make a cached item dependent on another resource so that when that resource changes the cached item is removed automatically.
ASP.NET includes three types of dependencies:
•Dependencies on other cache items
•Dependencies on files or folders
•Dependencies on a database query
In the following section, you’ll consider the first two options. Toward the end of this chapter, you’ll learn about SQL dependencies, and you’ll learn how to create your own custom dependen- cies—two tasks that are new in ASP.NET 2.0.

410 C H A P T E R 1 1 ■ C A C H I N G
File and Cache Item Dependencies
To create a cache dependency, you need to create a CacheDependency object and then use it when adding the dependent cached item. For example, the following code creates a cached item that will automatically be evicted from the cache when an XML file is changed, deleted, or overwritten.
//Create a dependency for the ProductList.xml file. CacheDependency prodDependency = new CacheDependency(
Server.MapPath("ProductList.xml"));
//Add a cache item that will be dependent on this file. Cache.Insert("ProductInfo", prodInfo, prodDependency);
If you point the CacheDependency to a folder, it watches for the addition, removal, or modification of any files in that folder. Modifying a subfolder (for example, renaming, creating, or removing a subfolder) also violates the cache dependency. However, changes further down the directory tree (such as adding a file into a subfolder or creating a subfolder in a subfolder) don’t have any effect.
The CacheDependency provides several constructors. You’ve already seen how it can make a dependency based on a file by using the filename constructor. You can also specify a directory that needs to be monitored for changes, or you can use a constructor that accepts an array of strings that represent multiple files or directories.
Yet another constructor accepts an array of filenames and an array of cache keys. The following example uses this constructor to create an item that is dependent on another item in the cache:
Cache["Key1"] = "Cache Item 1";
// Make Cache["Key2"] dependent on Cache["Key1"]. string[] dependencyKey = new string[1]; dependencyKey[0] = "Key1";
CacheDependency dependency = new CacheDependency(null, dependencyKey);
Cache.Insert("Key2", "Cache Item 2", dependency);
Next, when Cache["Key 1"] changes or is removed from the cache, Cache["Key 2"] will automatically be dropped.
■Tip CacheDependency monitoring begins as soon as the object is created. If the XML file changes before you have added the dependent item to the cache, the item will expire immediately once it’s added. If that’s not the behavior you want, use the overloaded constructor that accepts a DateTime object. This DateTime indicates when the dependency monitoring will begin.
Figure 11-5 shows a simple test page that is included with the online samples for this chapter. It sets up a dependency, modifies the file, and allows you to verify that the cache item has been dropped from the cache.
Aggregate Dependencies
Sometimes, you might want to combine dependencies to create an item that’s dependent on more than one other resource. For example, you might want to create an item that’s invalidated if any one of three files changes. Or, you might want to create an item that’s invalidated if a file changes or another cached item is removed. Creating these rules is easy with the new AggregateCacheDependency class introduced in ASP.NET 2.0.


412C H A P T E R 1 1 ■ C A C H I N G
reinsert the removed item into the cache. Not only will this waste time generating data that might not be immediately required, but it will also thwart ASP.NET’s attempt to reduce memory usage when server resources are scarce.
You can place the method that handles the callback in your web-page class, or you can use a static method in another accessible class. However, you should keep in mind that this code won’t be executed as part of a web request. That means you can’t interact with web-page objects or notify the user.
The following example uses a cache callback to make two interdependent items—a feat that wouldn’t be possible with dependencies alone. Two items are inserted in the cache, and when either one of those items is removed, the item removed callback removes the other.
public partial class ItemRemovedCallbackTest : System.Web.UI.Page
{
protected void Page_Load(object sender, System.EventArgs e)
{
if (!this.IsPostBack)
{
lblInfo.Text += "Creating items...<br />"; string itemA = "item A";
string itemB = "item B";
Cache.Insert("itemA", itemA, null, DateTime.Now.AddMinutes(60), TimeSpan.Zero, CacheItemPriority.Default,
new CacheItemRemovedCallback(ItemRemovedCallback)); Cache.Insert("itemB", itemB, null, DateTime.Now.AddMinutes(60),
TimeSpan.Zero, CacheItemPriority.Default,
new CacheItemRemovedCallback(ItemRemovedCallback));
}
}
protected void cmdCheck_Click(object sender, System.EventArgs e)
{
string itemList = ""; foreach(DictionaryEntry item in Cache)
{
itemList += item.Key.ToString() + " ";
}
lblInfo.Text += "<br />Found: " + itemList + "<br />";
}
protected void cmdRemove_Click(object sender, System.EventArgs e)
{
lblInfo.Text += "<br />Removing itemA.<br />"; Cache.Remove("itemA");
}
private void ItemRemovedCallback(string key, object value, CacheItemRemovedReason reason)
{
//This fires after the request has ended, when the
//item is removed.
//If either item has been removed, make sure
//the other item is also removed.
if (key == "itemA" || key == "itemB")
{
Cache.Remove("itemA");

C H A P T E R 1 1 ■ C A C H I N G |
413 |
Cache.Remove("itemB");
}
}
}
Figure 11-6 shows a test of this page.
Figure 11-6. Testing a cache callback
The callback also provides your code with additional information, including the removed item and the reason it was removed. Table 11-4 shows possible reasons.
Table 11-4. Values for the CacheItemRemovedReason Enumeration
Value |
Description |
DependencyChanged |
Removed because a file or key dependency changed |
Expired |
Removed because it expired (according to its sliding or absolute |
|
expiration policy) |
Removed |
Removed programmatically by a Remove method call or by an Insert |
|
method call that specified the same key |
Underused |
Removed because ASP.NET decided it wasn’t important enough and |
|
wanted to free memory |
|
|
Understanding SQL Cache Notifications
SQL cache dependencies are one of the most widely touted new ASP.NET 2.0 features—the ability to automatically invalidate a cached data object (such as a DataSet) when the related data is modified in the database. This feature is supported in both SQL Server 2005 and in SQL Server 2000, although the underlying plumbing is quite a bit different.
To understand how SQL cache dependencies work, it’s important to understand a few flawed solutions that developers have been forced to resort to in the past.

414 C H A P T E R 1 1 ■ C A C H I N G
One common technique is to use a marker file. With this technique, you add the data object to the cache and set up a file dependency. However, the file you use is empty—it’s just a marker file that’s intended to indicate when the database state changes.
Here’s how it works. When the user calls a stored procedure that modifies the table you’re interested in, your stored procedure removes or modifies the marker file. ASP.NET immediately detects the file change and removes the corresponding data object. This ugly workaround isn’t terribly scalable and can introduce concurrency problems if more than one user calls the stored procedure and tries to remove the file at once. It also forces you to clutter your stored procedure code, because every stored procedure that modifies the database needs similar file modification logic. Having a database interact with the file system is a bad idea from the start, because it adds to the complexity and reduces the security of your overall system.
Another common approach is to use a custom HTTP handler that removes cached items at your request. Once again, this only works if you build the appropriate level of support into the stored procedures that modify the corresponding tables. In this case, instead of interacting with a file, these stored procedures call the custom HTTP handler and pass a query string that indicates what change has taken place or what cache key has been affected. The HTTP handler can then use the Cache.Remove() method to get rid of the data.
The problem with this approach is that it requires the considerable complexity of an extended stored procedure. Also, the request to the HTTP handler must be synchronous, which causes a significant delay. Even worse, this delay happens every time the stored procedure executes, because the stored procedure has no way of determining if the call is necessary or if the cached item has already been removed. As a result, the overall time taken to execute the stored procedure increases significantly, and the overall scalability of the database suffers. Like the marker file approach, it works well in small scenarios but can’t handle large-scale, complex applications. Both of these approaches introduce a whole other set of complications in web farm scenarios with multiple servers.
What’s needed is an approach that can deliver notifications asynchronously, and in a scalable and reliable fashion. In other words, the database server should notify ASP.NET without stalling the current connection. Just as importantly, it should be possible to set up the cache dependency in a loosely coupled way so that stored procedures don’t need to be aware of the caching that’s in place. The database server should watch for changes that are committed by any means, including from a script, an inline SQL command, or a batch process. Even if the change doesn’t go through the expected stored procedures, the change should still be noticed, and the notification should still be delivered to ASP.NET. Finally, the notification method needs to support web farms.
Microsoft put together a team of architects from the ASP.NET, SQL Server, ADO.NET, and IIS groups to concoct a solution. They came up with two different architectures, depending on the database server you’re using. Both of them use the same SqlCacheDependency class, which derives from the CacheDependency class you saw earlier.
■Tip Using SQL cache dependencies still entails more complexity than just using a time-based expiration policy. If it’s acceptable for certain information to be used without reflecting all the most recent changes (and developers often overestimate the importance of up-to-the-millisecond live information), you may not need it at all.

C H A P T E R 1 1 ■ C A C H I N G |
415 |
Cache Notifications in SQL Server 2000 or SQL Server 7
ASP.NET uses a polling model for SQL Server 2000 and SQL Server 7. Older versions of SQL Server and other databases aren’t supported (although third parties can implement their own solutions by creating a custom dependency class).
With the polling model, ASP.NET keeps a connection open to the database and uses a dedicated thread to check periodically if a table has been updated. The effect of tying up one connection in this way isn’t terribly significant, but the extra database work involved with polling does add some database overhead. For the polling model to be effective, the polling process needs to be quicker and lighter than the original query that extracts the data.
Enabling Notifications
Before you can use SQL Server cache invalidation, you need to enable notifications for the database. This task is performed with the aspnet_regsql.exe command-line utility, which is located in the c:\[WinDir]\Microsoft.NET\Framework\[Version] directory. To enable notifications, you need to use the -ed command-line switch. You also need to identify the server (use -E for a trusted connection and -S to choose a server other than the current computer) and the database (use -d). Here’s an example that enables notifications for the Northwind database on the current server:
aspnet_regsql -ed -E -d AspNet
■Tip You’ll see aspnet_regsql used throughout this book. It’s required to create the tables for other new ASP.NET 2.0 features such as membership, profiles, and role management.
When you take this step, a new table named SqlCacheTablesForChangeNotification is added to the database named AspNet (which must already exist). The SqlCacheTablesForChangeNotification table has three columns: tableName, notificationCreated, and changeId. This table is used to track changes. Essentially, when a change takes place, a record is written into this table. The SQL Server polling queries this table.
This design achieves a number of benefits:
•Because the change notification table is much smaller than the table with the cached data, it’s much faster to query.
•Because the change notification table isn’t used for other tasks, reading these records won’t risk locking and concurrency issues.
•Because multiple tables in the same database will use the same notification table, you can monitor several tables at once without materially increasing the polling overhead.
Figure 11-7 shows an overview of how SQL Server 2000 cache invalidation works.

416 C H A P T E R 1 1 ■ C A C H I N G
Trigger creates a change record
Poll for changes
Figure 11-7. Monitoring a database for changes in SQL Server 2000
The aspnet_regsql utility adds several stored procedures to the database, as listed in Table 11-5.
Table 11-5. Stored procedures for managing notifications
Name |
Description |
AspNet_SqlCachePollingStoredProcedure |
Gets the list of changes from the |
|
AspNet_SqlCacheTablesForChange- |
|
Notification table. Used to perform |
|
the polling. |
AspNet_SqlCacheQueryRegisteredTablesStoredProcedure |
Extracts just the table names from |
|
the AspNet_SqlCacheTablesFor- |
|
ChangeNotification table. Used to |
|
get a quick look at all the registered |
|
tables. |
AspNet_SqlCacheRegisterTableStoredProcedure |
Sets a table up to support |
|
notifications. This process works by |
|
adding a notification trigger to the |
|
table, which will fire when any row is |
|
inserted, deleted, or updated. |
AspNet_SqlCacheUnRegisterTableStoredProcedure |
Takes a registered table and removes |
|
the notification trigger so that |
|
notifications won’t be generated. |
AspNet_SqlCacheUpdateChangeIdStoredProcedure |
The notification trigger calls this |
|
stored procedure to update the |
|
AspNet_SqlCacheTablesForChange- |
|
Notification, thereby indicating that |
|
the table has changed. |
|
|

C H A P T E R 1 1 ■ C A C H I N G |
417 |
Even once you’ve created the SqlCacheTablesForChangeNotification table, you still need to enable notification support for each individual table. You can do this manually using the SqlCacheRegisterTableStoredProcedure, or you can rely on aspnet_regsql, using the -et parameter to turn on the notifications and the -t parameter to name the table. Here’s an example that enables notifications for the Employees table:
aspnet_regsql -et -E -d Northwind -t Employees
This step generates the notification trigger for the Employees table.
How Notifications Work
Now you have all the ingredients in place to use the notification system. For example, imagine you cache the results of a query like this:
SELECT * FROM Employees
This query retrieves records from the Employees table. To check for changes that might invalidate your cached object, you need to know if any record in the Employees table is inserted, deleted, or updated. You can watch for these operations using triggers. For example, here’s the trigger on the Employees table that aspnet_regsql creates:
CREATE TRIGGER dbo.[Employees_AspNet_SqlCacheNotification_Trigger]
ON [Employees]
FOR INSERT, UPDATE, DELETE AS BEGIN
SET NOCOUNT ON
EXEC dbo.AspNet_SqlCacheUpdateChangeIdStoredProcedure N'Employees'
END
The AspNet_SqlCacheUpdateChangeIdStoredProcedure stored procedure simply increments the changeId for the table:
CREATE PROCEDURE dbo.AspNet_SqlCacheUpdateChangeIdStoredProcedure @tableName NVARCHAR(450)
AS
BEGIN
UPDATE dbo.AspNet_SqlCacheTablesForChangeNotification WITH (ROWLOCK)
SET changeId = changeId + 1
WHERE tableName = @tableName
END
GO
The AspNet_SqlCacheTablesForChangeNotification contains a single record for every table you’re monitoring. As you can see, when you make a change in the table (such as inserting a record), the changeId column is incremented by 1. ASP.NET queries this table repeatedly and keeps track of the most recent changeId values for every table. When this value changes in a subsequent read, ASP.NET knows that the table has changed.
This hints at one of the major limitations of cache invalidation as implemented in SQL Server 2000 and SQL Server 7. Any change to the table is deemed to invalidate any query for that table. In other words, if you use this query:
SELECT * FROM Employees WHERE City='London'
the caching still works in the same way. That means if any employee record is touched, even if the employee resides in another city (and therefore isn’t one of the cached records), the notification is