2. 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 in 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:
Key A
case-sensitive string, it is the key used to store the item in the
internal hashtable the ASP.NET cache relies upon. If this value is null,
an exception is thrown. If the key already exists, what happens depends
on the particular method you’re using: Add fails, while Insert just overwrites the existing item.
Value A non-null value of type Object that references the information stored in the cache. The value is managed and returned as an Object and needs casting to become useful in the application context.
Dependencies Object of type CacheDependency,
tracks a physical dependency between the item being added to the cache
and files, directories, database tables, or other objects in the
application’s cache. Whenever any of the monitored sources are modified,
the newly added item is marked obsolete and automatically removed.
Absolute Expiration Date A DateTime
object that represents the absolute expiration date for the item being
added. When this time arrives, the object is automatically removed from
the cache. Items not subject to absolute expiration dates must use the NoAbsoluteExpiration
constants representing the farthest allowable date. The absolute
expiration date doesn’t change after the item is used in either reading
or writing.
Sliding Expiration A TimeSpan
object, represents a relative expiration period for the item being
added. When you set the parameter to a non-null value, the
expiration-date parameter is automatically set to the current time plus
the sliding period. If you explicitly set the sliding expiration, you
cannot set the absolute expiration date too. From the user’s
perspective, these are mutually exclusive parameters. If the item is
accessed before its natural expiration time, the sliding period is
automatically renewed.
Priority A value picked out of the CacheItemPriority enumeration, denotes the priority of the item. It is a value ranging from Low to NotRemovable. The default level of priority is Normal. The priority level determines the importance of the item; items with a lower priority are removed first.
Removal Callback If specified, indicates the function that the ASP.NET Cache
object calls back when the item will be removed from the cache. In this
way, applications can be notified when their own items are removed from
the cache no matter what the reason is. When the session state works in InProc mode, a removal callback function is used to fire the Session_End event. The delegate type used for this callback is CacheItemRemoveCallback.
There are basically three ways to add new items to the ASP.NET Cache object—the set accessor of the 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. Note 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 3.
Table 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 4.
Table 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
Starting with ASP.NET 2.0, cache dependencies underwent some significant changes and improvements in. In previous versions, 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. Now, the CacheDependency
class is inheritable and can be used as a base to build custom
dependencies. In addition, ASP.NET 2.0 and newer versions come 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 some
// code here to trace the event
}
}
void Remove_Click(object sender, EventArgs e)
{
Cache.Remove("MyData");
}
Figure 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.
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 in Figure 16-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 CacheItemRemovedCallback. 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 callback. 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 Item’s 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 5.
Table 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 programmatic 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 HttpApplicationState,
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.
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.