Extending ASP.NET Output CachingBy Scott Mitchell
One of the most sure-fire ways to improve a web application's performance is to employ caching. Caching takes some expensive operation and stores its results in a quickly accessible location. Since it's inception, ASP.NET has offered two flavors of caching:
- Output Caching - caches the entire rendered markup of an ASP.NET page or User Control for a specified duration.
- Data Caching - a API for caching objects. Using the data cache you can write code to add, remove, and retrieve items from the cache.
This article focuses on extending the output caching feature. We'll walk through how to create a custom output cache provider that caches a page or User Control's rendered output to disk (as opposed to memory) and then see how to plug the provider into an ASP.NET application. A complete working example, available in both VB and C#, is available for download at the end of this article. Read on to learn more!
A Quick Overview of ASP.NET's Output Caching
Output caching has been a feature of ASP.NET since its inception. In a nutshell, output caching allows developers to cache the rendered output of an ASP.NET page or User Control for a specified duration. Output caching can improve the performance of a website by caching the rendered markup of a page on an initial request so that on subsequent requests the page does not need to be re-rendered (as its cached version is returned).
For example, imagine that you had an ASP.NET page that displayed the records from a database table named
Employees that listed the current employee
information. Without caching, each time this page is accessed a connection is made to the database, the table queried, and the results returned to the requesting
client. But how often does the employee information change? Probably not more than once a day or so, making many of those requests to the database superfluous. With
output caching, when this page is first visited the rendered HTML will be cached for a specified duration. During that time period, if a user requests this page,
the cached markup will be returned, thereby saving the database access and page rendering costs.
To implement output caching, simply add the
<% @OutputCache %> directive to the top of your page like so:
duration is the number of seconds for which the rendered markup remains in the cache. paramList is a list of parameters whose values vary the cache. For example, if the page that displays employees displays just those employees in a particular department as determined by a querystring parameter, then we want to vary the cache by this parameter.
Output caching can be used to cache the entire contents of a web page, but oftentimes there are specific regions in a page that need to remain dynamic. Perhaps part of the page shows the name of the currently logged on user or is dynamically generated to show randomly selected advertisements. In these cases, it is important that these parts of the page remain dynamic. There are two mechanisms by which dynamic regions can be defined when using output caching:
- Donut caching - allows you to define dynamic regions in an output cached web page, and
- Fragment caching - enables cached regions to be created (via User Controls) within a dynamic page.
By default, output caching stores the rendered markup of ASP.NET pages and User Controls in the web server's memory. As this article shows, with ASP.NET 4 it is possible to create a custom output cache provider that stores the rendered output elsewhere - to disk, to the cloud, to a distributed caching architecture, etc.
For more information on output caching, including a closer look at fragment caching and donut caching, refer to a previous article of mine, Output Caching in ASP.NET.
Creating a Custom Output Cache Provider
Implementing your own logic for ASP.NET's output caching behavior involves creating a custom output cache provider, which is a class that must derive from the
OutputCacheProviderclass, which is found in the
System.Web.Cachingnamespace. This class defines four abstract methods, which we must implement. These four methods are responsible for adding, updating, retrieving, and removing items from the cache. Moreover, these methods are all passed a key, which is a string that uniquely identifies the item to be added to the cache / retrieved from the cache / removed from the cache. Also, keep in mind that with output caching items are cached for a duration, so when adding or updating items in the cache, an time value is passed in, which specifies the expiry of the cache item in universal time.
- Add - this method is passed a key, the object to
add to the cache, and its expiry. However, if there already exists an item in the cache with this key, the
Addmethod must return the data associated with the key and not add the passed-in data.
- Get - this method is passed a key and returns the requested cached data (if it still exists in the cache and hasn't expired).
- Remove - this method is passed a key and is tasked with removing that key (and its data) from the cache.
- Set - this method is passed a key, the object to add or update in the cache, and its expiry.
DiskOutputCacheProvider, which is available for download at the end of this article. (The code samples we'll see in this article are in C#, but the download includes two projects - one in C# and one in VB.) Each time a request comes into the
DiskOutputCacheProviderclass to add, remove, or update a cache item, the
DiskOutputCacheProviderclass needs to track the expiry of the cache item and the name of the file where the cached data is located. This is handled by a property named
CacheItems, which is a
Dictionaryobject that associates the cache key supplied by ASP.NET with a
DiskOutputCacheItemclass, which I created, has two properties -
UtcExpiry- that indicate where the cached data is stored on disk and when it expires.
The actual files are stored on disk in the
~/DiskCache folder, although this folder name can be customized when registering the custom output cache provider
Web.config. In addition to storing the cached data to disk, the
DiskOutputCacheProvider class also maintains a log file (
that tracks what methods were called and what key and expiry information was supplied. Note that this logging only occurs when the application is in debug mode (that
<compilation debug="true" />).
Let's examine some of the more interesting methods in the
DiskOutputCacheProvider class, starting with the
Set method. Recall that the
method is called to add or update an item in the cache. It is passed a key, the data to cache, and its expiry.
The first thing the
Set method does is create a
DiskOutputCacheItem that stores the name of the file where the data will be cached and the
cache item's expiry. The name of the file where the cached data will be stored is determined by the
GetSafeFileName method, which removes any characters
key that are not allowed in file names. Next, a call to the
WriteCacheData method is made. The
WriteCacheData method (not shown here)
BinaryFormatter class to
serialize the contents of the data to be cached to a file.
After calling the
WriteCacheData method, the
Set method completes by adding the new
DiskOutputCacheItem to the
Dictionary if the key is not already in there. If the key already exists in
CacheItems then it is overwritten with the new
object. Note that the code that adds or updates
CacheItems is done within a
statement. This ensures that the code within the
lock statement is treated as a critical section, meaning that only one thread can execute the code
at a time. The reason this
lock statement is needed is because there is only one instance of the
DiskOutputCacheProvider class for the
entire application. Without the
lock consider what would happen if two threads entered this method when attempting to cache the same page's output,
which currently does not exist in the cache. Imagine that thread 1 reaches the
if statement that calls
ContainsKey to see if
is in the
Dictionary. This will return false (since we are presuming the item is not yet in the cache) so the thread will next call
item to the
Dictionary. But if thread 1's time slice ends and thread 2 comes in, we can run into a problem, as thread 2 will undergo
the same test and determine that
key is not in the
Dictionary. So now we have two threads, each of which is trying to add
item to the
Dictionary using the same
lock statement prevents this scenario from unfolding.
The next method I'd like to look at is the
Get method. The
Get method is responsible for returning the cached data for the specified key if it
exists and has not expired. If requested cache item does not exist (or has expired) then
Get should return
Get method starts by seeing if a
DiskOutputCacheItem object exists for the specified key. If not, it returns
null. If a
DiskOutputCacheItem object exists, its expiry is checked. If the item has expired then it is removed from the cache (by calling the
null is returned. If the item exists in the cache and has not expired then its data is returned. The
GetCacheData method (not shown here)
BinaryFormatter class to read the contents of the file and deserialize it.
Registering Your Custom Output Cache Provider
Now that we've examined the
DiskOutputCacheProviderclass's most important methods, let's turn our attention to actually using
DiskOutputCacheProviderin an ASP.NET application. For starters, we need to register our custom provider in
Web.config. This is done via the
Note that I've added a provider named
DiskOutputCache of type
DiskOutputCacheProvider (the name of our custom output cache provider class).
Moreover, I've instructed ASP.NET that my custom provider,
DiskOutputCache is the default output cache provider. With this configuration in place, anytime
my site uses output caching - either through an output cached ASP.NET page or User Control - the
DiskOutputCacheProvider custom provider will be invoked.
The output cache provider can also be specified on a per-request basis by overriding the
GetOutputCacheProviderName method in
Global.asax. This method returns the provider name that the ASP.NET output caching engine should use. By default, it will return the name based on the
provider configuration in
Web.config, but by overriding this method in
Global.asax you can implement custom logic to determine when to use
what provider. For instance, you may want to have your homepage and other very popular pages use ASP.NET's internal output cache provider (named
to store the rendered output in memory, while having all other pages use the disk-based provider we registered in
The demo application available for download provides an example that shows just how to do this.
ASP.NET has long offered developers the ability to cache the rendered output of their ASP.NET pages and User Controls. Until ASP.NET 4, the output cache engine had fixed internal behaviors, caching the rendered output in the web server's memory. With ASP.NET 4, the output caching and data caching features have been reworked to use the provider model, thereby allowing the underlying behavior to be customized. This article (and its associated code download) showed how to implement a disk-based output cache engine.