When you think ASP, think...
Recent Articles
All Articles
ASP.NET Articles
ASPFAQs.com
Message Board
Related Web Technologies
User Tips!
Coding Tips
Search

Sections:
Book Reviews
Sample Chapters
Commonly Asked Message Board Questions
JavaScript Tutorials
MSDN Communities Hub
Official Docs
Security
Stump the SQL Guru!
Web Hosts
XML
Information:
Advertise
Feedback
Author an Article

ASP ASP.NET ASP FAQs Message Board Feedback
 
Print this Page!
Published: Wednesday, June 16, 2010

Extending ASP.NET Output Caching

By Scott Mitchell


Introduction


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.
Until recently, the underlying functionality of these two caching mechanisms was fixed - both cached data in the web server's memory. This has its drawbacks. In some cases, developers may want to save output cache content to disk. When using the data cache you may want to cache items to the cloud or to a distributed caching architecture like memcached. The good news is that with ASP.NET 4 and the .NET Framework 4, the output caching and data caching options are now much more extensible. Both caching features are now based upon the provider model, meaning that you can create your own output cache and data cache providers (or download and use a third-party or open source provider) and plug them into a new or existing ASP.NET 4 application.

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!

- continued -

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:

<%@ Page ... %>
<%@ OutputCache Duration="duration" VaryByParam="paramList" %>

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 OutputCacheProvider class, which is found in the System.Web.Caching namespace. 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 Add method 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.
To see these methods in action, I've created a disk-based custom output cache provider class named 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 DiskOutputCacheProvider class to add, remove, or update a cache item, the DiskOutputCacheProvider class 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 Dictionary object that associates the cache key supplied by ASP.NET with a DiskOutputCacheItem object. The DiskOutputCacheItem class, which I created, has two properties - FileName and 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 in Web.config. In addition to storing the cached data to disk, the DiskOutputCacheProvider class also maintains a log file (~/DiskCache/log.txt) 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 is, when Web.config has <compilation debug="true" />).

Let's examine some of the more interesting methods in the DiskOutputCacheProvider class, starting with the Set method. Recall that the Set method is called to add or update an item in the cache. It is passed a key, the data to cache, and its expiry.

public override void Set(string key, object entry, DateTime utcExpiry)
{
   var item = new DiskOutputCacheItem(this.GetSafeFileName(key), utcExpiry);

   WriteCacheData(item, entry);

   // Add item to CacheItems, if needed, or update the existing key, if it already exists
   lock (_lockObject)
   {
      if (this.CacheItems.ContainsKey(key))
         this.CacheItems[key] = item;
      else
         this.CacheItems.Add(key, item);
   }
}

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 from key that are not allowed in file names. Next, a call to the WriteCacheData method is made. The WriteCacheData method (not shown here) uses the 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 CacheItems Dictionary if the key is not already in there. If the key already exists in CacheItems then it is overwritten with the new DiskOutputCacheItem object. Note that the code that adds or updates CacheItems is done within a lock 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 key 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 CacheItems.Add to add 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 key. The 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 null.

public override object Get(string key)
{
   DiskOutputCacheItem item = null;
   CacheItems.TryGetValue(key, out item);

   // Was the item found?
   if (item == null)
      return null;

   // Has the item expired?
   if (item.UtcExpiry < DateTime.UtcNow)
   {
      // Item has expired
      this.Remove(key);

      return null;
   }
      
   return GetCacheData(item);
}

The 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 Remove method) and 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) uses the BinaryFormatter class to read the contents of the file and deserialize it.

Registering Your Custom Output Cache Provider


Now that we've examined the DiskOutputCacheProvider class's most important methods, let's turn our attention to actually using DiskOutputCacheProvider in an ASP.NET application. For starters, we need to register our custom provider in Web.config. This is done via the <caching> section's <outputCache> element:

<?xml version="1.0"?>
<configuration>
   <system.web>
      <compilation debug="true" targetFramework="4.0"/>
      <caching>
         <outputCache defaultProvider="DiskOutputCache">
            <providers>
               <add name="DiskOutputCache" type="DiskOutputCacheProvider"/>
            </providers>
         </outputCache>

      </caching>
   </system.web>
</configuration>

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 AspNetInternalProvider) to store the rendered output in memory, while having all other pages use the disk-based provider we registered in Web.config (DiskOutputCache). The demo application available for download provides an example that shows just how to do this.

Conclusion


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.

Happy Programming!

  • By Scott Mitchell


    Attachments:


  • Download the code used in this article

    Further Readings:


  • Output Caching in ASP.NET
  • Tip/Trick: Implement "Donut Caching" with the ASP.NET 2.0 Output Cache Substitution Feature
  • Extensible Output Caching with ASP.NET 4
  • Write your own OutputCacheProvider


  • ASP.NET [1.x] [2.0] | ASPMessageboard.com | ASPFAQs.com | Advertise | Feedback | Author an Article