
Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]
.pdf
398 C H A P T E R 1 1 ■ C A C H I N G
■Tip Make sure you use the Response.Cache property of the page, not the Cache property. The Cache property isn’t used for output caching—instead, it gives you access to the data cache (discussed in the “Data Caching” section).
Post-Cache Substitution and Fragment Caching
In some cases, you may find that you can’t cache an entire page, but you would still like to cache a portion that is expensive to create and doesn’t vary. You have two ways to handle this challenge:
•Fragment caching: In this case, you identify just the content you want to cache, wrap that in a dedicated user control, and cache just the output from that control.
•Post-cache substitution: In this case, you identify just the dynamic content you don’t want to cache. You then replace this content with something else using the Substitution control. Post-cache substitution is new in ASP.NET 2.0.
Out of the two, fragment caching is the easiest to implement. However, the decision of which you want to use will usually be based on the amount of content you want to cache. If you have a small, distinct portion of content to cache, fragment caching makes the most sense. Conversely, if you have only a small bit of dynamic content, post-cache substitution may be the more straightforward approach. Both approaches offer similar performance.
■Tip The most flexible way to implement a partial caching scenario is to step away from output caching altogether and use data caching to handle the process programmatically in your code. You’ll see this technique in the “Data Caching” section.
Fragment Caching
To implement fragment caching, you need to create a user control for the portion of the page you want to cache. You can then add the OutputCache directive to the user control. The result is that the page will not be cached, but the user control will. Chapter 14 discusses user controls.
Fragment caching is conceptually the same as page caching. There is only one catch—if your page retrieves a cached version of a user control, it cannot interact with it in code. For example, if your user control provides properties, your web-page code cannot modify or access these properties. When the cached version of the user control is used, a block of HTML is simply inserted into the page. The corresponding user control object is not available.
Post-Cache Substitution
The post-cache substitution feature (which is new in ASP.NET 2.0) revolves around a single method that has been added to the HttpResponse class. The method is WriteSubstitution(), and it accepts a single parameter—a delegate that points to a callback method that you implement in your page class. This callback method returns the content for that portion of the page.
Here’s the trick: when the ASP.NET page framework retrieves the cached page, it automatically triggers your callback method to get the dynamic content. It then inserts your content into the cached HTML of the page. The nice thing is that even if your page hasn’t been cached yet (for example, it’s being rendered for the first time), ASP.NET still calls your callback in the same way to get the dynamic content. In essence, the whole idea is that you create a method that generates some

C H A P T E R 1 1 ■ C A C H I N G |
399 |
dynamic content, and by doing so you guarantee that your method is always called, and its content is never cached.
The method that generates the dynamic content needs to be static. That’s because ASP.NET needs to be able to call this method even when there isn’t an instance of your page class available. (Obviously, when your page is served from the cache, the page object isn’t created.) The signature for the method is fairly straightforward—it accepts an HttpContext object that represents the current request, and it returns a string with the new HTML. Here’s an example that returns a date with bold formatting:
private static string GetDate(HttpContext context)
{
return "<b>" + DateTime.Now.ToString() + "</b>";
}
To get this in the page, you need to use the Response.WriteSubstitution() method at some point:
protected void Page_Load(object sender, EventArgs e)
{
Response.Write("This date is cached with the page: "); Response.Write(DateTime.Now.ToString() + "<br />"); Response.Write("This date is not: ");
Response.WriteSubstitution(new HttpResponseSubstitutionCallback(GetDate));
}
Now, even if you apply caching to this page with the OutputCache directive, the date will still be updated for each request. That’s because the callback bypasses the caching process. Figure 11-2 shows the result of running the page and refreshing it several times.
Figure 11-2. Injecting dynamic content into a cached page
The problem with this technique is that post-cache substitution works at a lower level than the rest of your user interface. Usually, when you design an ASP.NET page, you don’t use the Response object at all—instead, you use web controls, and those web controls use the Response object to generate their content. One problem is that if you use the Response object as shown in the previous example, you’ll lose the ability to position your content with respect to the rest of the page. The only realistic solution is to wrap your dynamic content in some sort of control. That way, the control can use Response.WriteSubstitution() when it renders itself. You’ll learn more about control rendering in Chapter 27.

400 C H A P T E R 1 1 ■ C A C H I N G
However, if you don’t want to go to the work of developing a custom control just to get the postcache substitution feature, ASP.NET has one shortcut—a generic Substitution control that uses this technique to make all its content dynamic. You bind the Substitution control to a static method that returns your dynamic content, exactly as in the previous example. However, you can place the Substitution control alongside other ASP.NET controls, allowing you to control exactly where the dynamic content appears.
Here’s an example that duplicates the earlier example using markup in the .aspx portion of the page:
This date is cached with the page:
<asp:Label ID="lblDate" runat="server" /><br /> This date is not:
<asp:Substitution ID="Substitution1" runat="server" MethodName="GetDate" />
Unfortunately, at design time you won’t see the content for the Substitution control. Remember, post-cache substitution allows you to execute only a static method. ASP.NET still
skips the page life cycle, which means it won’t create any control objects or raise any control events. If your dynamic content depends on the values of other controls, you’ll need to use a different technique (such as data caching), because these control objects won’t be available to your callback.
■Note Custom controls are free to use Response.WriteSubstitution() to set their caching behavior. For example, the AdRotator uses this feature to ensure that the advertisement on a page is always rotated, even when the rest of the page is served from the output cache.
Cache Profiles
One problem with output caching is that you need to embed the instruction into the page—either in the .aspx markup portion or in the code of the class. Although the first option (using the OutputCache) is relatively clean, it still produces management problems if you create dozens of cached pages. If you want to change the caching for all these pages (for example, moving the caching duration from 30 to 60 seconds), you need to modify every page. ASP.NET also needs to recompile these pages.
ASP.NET 2.0 introduces a new option that’s suitable if you need to apply the same caching settings to a group of pages. This feature, called cache profiles, allows you to define the caching settings in a web.config file, associate a name with these settings, and then apply these settings to multiple pages using the name. That way, you have the freedom to modify all the linked pages at once simply by changing the caching profile in the web.config file.
To define a cache profile, you use the <add> tag in the <outputCacheProfiles> section, as follows. You assign a name and a duration.
<configuration>
<system.web>
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="ProductItemCacheProfile" duration="60" /> </outputCacheProfiles>
</outputCacheSettings>
</caching>
...
</system.web>
</configuration>


402 C H A P T E R 1 1 ■ C A C H I N G
the Application object. It’s globally available to all requests from all clients in the application. However, a few key differences exist:
The Cache object is thread-safe: This means you don’t need to explicitly lock or unlock the Cache collection before adding or removing an item. However, the objects in the Cache collection will still need to be thread-safe themselves. For example, if you create a custom business object, more than one client could try to use that object at once, which could lead to invalid data. You can code around this limitation in various ways. One easy approach that you’ll see in this chapter is just to make a duplicate copy of the object if you need to work with it in a web page.
Items in the cache are removed automatically: ASP.NET will remove an item if it expires, if one of the objects or files it depends on is changed, or if the server becomes low on memory. This means you can freely use the cache without worrying about wasting valuable server memory, because ASP.NET will remove items as needed. But because items in the cache can be removed, you always need to check if a cached object exists before you attempt to use it. Otherwise, you’ll run into a NullReferenceException.
Items in the cache support dependencies: You can link a cached object to a file, a database table, or another type of resource. If this resource changes, your cached object is automatically deemed invalid and released.
As with application state, the cached object is stored in process, which means it doesn’t persist if the application domain is restarted and it can’t be shared between computers in a web farm. This behavior is by design, because the cost of allowing multiple computers to communicate with an out-of-process cache mitigates some of its performance benefit. It makes more sense for each web server to have its own cache.
Adding Items to the Cache
As with the Application and Session collections, you can add an item to the Cache collection just by assigning to a new key name:
Cache["key"] = item;
However, this approach is generally discouraged because it does not allow you to have any control over the amount of time the object will be retained in the cache. A better approach is to use the Insert() method. Table 11-1 lists the four versions of the Insert() method.
The most important choice you make when inserting an item into the cache is the expiration policy. ASP.NET allows you to set a sliding expiration or an absolute expiration policy, but you cannot use both at the same time. If you want to use an absolute expiration, set the slidingExpiration parameter to TimeSpan.Zero. To set a sliding expiration policy, set the absoluteExpiration parameter to DateTime.Max.
With sliding expiration, ASP.NET waits for a set period of inactivity to dispose of a neglected cache item. For example, if you use a sliding expiration period of ten minutes, the item will be removed only if it is not used within a ten-minute period. Sliding expiration works well when you have information that is always valid but may not be in high demand, such as historical data or a product catalog. This information doesn’t expire because it’s no longer valid but shouldn’t be kept in the cache if it isn’t doing any good.
Here’s an example that stores an item with a sliding expiration policy of ten minutes, with no dependencies:
Cache.Insert("MyItem", obj, null,
DateTime.MaxValue, TimeSpan.FromMinutes(10));

C H A P T E R 1 1 ■ C A C H I N G |
403 |
Table 11-1. The Insert() Method Overloads
Overload |
Description |
Cache.Insert(key, value); |
Inserts an item into the cache under the specified |
|
key name, using the default priority and expiration. |
|
This is the same as using the indexer-based |
|
collection syntax and assigning to a new key name. |
Cache.Insert(key, value, dependencies); |
Inserts an item into the cache under the specified |
|
key name, using the default priority and expiration. |
|
The last parameter contains a CacheDependency |
|
object that links to other files or cached items and |
|
allows the cached item to be invalidated when |
|
these change. |
Cache.Insert(key, value, dependencies, |
Inserts an item into the cache under the specified |
absoluteExpiration, slidingExpiration); |
key name, using the default priority and the |
|
indicated sliding or absolute expiration policy (you |
|
cannot set both at once). This is the most |
|
commonly used version of the Insert() method. |
Cache.Insert(key, value, dependencies, |
Allows you to configure every aspect of the cache |
absoluteExpiration, slidingExpiration, |
policy for the item, including expiration, |
priority, onRemoveCallback); |
dependencies, and priority. In addition, you can |
|
submit a delegate that points to a method you |
|
want invoked when the item is removed. |
|
|
■Note The similarity between caching with absolute expiration and session state is no coincidence. When you use the in-process state server for session state, it actually uses the cache behind the scenes! The session state information is stored in a private slot and given an expiration policy to match the timeout value. The session state item is not accessible through the Cache object.
Absolute expirations are best when you know the information in a given item can be considered valid only for a specific amount of time, such as a stock chart or weather report. With absolute expiration, you set a specific date and time when the cached item will be removed.
Here’s an example that stores an item for exactly 60 minutes:
Cache.Insert("MyItem", obj, null,
DateTime.Now.AddMinutes(60), TimeSpan.Zero);
When you retrieve an item from the cache, you must always check for a null reference. That’s because ASP.NET can remove your cached items at any time. One way to handle this is to add special methods that re-create the items as needed. Here’s an example:
private DataSet GetCustomerData()
{
if (Cache["CustomerData"] != null)
{
// Return the object from the cache. return (DataSet)Cache["CustomerData"];
}
else
{
// Re-create the item and insert it into the cache. DataSet customers = QueryCustomerDataFromDatabase(); Cache.Insert("CustomerData", customers);

404 C H A P T E R 1 1 ■ C A C H I N G
return customers;
}
}
private DataSet QueryCustomerDataFromDatabase()
{
// (Code to query the database goes here.)
}
Now you can retrieve the DataSet elsewhere in your code using the following syntax, without worrying about the caching details:
GridView1.DataSource = GetCustomerData();
For an even better design, move the QueryDataFromDatabase() method to a separate data component.
There’s no method for clearing the entire data cache, but you can enumerate through the collection using the DictionaryEntry class. This gives you a chance to retrieve the key for each item and allows you to empty the class using code like this:
foreach (DictionaryEntry item in Cache)
{
Cache.Remove(item.Key.ToString());
}
Or you can retrieve a list of cached items, as follows:
string itemList = "";
foreach (DictionaryEntry item in Cache)
{
itemList += item.Key.ToString() + " ";
}
This code is rarely used in a deployed application but is extremely useful while testing your caching strategies.
A Simple Cache Test
The following example presents a simple caching test. An item is cached for 30 seconds and reused for requests in that time. The page code always runs (because the page itself isn’t cached), checks the cache, and retrieves or constructs the item as needed. It also reports whether the item was found in the cache.
All the caching logic takes place when the Page.Load event fires.
protected void Page_Load(Object sender, EventArgs e)
{
if (this.IsPostBack)
{
lblInfo.Text += "Page posted back.<br />";
}
else
{
lblInfo.Text += "Page created.<br />";
}
if (Cache["TestItem"] == null)
{

C H A P T E R 1 1 ■ C A C H I N G |
405 |
lblInfo.Text += "Creating TestItem...<br />"; DateTime testItem = DateTime.Now;
lblInfo.Text += "Storing TestItem in cache "; lblInfo.Text += "for 30 seconds.<br />"; Cache.Insert("TestItem", testItem, null,
DateTime.Now.AddSeconds(30), TimeSpan.Zero);
}
else
{
lblInfo.Text += "Retrieving TestItem...<br />"; DateTime testItem = (DateTime)Cache["TestItem"]; lblInfo.Text += "TestItem is '" + testItem.ToString(); lblInfo.Text += "'<br />";
}
lblInfo.Text += "<br />";
}
Figure 11-3 shows the result after the page has been loaded and posted back several times in the 30-second period.
Figure 11-3. Retrieving data from the cache
Cache Priorities
You can also set a priority when you add an item to the cache. The priority only has an effect if ASP.NET needs to perform cache scavenging, which is the process of removing cached items early because memory is becoming scarce. In this situation, ASP.NET will look for underused items that haven’t yet expired. If it finds more than one similarly underused item, it will compare the priorities to determine which one to remove first. Generally, you would set a higher cache priority for items that take more time to reconstruct in order to indicate its heightened importance.

406 C H A P T E R 1 1 ■ C A C H I N G
To assign a cache priority, you choose a value from the CachePriority enumeration. Table 11-2 lists all the values.
Table 11-2. Values of the CachePriority Enumeration
Value |
Description |
High |
These items are the least likely to be deleted from the cache as the server |
|
frees system memory. |
AboveNormal |
These items are less likely to be deleted than Normal priority items. |
Normal |
These items have the default priority level. They are deleted only after Low |
|
or BelowNormal priority items have been removed. |
BelowNormal |
These items are more likely to be deleted than Normal priority items. |
Low |
These items are the most likely to be deleted from the cache as the server |
|
frees system memory. |
NotRemovable |
These items will ordinarily not be deleted from the cache as the server frees |
|
system memory. |
|
|
Caching with the Data Source Controls
In Chapter 9, you spent considerable time working with the data source controls. The SqlDataSource, ObjectDataSource, and XmlDataSource all support built-in data caching. Using caching with these controls is highly recommended, because unlike your own custom data code, the data source controls always requery the data source in every postback. They also query the data source once for every bound control, so if you have three controls bound to the same data source, three separate queries are executed against the database just before the page is rendered. Even a little caching can reduce this overhead dramatically.
■Note Although many data source controls support caching, it’s not a required data source control feature, and you’ll run into data source controls that don’t support it or for which it may not make sense (such as the SiteMapDataSource).
To support caching, the data source controls all use the same properties, which are listed in Table 11-3.
Caching with SqlDataSource
When you enable caching for the SqlDataSource control, you cache the results of the SelectQuery. However, if you create a select query that takes parameters, the SqlDataSource will cache a separate result for every set of parameter values.
For example, imagine you create a page that allows you to view employees by city. The user selects the desired city from a list box, and you use a SqlDataSource control to fill in the matching employee records in a grid (see Figure 11-4). This example was first presented in Chapter 9.

C H A P T E R 1 1 ■ C A C H I N G |
407 |
Table 11-3. Values for the CacheItemRemovedReason Enumeration
Property |
Description |
EnableCaching |
If true, caching is switched on. It’s false by default. |
CacheExpirationPolicy |
Uses a value from the DataSourceCacheExpiry enumeration— |
|
Absolute for absolute expiration (which times out after a fixed |
|
interval of time) or Sliding for sliding expiration (which resets |
|
the time window every time the data object is retrieved from |
|
the cache). |
CacheDuration |
The number of seconds to cache the data object. If you are using |
|
sliding expiration, the time limit is reset every time the object is |
|
retrieved from the cache. The default, DataSourceCache- |
|
Expiry.Infinite, keeps cached items perpetually. |
CacheKeyDependency and |
Allows you to make a cached item dependent on another item |
SqlCacheDependency |
in the data cache (CacheKeyDependency) or on a table in your |
|
database (SqlCacheDependency). Dependencies are discussed |
|
in the “Cache Dependencies” section. |
|
|
Figure 11-4. Retrieving data from the cache
To fill the grid, you use the following SqlDataSource:
<asp:SqlDataSource ID="sourceEmployees" runat="server" ProviderName="System.Data.SqlClient" ConnectionString="<%$ ConnectionStrings:Northwind %>"
SelectCommand="SELECT EmployeeID, FirstName, LastName, Title, City FROM Employees WHERE City=@City">
<SelectParameters>
<asp:ControlParameter ControlID="lstCities" Name="City" PropertyName="SelectedValue" />
</SelectParameters>
</asp:SqlDataSource>