Chapter 22

Caching

WHAT’S IN THIS CHAPTER?

WROX.COM CODE DOWNLOADS FOR THIS CHAPTER

Please note that all the code examples in this chapter are available as a part of this chapter’s code download on the book’s website at www.wrox.com on the Download Code tab.

Performance is a key requirement for any application or piece of code that you develop. The browser helps with client-side caching of text and images, whereas the server-side caching you choose to implement is vital for creating the best possible performance. Caching is the process of storing frequently used data on the server to fulfill subsequent requests. You will discover that grabbing objects from memory is much faster than re-creating the web pages or items contained in them from scratch each time they are requested. Caching increases your application’s performance, scalability, and availability. The more you fine-tune your application’s caching approach, the better it performs.

This chapter focuses on caching, including the SQL cache-invalidation capabilities that ASP.NET provides. This chapter takes a close look at this unique aspect of caching. When you are using SQL cache invalidation, if the resultset from SQL Server changes, the output cache can be triggered to change automatically. This ensures that the end user always sees the latest resultset, and the data presented is never stale. After introducing SQL cache invalidation, this chapter also covers other performance enhancements. It discusses the post-cache substitution feature, which caches entire pages while dynamically replacing specified bits of content. Lastly, this chapter covers a capability that enables a developer to create custom dependencies.

CACHING

You have several ways to deal with caching in ASP.NET, one of which being that you can cache an entire HTTP response (the entire web page) using a mechanism called output caching. Two other methods are partial page caching and data caching. The following sections describe these methods.

Output Caching

Output caching is a way to keep the dynamically generated page content in the server’s memory or disk for later retrieval. This type of cache saves post-rendered content so it will not have to be regenerated again the next time it is requested. After a page is cached, it can be served up again when any subsequent requests are made to the server. You apply output caching by inserting an OutputCache page directive at the top of an .aspx page, as follows:

<%@ OutputCache Duration="60" VaryByParam="None" %>

The Duration attribute defines the number of seconds a page is stored in the cache. The VaryByParam attribute determines which versions of the page output are actually cached. You can generate different responses based on whether an HTTP-POST or HTTP-GET response is required. Other than the attributes for the OutputCache directive, ASP.NET includes the VaryByHeader, VaryByCustom, VaryByControl, and Location attributes. Additionally, the Shared attribute can affect user controls, as you will see later.

Caching in ASP.NET is implemented as an HttpModule that listens to all HttpRequests that come through the ASP.NET worker process. The OutputCacheModule listens to the application’s ResolveRequestCache and UpdateRequestCache events, handles cache hits and misses, and returns the cached HTML, bypassing the page handler if need be.

VaryByParam

The VaryByParam attribute can specify which QueryString parameters cause a new version of the page to be cached:

<%@ OutputCache Duration="90" VaryByParam="pageId;subPageId" %>

For example, if you have a page called navigation.aspx that includes navigation information in the QueryString, such as pageId and subPageId, the OutputCache directive shown here caches the page for every different value of pageId and subPageId. In this example, the number of pages is best expressed with an equation:

cacheItems = (num of pageIds) * (num of subPageIds)

where cacheItems is the number of rendered HTML pages that would be stored in the cache. Pages are cached only after they are requested and pass through the OutputCacheModule. The maximum amount of cache memory in this case is used only after every possible combination is visited at least once. Although these are just potential maximums, creating an equation that represents your system’s potential maximum is an important exercise.

If you want to cache a new version of the page based on any differences in the QueryString parameters, use VaryByParam="*", as in the following code:

<%@ OutputCache Duration="90" VaryByParam="*" %>

“Doing the math” when using the VaryBy attributes is important. For example, you could add VaryByHeader and cache a different version of the page based on the browser’s reported User-Agent HTTP header.

<%@ OutputCache Duration="90" VaryByParam="*" VaryByHeader="User-Agent"%>

The User-Agent identifies the user’s browser type. ASP.NET can automatically generate different renderings of a given page that are customized to specific browsers, so in many cases saving these various renderings in the cache makes sense. A Firefox user might have slightly different HTML than an IE user, so you do not want to send all users the same post-rendered HTML. Literally dozens, if not hundreds, of User-Agent strings exist in the wild because they identify more than just the browser type; this OutputCache directive could multiply into thousands of versions of this page being cached, depending on server load. In this case, you should measure the cost of the caching against the cost of re-creating the page dynamically.


WARNING Always cache what will give you the biggest performance gain, and prove that assumption with testing. Don’t “cache by coincidence” using attributes like VaryByParam="*". A common general rule is to cache the least possible amount of data at first and add more caching later if you determine a need for it. Remember that the server memory is a limited resource, so you may want configure the use of disk caching in some cases. Be sure to balance your limited resources with security as a primary concern; do not put sensitive data on the disk.

VaryByControl

VaryByControl can be a very easy way to get some serious performance gains from complicated user controls that render a lot of HTML that does not change often. For example, imagine a user control that renders a ComboBox showing the names of all the countries in the world. Perhaps those names are retrieved from a database and rendered in the combo box as follows:

<%@ OutputCache Duration="2592000" VaryByControl="comboBoxOfCountries" %>

Certainly, the names of the world’s countries do not change that often, so the Duration might be set to a month (in seconds). The rendered output of the UserControl is cached, allowing a page using that control to reap performance benefits of caching the control while the page itself remains dynamic.

VaryByCustom

Although the VaryBy attributes offer a great deal of power, sometimes you need more flexibility. If you want to take the OutputCache directive from the previous navigation example and cache by a value stored in a cookie, you can add VaryByCustom. The value of VaryByCustom is passed into the GetVaryByCustomString method that can be added to the Global.asax.cs. This method is called every time the page is requested, and it is the function’s responsibility to return a value.

A different version of the page is cached for each unique value returned. For example, suppose your users have a cookie called Language that has three potential values: en, es, and fr. You want to allow users to specify their preferred language, regardless of their language reported by their browser. Language also has a fourth potential value — it may not exist! Therefore, the OutputCache directive in the following example caches many versions of the page, as described in this equation:

cacheItems = (num of pageIds) * (num of subPageIds) * (4 possible Language values)

To summarize, suppose there were ten potential values for pageId, five potential subPageId values for each pageId, and four possible values for Language. That adds up to 200 different potential cached versions of this single navigation page. This math is not meant to scare you away from caching, but you should realize that with great (caching) power comes great responsibility.

The following OutputCache directive includes pageId and subPageId as values for VaryByParam, and VaryByCustom passes in the value of "prefs" to the GetVaryByCustomString callback function in Listing 22-1:

<%@ OutputCache Duration="90" VaryByParam="pageId;subPageId" VaryByCustom="prefs"%>

Caching in ASP.NET involves a tradeoff between CPU and memory: how hard is it to make this page, versus whether you can afford to hold 200 versions of it. If it is only 5KB of HTML, a potential megabyte of memory could pay off handsomely versus thousands and thousands of database accesses. Because most pages will hit the database at least once during a page cycle, every page request served from the cache saves you a trip to the database. Efficient use of caching can translate into cost savings if fewer database servers and licenses are needed.

The code in Listing 22-1 (code file Global.asax) returns the value stored in the Language cookie. The arg parameter to the GetVaryByCustomString method contains the string "prefs", as specified in VaryByCustom.

LISTING 22-1: GetVaryByCustomString callback method in the HttpApplication

VB

Overrides Function GetVaryByCustomString(ByVal context As HttpContext,
        ByVal arg As String) As String
    If arg.ToLower() = "prefs" Then
        Dim cookie As HttpCookie = context.Request.Cookies("Language")
        If cookie IsNot Nothing Then
            Return cookie.Value
        End If
    End If
    Return MyBase.GetVaryByCustomString(context, arg)
End Function

C#

public override string GetVaryByCustomString(HttpContext context, string arg)
{
    if (arg.ToLower() == "prefs")
    {
      HttpCookie cookie = context.Request.Cookies["Language"];
      if (cookie != null)
      {
          return cookie.Value;
      }
    }
    return base.GetVaryByCustomString(context, arg);
}

The GetVaryByCustomString method in Listing 22-1 is used by the HttpApplication in Global.asax and will be called for every page that uses the VaryByCustom OutputCache directive. If your application has many pages that use VaryByCustom, you can create a switch statement and a series of helper functions to retrieve whatever information you want from the user’s HttpContext and to generate unique values for cache keys.

Extending <outputCache>

Starting with the release of ASP.NET 4, you can extend how the OutputCache directive works and have it work off your own custom means to caching. This means that you can wire the OutputCache directive to any type of caching means including distributed caches, cloud caches, disc, XML, or anything else you can dream up.

To accomplish this, you are required to create a custom output-cache provider as a class and this class will need to inherit from the new System.Web.Caching.OutputCacheProvider class. To inherit from OutputCacheProvider, you must override the Add(), Get(), Remove(), and Set() methods to implement your custom version of output caching.

After you have your custom implementation in place, the next step is to configure this in a configuration file: the machine.config or the web.config file. Some changes have been made to the <outputCache> element in the configuration file to allow you to apply your custom cache extensions.

The <outputCache> element is found within the <caching> section of the configuration file and it now includes a new <providers> subelement.

<caching>
   <outputCache>
      <providers>
      </providers>
   </outputCache>
</caching>

Within the new <providers> subelement, you can nest an <add> element to make the appropriate references to your new output cache capability, which you built by deriving from the OutputCacheProvider class.

<caching>
   <outputCache defaultProvider="AspNetInternalProvider">
      <providers>
         <add name="myDistributedCacheExtension"
          type="Wrox.OutputCacheExtension.DistributedCacheProvider,
                  DistributedCacheProvider" />
      </providers>
   </outputCache>
</caching>

With this new <add> element in place, your new extended output cache is available to use. One new addition here to also pay attention to is the new defaultProvider attribute within the <outputCache> element. In this case, it is set to AspNetInternalProvider, which is the default setting in the configuration file. This means that by default the output cache works as it always has done and stores its cache in the memory of the computer that the program is running.

With your own output cache provider in place, you can now point to this provider through the OutputCache directive on the page as defined here:

<%@ OutputCache Duration="90" VaryByParam="*" 
    providerName="myDistributedCacheExtension" %>

If the provider name isn’t defined, then the provider that is defined in the configuration’s defaultProvider attribute is utilized.

Partial Page (UserControl) Caching

Similar to output caching, partial page caching (also called donut caching) enables you to cache only specific blocks of a web page. You can, for example, cache only the center of the page the user sees. Partial page caching is achieved with the caching of user controls so you can build your ASP.NET pages to utilize numerous user controls and then apply output caching to the selected user controls. This, in essence, caches only the parts of the page that you want, leaving other parts of the page outside the reach of caching. This feature is nice, and, if done correctly, can lead to pages that perform better. This requires a modular design to be planned up front so you can partition the components of the page into logical units composed of user controls.

Typically, user controls are designed to be placed on multiple pages to maximize reuse of common functionality. However, when these user controls (.ascx files) are cached with the @OutputCache directive’s default attributes, they are cached on a per-page basis. That means that even if a user control outputs the identical HTML when placed on pageA.aspx, as it does when placed on pageB.aspx, its output is cached twice. By enabling the Shared="true" attribute, the UserControl’s output can be shared among multiple pages and on sites that make heavy use of shared UserControls:

<%@ OutputCache Duration="300" VaryByParam="*" Shared="true" %>

The resulting memory savings can be surprisingly large because you only cache one copy of the post-rendered user control instead of caching a copy for each page. As with all optimizations, you need to test both for correctness of output as well as for memory usage.


WARNING If you have an ASCX UserControl using the OutputCache directive, remember that the UserControl exists only for the first request. If a UserControl has its HTML retrieved from the OutputCache, the control does not really exist on the ASPX page. Instead, a PartialCachingControl is created that acts as a proxy or ghost of that control.

Any code in the ASPX page that requires a UserControl to be constantly available will fail if that control is reconstituted from the OutputCache. So be sure to always check for this type of caching before using any control. The following code fragment illustrates the kind of logic required when accessing a potentially cached UserControl:

VB

Protected Sub Page_Load()
    If Not PossiblyCachedUserControl is Nothing Then
       ' Place code manipulating PossiblyCachedUserControl here.
    End If
End Sub

C#

protected void Page_Load()
{
    if (PossiblyCachedUserControl != null)
    {
       // Place code manipulating PossiblyCachedUserControl here.
    }
}

Post-Cache Substitution

Output caching has typically been an all-or-nothing proposition. The output of the entire page is cached for later use. However, often you want the benefits of output caching, but you also want to keep a small bit of dynamic content on the page. It would be a shame to cache a page but be unable to output a dynamic “Welcome, Scott!”

Ever since ASP.NET 2.0, a means has existed of using a post-cache substitution as an opportunity to affect the about-to-be-rendered page. A control is added to the page that acts as a placeholder. It calls a method that you specify after the cached content has been returned. The method returns any string output you like, but you should be careful not to abuse the feature. If your post-cache substitution code calls an expensive stored procedure, you could easily lose any performance benefits you might have expected.

Post-cache substitution is an easy feature to use. It gives you two ways to control the substitution:

To try this feature, create a new website with a Default.aspx page. Drag a label control and a substitution control to the design surface. The code in Listing 22-2 updates the label to display the current time, but the page is cached immediately and future requests return that cached value. Set the methodName property in the substitution control to GetUpdatedTime, meaning the name of the static method that is called after the page is retrieved from the cache.

The callback function must be static because the page that is rendered does not really exist at this point (an instance of it does not). Because you do not have a page instance to work with, this method is limited in its scope. However, the current HttpContext is passed into the method, so you have access to the Session, Request, and Response. The string returned from this method is injected into the Response in place of the substitution control.

LISTING 22-2: Using the substitution control

VB

<%@ Page Language="VB" %>
<%@ OutputCache Duration="30" VaryByParam="None" %>
 
<script runat="server">
Public Shared Function GetUpdatedTime(ByVal context As HttpContext) As String
    Return DateTime.Now.ToLongTimeString() + " by " + _
            context.User.Identity.Name
End Function
 
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
        Handles Me.Load
    Label1.Text = DateTime.Now.ToLongTimeString()
End Sub
</script>
 
<!DOCTYPE html>
 
<html>
<head>
    <title>Substitution Control</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Label ID="Label1" Runat="server" Text="Label"></asp:Label>
        <br />
        <asp:Substitution ID="Substitution1" runat="server"
             MethodName="GetUpdatedTime" />
        <br />
    </div>
    </form>
</body>
</html>

C#

<%@ Page Language="C#" %>
<%@ OutputCache Duration="30" VaryByParam="None" %>
 
<script runat="server">
    public static string GetUpdatedTime(HttpContext context)
    {
        return DateTime.Now.ToLongTimeString() + " by " +
                context.User.Identity.Name;
    }
    protected void Page_Load(object sender, EventArgs e)
    {
        Label1.Text = DateTime.Now.ToLongTimeString();
    }
</script>

The ASPX page in Listing 22-2 has a label and a post-cache substitution control. The control acts as a placeholder in the spot where you want fresh content injected after the page is returned from the cache. The first time the page is visited only the label is updated because no cached content is returned. The second time the page is visited, however, the entire page is retrieved from the cache — the page handler is not called and, consequently, none of the page-level events fire. However, the GetUpdatedTime method is called after the cache module completes its work. Figure 22-1 shows the result if the first line is cached and the second line is created dynamically.

FIGURE 22-1

image

NOTE Note that you use context.User.Identity.Name to determine the current user’s name. For it to work you may need to configure your application to use Windows authentication, and might also want to disable anonymous authentication, as Figure 22-2 shows.

FIGURE 22-2

image

HttpCachePolicy and Client-Side Caching

Caching is more than just holding data in memory on the server-side. A good caching strategy should also include the browser and its client-side caches, controlled by the Cache-Control HTTP header. HTTP headers are hints and directives to the browser on how to handle a request.

Some people recommend using HTML <meta> tags to control caching behavior. Be aware that neither the browsers nor routers along the way are obligated to pay attention to these directives. You might have more success using HTTP headers to control caching.

Because HTTP headers travel outside the body of the HTTP message, you have several options for viewing them. You can enable tracing (see Chapter 29) and view the headers from the tracing output. You can also use additional software, like your browsers’ web developer plug-ins (usually activated by F12), or the popular Firefox extension Firebug from getfirebug.com.


NOTE For background information on HTTP headers and controlling caching, see the document RFC 2616: Hypertext Transfer Protocol - HTTP/1.1, available on the World Wide Web Consortium’s site at www.w3c.org. You might also check out Fiddler at www.fiddler2.com. Commercial tools such as HttpWatch from www.httpwatch.com add more features.

To set up caching, create a file that writes the current time in its Load event. Now, view the default HTTP headers used by ASP.NET. Note that one header, Cache-Control: private, indicates to routers and other intermediates that this response is intended only for you (private).

The HttpCachePolicy class gives you an object model for managing client-side state that insulates you from adding HTTP headers yourself. Add the lines from Listing 22-3 to your Page_Load to influence the response’s headers and the caching behavior of the browser. This listing tells the browser not to cache this response in memory nor store it on disk. It also directs the response to expire immediately.

LISTING 22-3: Using HTTP headers to force the browser not to cache on the client side

VB

Protected Sub Page_Load(ByVal sender As Object, _
  ByVal e As System.EventArgs) Handles Me.Load
   Response.Cache.SetCacheability(HttpCacheability.NoCache)
   Response.Cache.SetNoStore()
   Response.Cache.SetExpires(DateTime.MinValue)
        
   Response.Write(DateTime.Now.ToLongTimeString())
End Sub

C#

protected void Page_Load(object sender, EventArgs e)
{
    Response.Cache.SetCacheability(HttpCacheability.NoCache);
    Response.Cache.SetNoStore();
    Response.Cache.SetExpires(DateTime.MinValue);
        
    Response.Write(DateTime.Now.ToLongTimeString());
}

Compare the results of running the page before adding the code from Listing 22-3 (top part of Figure 22-3) and then after adding the code (bottom part of Figure 22-3). Two new HTTP headers have been injected directing the client’s browser and the Cache-Control header has changed to no-cache, no-store. The output caching HttpModule will respect these HTTP headers, so sending no-cache, no-store to the browser also advises the HttpModule to record the response as a cache miss. Figure 22-3 shows the difference.

FIGURE 22-3

image

If your ASP.NET application contains a considerable number of relatively static or non–time-sensitive pages, consider what your client-side caching strategy is. Taking advantage of the disk space and the memory of your users’ powerful client machines is better than burdening your server’s limited resources.

CACHING PROGRAMMATICALLY

Output caching is a very declarative business. UserControls and pages can be marked up with OutputCache directives and dramatically change the behavior of your site. Declarative caching controls the lifecycle of HTML markup, but ASP.NET also includes deep imperative programmatic support for caching objects.

Data Caching Using the Cache Object

Another method of caching is to use the System.Web.Caching.Cache object to start caching specific data items for later use on a particular page or group of pages. The Cache object enables you to store everything from simple name/value pairs to more complex objects, such as data sets and entire .aspx pages.


WARNING Although it is quite similar to session state, the Cache object is shared by all users of the particular web server’s app domain that is hosting this application. Therefore, if you put a particular item in the cache, all users will be able to see that object. This may not work as expected in a server farm scenario because you cannot be assured of which server the user will hit next, and even if there is only one server involved, more than one app domain may be running this application. In addition, the server is free to invalidate any cached item at any time if it needs to reclaim some of the memory.

You use the Cache object in the following fashion:

VB

Cache("WhatINeedToStore") = myDataSet

C#

Cache["WhatINeedToStore"] = myDataSet;

After an item is in the cache, you can retrieve it later as shown here:

VB

Dim ds As New DataSet
ds = CType(Cache("WhatINeedToStore"), DataSet)

C#

DataSet ds = new DataSet();
ds = (DataSet)Cache["WhatINeedToStore"];

Using the Cache object is an outstanding way to cache your pages and is, in fact, what the OutputCache directive uses under the covers. This small fragment shows the simplest use of the Cache object. Simply put an object reference in it. However, the real power of the Cache object comes with its capability to invalidate itself. That is where cache dependencies come in.


WARNING You must always follow the pattern of testing to see whether an item is in the cache, and if not, you need to do whatever processing is necessary to re-create the object. After it’s re-created, you can insert it back into the cache to become available for the next request.

Controlling the ASP.NET Cache

Ordinarily the default parameters set by ASP.NET for the caching subsystem are appropriate for general use. They are configurable, however, within the machine.config or web.config files. These options let you make changes like preventing cached items from expiring when the system is under memory pressure, or turning off item expiration completely. You can set the maximum size of the application’s private bytes before the cache begins to flush items:

<system.web>
  <cache disableMemoryCollection="false"
  disableExpiration="false" privateBytesLimit="0"
  percentagePhysicalMemoryUsedLimit="90"
  privateBytesPollTime="00:02:00" />
. . .snip. . .

NOTE It’s best to leave the default values as they are unless you have done formal profiling of your application and understand how it utilizes the cache. You can find more detail on this section on MSDN at http://msdn.microsoft.com/en-us/library/vstudio/ms228248(v=vs.100).aspx.

Cache Dependencies

Using the Cache object, you can store and invalidate items in the cache based on several different dependencies. Way back in ASP.NET 1.0/1.1, the only possible dependencies were the following:

When inserting items into the cache using the Cache object, you set the dependencies with the Insert method, as shown in the following example:

Cache.Insert("DSN", connectionString,
   New CacheDependency(Server.MapPath("~/myconfig.xml")))

By using a dependency when the item being referenced changes, you remove the cache for that item from memory.

Cache dependencies have been improved ever since ASP.NET 2.0 with the addition of the AggregateCacheDependency class, the extendable CacheDependency class, and the capability to create your own custom CacheDependency classes. These three things are discussed in the following sections.

The AggregateCacheDependency Class

The AggregateCacheDependency class is like the CacheDependency class but it enables you to create an association connecting an item in the cache with many disparate dependencies of different types. For example, if you have a cached data item that is built from XML from a file and you have information from a SQL database table, you can create an AggregateCacheDependency with inserted CacheDependency objects for each subdependency. To do this, you call Cache.Insert() and add the AggregateCacheDependency instance. For example:

var agg = new AggregateCacheDependency();
agg.Insert(new CacheDependency(Server.MapPath("~/myconfig.xml")));
agg.Insert(new SqlCacheDependency("Northwind", "Customers"));
Cache.Insert("DSN", connectionString, agg);

Note that AggregateCacheDependency is meant to be used with different kinds of CacheDependency classes. If you simply want to associate one cached item with multiple files, use an overload of CacheDependency, as in this example:

VB

Cache.Insert("DSN", yourObject, _
    New System.Web.Caching.CacheDependency( _
        New String() _
       { _
         Server.MapPath("~/file1.xml"), _
         Server.MapPath("~/file2.xml") _
       } _
    ) _
)

C#

Cache.Insert("DSN", yourObject,
    new System.Web.Caching.CacheDependency(
        new string[]
        {
            Server.MapPath("~/file1.xml"),
            Server.MapPath("~/file2.xml")
        }
    )
);

The AggregateCacheDependency class is made possible by the support for extending the previously sealed CacheDependency class. You can use this innovation to create your own custom CacheDependency.

The Unsealed CacheDependency Class

A big change in caching ever since ASP.NET 2.0 was released was that the CacheDependency class had been refactored and unsealed (or made overrideable). This allowed you to create classes that inherit from the CacheDependency class and create more elaborate dependencies that are not limited to the Time, Key, or File dependencies.

When you create your own cache dependencies, you have the option to add procedures for such things as web services data, only-at-midnight dependencies, or textual string changes within a file. The dependencies you create are limited only by your imagination. The unsealing of the CacheDependency class puts you in the driver’s seat to let you decide when items in the cache need to be invalidated.

Along with the unsealing of the CacheDependency class, the ASP.NET team also built a SQL Server cache dependency — SqlCacheDependency. When a cache becomes invalid because a table changes within the underlying SQL Server, you now know it immediately in your ASP.NET application.

Because CacheDependency is unsealed, you can derive your own custom cache dependencies; that’s what you do in the next section.

Creating Custom Cache Dependencies

ASP.NET has time-based, file-based, and SQL-based CacheDependency support. You might ask yourself why you would write your own CacheDependency. Here are a few ideas:

The CacheDependency class exposes three members and a constructor overload that developers can use to do this work:

Listing 22-4 (code files RssCacheDependency.cs and RssCacheDependency.vb in the download for this chapter) creates a new RssCacheDependency that invalidates a cache key if an RSS (Rich Site Summary) XML document has changed.

LISTING 22-4: Creating an RssCacheDependency class

VB

Imports System
Imports System.Web
Imports System.Threading
Imports System.Web.Caching
Imports System.Xml
        
Public Class RssCacheDependency
        Inherits CacheDependency
        
    Dim backgroundThread As Timer
    Dim howOften As Integer = 900
    Dim RSS As XmlDocument
    Dim RSSUrl As String
        
    Public Sub New(ByVal URL As String, ByVal polling As Integer)
        howOften = polling
        RSSUrl = URL
        RSS = RetrieveRSS(RSSUrl)
        
        If backgroundThread Is Nothing Then
            backgroundThread = New Timer(
                New TimerCallback(AddressOf CheckDependencyCallback),
                Me, (howOften * 1000), (howOften * 1000))
        End If
    End Sub
        
    Function RetrieveRSS(ByVal URL As String) As XmlDocument
        Dim retVal As New XmlDocument
        retVal.Load(URL)
        Return retVal
    End Function
        
    Public Sub CheckDependencyCallback(ByVal Sender As Object)
        Dim CacheDepends As RssCacheDependency = _
            CType(Sender, RssCacheDependency)
        Dim NewRSS As XmlDocument = RetrieveRSS(RSSUrl)
        If Not NewRSS.OuterXml = RSS.OuterXml Then
            CacheDepends.NotifyDependencyChanged(CacheDepends, EventArgs.Empty)
        End If
        End Sub
        
    Protected Overrides Sub DependencyDispose()
        backgroundThread = Nothing
        MyBase.DependencyDispose()
    End Sub
        
    Public ReadOnly Property Document() As XmlDocument
        Get
            Return RSS
        End Get
    End Property
End Class

C#

using System;
using System.Web;
using System.Threading;
using System.Web.Caching;
using System.Xml;
        
public class RssCacheDependency : CacheDependency
{
    Timer backgroundThread;
    int howOften = 900;
    XmlDocument RSS;
    string RSSUrl;
        
    public RssCacheDependency(string URL, int polling)
    {
        howOften = polling;
        RSSUrl = URL;
        RSS = RetrieveRSS(RSSUrl);
        
        if (backgroundThread == null)
        {
            backgroundThread = new Timer(
                   new TimerCallback(CheckDependencyCallback),
                   this, (howOften * 1000), (howOften * 1000));
        }
    }
        
    public XmlDocument RetrieveRSS(string URL)
    {
        XmlDocument retVal = new XmlDocument();
        retVal.Load(URL);
        return retVal;
    }
        
    public void CheckDependencyCallback(object sender)
    {
        RssCacheDependency CacheDepends = sender as RssCacheDependency;
        XmlDocument NewRSS = RetrieveRSS(RSSUrl);
        if (NewRSS.OuterXml != RSS.OuterXml)
        {
            CacheDepends.NotifyDependencyChanged(CacheDepends, EventArgs.Empty);
        }
    }
        
    override protected void DependencyDispose()
    {
        backgroundThread = null;
        base.DependencyDispose();
    }
        
    public XmlDocument Document
    {
        get
        {
            return RSS;
        }
    }
}

To use the new code, open a new website and put the RssCacheDependency class in the App_Code folder. Create a new file and add two textboxes, a label, and a button onto the page. Execute the website and enter an RSS URL for a blog (like the MSDN feed at http://sxp.microsoft.com/feeds/3.0/msdnnews/msdnnews), and click the button. The program checks the Cache object using the URL itself as a key. If the XmlDocument containing RSS does not exist in the cache, a new RssCacheDependency is created with a 10-minute (600-second) timeout. The XmlDocument is then cached, and all future requests within the next 10 minutes to this page retrieve the RSS XmlDocument from the cache.

Next, your new RssCacheDependency class from Listing 22-4 is illustrated in Listing 22-5. The RssCacheDependency is created and passed into the call to Cache.Insert(). The Cache object handles the lifetime and calling of the methods of the RssCacheDependency instance.

LISTING 22-5: Using the RssCacheDependency class

VB

<%@ Page Language="VB" ValidateRequest="false" %>
 
<script runat="server">
Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs)
    Dim RSSUrl As String = TextBox1.Text
    Label2.Text = "Loaded From Cache"
    If Cache(TextBox1.Text) Is Nothing Then
        Label2.Text = "Loaded Fresh"
        Dim itDepends As New RssCacheDependency(RSSUrl, 600)
        Cache.Insert(RSSUrl, itDepends.Document, itDepends)
    End If
    TextBox2.Text = CType(Cache(TextBox1.Text), _ 
        System.Xml.XmlDocument).OuterXml
End Sub
</script>
 
<!DOCTYPE html>
 
<html>
<head id="Head1" runat="server">
    <title>Custom Cache Dependency Example</title>
</head>
<body>
    <form id="Form1" runat="server"> RSS URL:
       <asp:TextBox ID="TextBox1" Runat="server"/>
       <asp:Button ID="Button1" onclick="Button1_Click" Runat="server" 
        Text="Get RSS" />
        Cached:<asp:Label ID="Label2" Runat="server"></asp:Label><br />
        RSS:<br />
        <asp:TextBox ID="TextBox2" Runat="server" TextMode="MultiLine"
         Width="800px" Height="300px"></asp:TextBox>
    </form>
</body>
</html>

C#

<script runat="server">
    void Button1_Click(object sender, System.EventArgs e)
    {
        string RSSUrl = TextBox1.Text;
        Label2.Text = "Loaded From Cache";
        if (Cache[TextBox1.Text] == null)
        {
            Label2.Text = "Loaded Fresh";
            RssCacheDependency itDepends = new RssCacheDependency(RSSUrl, 30);
            Cache.Insert(RSSUrl, itDepends.Document, itDepends);
        }
        TextBox2.Text = ((System.Xml.XmlDocument)Cache[TextBox1.Text]).OuterXml;
    }
</script>

The RssCacheDependency class creates a Timer background thread to poll for changes in the RSS feed. If it detects changes, the RssCacheDependency notifies the caching subsystem with the NotifyDependencyChanged event. The cached value with that key clears, and the next page view forces a reload of the requested RSS from the specified feed.

.NET 4.x’s New Object Caching Option

From what you have seen so far with the System.Web.Caching.Cache object, you can see that it is quite powerful and allows for you to even create a custom cache. This extensibility and power has changed under the hood of the Cache object, though.

Driving this is the System.Runtime.Caching.dll, as what was in the System.Web version has been refactored out and everything was rebuilt into the new namespace of System.Runtime.Caching.

The reason for this change wasn’t so much for the ASP.NET developer, but instead for other application types such as Windows Forms, Windows Presentation Foundation, and more. The reason for this is that the System.Web.Caching.Cache object was so useful that other application developers were bringing over the System.Web namespace into their projects to make use of this object. So, to get away from a Windows Forms developer needing to bring the System.Web.dll into their project just to use the Cache object it provided, this was all extracted out and extended with the System.Runtime.Caching namespace.

As an ASP.NET developer, you can still make use of the System.Web.Caching.Cache object just as you did in all the prior versions of ASP.NET. It isn’t going away. However, it is important to note that as the .NET Framework evolves, the .NET team will be making its investments into the System.Runtime.Caching namespace rather than System.Web.Caching. This means that over time, you will most likely see additional enhancements in the System.Runtime.Caching version that don’t appear in the System.Web.Caching namespace as you might expect. With that said, it doesn’t also mean that you need to move everything over to the new System.Runtime.Caching namespace to make sure you are following the strategic path of Microsoft, because the two caches are managed together under the covers.

This section runs through an example of using the cache from the System.Runtime.Caching namespace. For this example, the ASP.NET page simply uses a Label control that shows the name of a user that is stored in an XML file. The first step is to create an XML file in the App_Data folder and name it Username.xml. This simple file is presented in Listing 22-6 (code file Username.xml).

LISTING 22-6: The contents of the Username.xml file

<?xml version="1.0" encoding="utf-8" ?>
<usernames>
  <user>Christian Wenz</user>
</usernames>

With this XML file sitting in the root of your drive, now turn your attention to the Default.aspx code-behind page to use the name in the file and present it into a single Label control on the page. The code-behind for the Default.aspx page is presented in Listing 22-7.

LISTING 22-7: Using the System.Runtime.Caching namespace

VB

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Runtime.Caching" %>
 
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object, _
  ByVal e As EventArgs)
 
    Dim cache As ObjectCache = MemoryCache.Default
 
    Dim usernameFromXml As String = _
      TryCast(cache("userFromXml"), String)
 
    If usernameFromXml Is Nothing Then
        Dim userFilePath As New List(Of String)()
        userFilePath.Add(Server.MapPath("~/App_Data/Username.xml"))
 
        Dim policy As New CacheItemPolicy()
        policy.ChangeMonitors.Add(New HostFileChangeMonitor(userFilePath))
 
        Dim xdoc As XDocument = _
          XDocument.Load(Server.MapPath("~/App_Data/Username.xml"))
        Dim query = From u In xdoc.Elements("usernames")
            Select u.Value
 
        usernameFromXml = query.First().ToString()
 
        cache.Set("userFromXml", usernameFromXml, policy)
    End If
 
    Label1.Text = usernameFromXml
End Sub 
</script>
 
<!DOCTYPE html>
 
<html>
<head runat="server">
    <title>Using System.Runtime.Caching</title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <asp:Label ID="Label1" runat="server"></asp:Label>
        </div>
    </form>
</body>
</html>

C#

<script runat="server">
    protected void Page_Load(object sender, EventArgs e)
    {
        ObjectCache cache = MemoryCache.Default;
 
        string usernameFromXml = cache["userFromXml"] as string;
 
        if (usernameFromXml == null)
        {
            List<string> userFilePath = new List<string>();
            userFilePath.Add(Server.MapPath("~/App_Data/Username.xml"));
 
            CacheItemPolicy policy = new CacheItemPolicy();
            policy.ChangeMonitors.Add(new HostFileChangeMonitor(userFilePath));
 
            XDocument xdoc = XDocument.Load(Server.MapPath(
                "~/App_Data/Username.xml"));
            var query = from u in xdoc.Elements("usernames")
                        select u.Value;
 
            usernameFromXml = query.First().ToString();
 
            cache.Set("userFromXml", usernameFromXml, policy);
        }
 
        Label1.Text = usernameFromXml;
    }
</script>

This example from Listing 22-7 makes use of the new cache at System.Runtime.Caching. You need to reference this namespace in your ASP.NET project for this to work (see Figure 22-4).

FIGURE 22-4

image

To start, you create a default instance of the cache object:

ObjectCache cache = MemoryCache.Default;

You then can work with this cache as you would with the traditional ASP.NET cache object:

string usernameFromXml = cache["userFromXml"] as string;

To get the cache started, you need to create an object that defines what type of cache you are dealing with. You can build a custom implementation, or you can use one of the default implementations provided with .NET 4.x:

CacheItemPolicy policy = new CacheItemPolicy();
policy.ChangeMonitors.Add(new HostFileChangeMonitor(userFilePath));

The HostFileChangeMonitor is a means to look at directories and file paths and monitor for change. So, for instance, when the XML file changes, that triggers an invalidation of the cache. Other implementations of the ChangeMonitor object include the FileChangeMonitor and the SqlChangeMonitor.

Running this example, notice that the text Christian Wenz is loaded into the cache on the first run, and this text appears in the Label1 control. Keep your application running and then you can go back to the XML file and change the value, and you will then notice that this causes the cache to be invalidated on the page’s refresh.

USING THE SQL SERVER CACHE DEPENDENCY

To utilize the SQL Server Cache Dependency feature in ASP.NET, you must perform a one-time setup of your SQL Server database. To set up your SQL Server, use the aspnet_regsql.exe tool found at C:\Windows\MicroSoft.NET\Framework\v4.0.xxxxx\. This tool makes the necessary modifications to SQL Server so that you can start working with the SQL cache-invalidation features.

Follow these steps when using the SQL Server Cache Dependency features:

1. Enable your database for SQL Cache Dependency support.
2. Enable a table or tables for SQL Cache Dependency support.
3. Include SQL connection string details in the ASP.NET application’s web.config file.
4. Utilize the SQL Cache Dependency features in one of the following ways:
  • Programmatically create a SqlCacheDependency object in code.
  • Add a SqlDependency attribute to an OutputCache directive.
  • Add a SqlCacheDependency instance to the Response object via Response.AddCacheDependency.

This section explains all the steps required and the operations available to you.

To start, you need to get at the aspnet_regsql.exe tool. Open the Visual Studio Command Prompt, and then type this command:

aspnet_regsql.exe -?

This code outputs the help command list for this command-line tool, as shown in the following:

                       -- SQL CACHE DEPENDENCY OPTIONS --
        
-d <database>            Database name for use with SQL cache dependency. The
                          database can optionally be specified using the
                          connection string with the -c option instead.
                          (Required)
        
-ed                      Enable a database for SQL cache dependency.
        
-dd                      Disable a database for SQL cache dependency.
        
-et                      Enable a table for SQL cache dependency. Requires -t
                         option.
        
-dt                      Disable a table for SQL cache dependency. Requires -t
                         option.
        
-t <table>               Name of the table to enable or disable for SQL cache
                         dependency. Requires -et or -dt option.
        
-lt                      List all tables enabled for SQL cache dependency.

The following sections show you how to use some of these commands.


NOTE Microsoft’s trusted Northwind database is used as an example in this chapter, and it’s not part of the chapter code downloads. It needs to be retrieved and installed separately. You can download the Northwind database used for this chapter at www.wrox.com/go/SQLServer2012DataSets. Chapter 24 contains more information on retrieving this sample database. Of course, the techniques covered also work with other databases.

Enabling Databases for SQL Server Cache Invalidation

To use SQL Server cache invalidation with a SQL Server database, begin with two steps. The first step enables the appropriate database. In the second step, you enable the tables that you want to work with. You must perform both steps for this process to work. If you want to enable your databases for SQL cache invalidation and you are working on the computer where the SQL Server instance is located, you can use the following construct. If your SQL instance is on another computer, change localhost in this example to the name of the remote machine.

aspnet_regsql.exe -S localhost -U username -P password -d Northwind -ed

The syntax for using the SQLEXPRESS instance on the local machine, using Windows authentication, looks like this:

aspnet_regsql.exe -S .\SQLEXPRESS -E -d Northwind -ed

This produces something similar to the following output:

Enabling the database for SQL cache dependency.
 
.
 
Finished.

From this command prompt, you can see that you simply enabled the Northwind database (a sample database for SQL Server) for SQL cache invalidation. The name of the SQL machine was passed in with -S, the username with -U, the database with -d, and most importantly, the command to enable SQL cache invalidation was -ed.

Now that you have enabled the database for SQL cache invalidation, you can enable one or more tables contained within the Northwind database.

Enabling Tables for SQL Server Cache Invalidation

You enable more tables by using the following command:

aspnet_regsql.exe -S localhost -U username -P password -d Northwind -t Customers 
-et
        
aspnet_regsql.exe -S localhost -U username -P password -d Northwind -t Products -et

You can see that this command is not much different from the one for enabling the database, except for the extra -t Customers entry and the use of -et to enable the table rather than -ed to enable a database. Customers is the name of the table that is enabled in this case.

Go ahead and enable both the Customers and Products tables. You run the command once per table. After a table is successfully enabled, you receive the following response:

Enabling the table for SQL cache dependency.
 
.
 
Finished.

After the table is enabled, you can begin using the SQL cache-invalidation features. However, before you do, the following section shows you what happens to SQL Server when you enable these features.

Looking at SQL Server

Now that the Northwind database and the Customers and Products tables have all been enabled for SQL cache invalidation, look at what has happened in SQL Server. If you open the SQL Server Management Studio (or its Express edition), you see a new table contained within the Northwind database — AspNet_SqlCacheTablesForChangeNotification (whew, that’s a long one!). Your screen should look like Figure 22-5.

FIGURE 22-5

image

At the top of the list of tables in the left pane, you see the AspNet_SqlCacheTablesForChangeNotification table. ASP.NET uses this table to learn which tables are being monitored for change notification and also to make note of any changes to the tables being monitored. The new table is actually quite simple when you look at its columns, also shown in Figure 22-5.

Looking at the Tables That Are Enabled

Using the aspnet_regsql.exe tool, you can see (by using a simple command) which tables are enabled in a particular database. If you are working through the preceding examples, you see that so far you have enabled the Customers and Products tables of the Northwind database. To get a list of the tables that are enabled, use something similar to the following command:

aspnet_regsql.exe -S localhost -U username -P password -d Northwind -lt

The -lt command produces a simple list of tables enabled for SQL cache invalidation. Inputting this command produces the following results:

Listing all tables enabled for SQL cache dependency:
Customers
Products

Disabling a Table for SQL Server Cache Invalidation

Now that you know how to enable your SQL Server database for SQL Server cache invalidation, look at how you remove the capability for a specific table to be monitored for this process. To remove a table from the SQL Server cache-invalidation process, you use the -dt command.

In the preceding example, using the -lt command showed that you have both the Customers and Products tables enabled. Next, you remove the Products table from the process using the following command:

aspnet_regsql.exe -S localhost -U username -P password -d Northwind -t Products -dt

You can see that all you do is specify the name of the table using the -t command followed by a -dt command (disable table). The command line for disabling table caching will again list the tables that are enabled for SQL Server cache invalidation; this time, the Products table is not listed — instead, Customers, the only enabled table, is listed.

Disabling a Database for SQL Server Cache Invalidation

Not only can you pick and choose the tables that you want to remove from the process, but you can also disable the entire database for SQL Server cache invalidation. To disable an entire database, you use the -dd command (disable database).


NOTE Note that disabling an entire database for SQL Server cache invalidation also means that every single table contained within this database is also disabled.

This example shows the Northwind database being disabled on my computer:

C:\>aspnet_regsql -S localhost -U username -P password -d Northwind -dd
 
Disabling the database for SQL cache dependency.
.
Finished.

To ensure that the table is no longer enabled for SQL Server cache invalidation, I attempted to list the tables that were enabled for cache invalidation using the -lt command. I received the following error:

C:\ >aspnet_regsql -S localhost -U username -P password -d Northwind -lt
An error has happened. Details of the exception:
The database 'Northwind' is not enabled for SQL cache notification, please
 use the System.Web.Caching.SqlCacheDependencyAdmin.EnableNotifications method,
 or the command line tool aspnet_regsql. To use the tool, please run
 'aspnet_regsql.exe -?' for more information.

If you now open the Northwind database in the SQL Server Management Studio, you can see that the AspNet_SqlCacheTablesForChangeNotification table has been removed from the database.

SQL Server Cache Invalidation

SQL Server supports a more granular series of notification that doesn’t require polling. Direct notification of changes is a built-in feature of SQL Server and is presented via the ADO.NET SqlCommand. Listing 22-8 shows this.

LISTING 22-8: Using SQL Server cache invalidation

VB

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data.SqlClient" %>
 
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
  Handles Me.Load
    Response.Write("Page created: " + DateTime.Now.ToLongTimeString())
    Dim connStr As String = ConfigurationManager.ConnectionStrings( _
      "AppConnectionString1").ConnectionString
    SqlDependency.Start(connStr)
    Dim connection As New SqlConnection(connStr)
    Dim command As New SqlCommand("Select * FROM Customers", connection)
    Dim depends As New SqlCacheDependency(command)
 
    connection.Open()
    GridView1.DataSource = command.ExecuteReader()
    GridView1.DataBind()
 
    connection.Close()
 
    'Now, do what you want with the sqlDependency object like:
    Response.AddCacheDependency(depends)
End Sub
</script>
 
<!DOCTYPE html>
 
<html>
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <asp:GridView ID="GridView1" runat="server" Height="400px" Width="400px">
        </asp:GridView>
    </form>
</body>
</html>

C#

<script runat="server">
    protected void Page_Load(object sender, EventArgs e)
    {
        Response.Write("Page created: " + DateTime.Now.ToLongTimeString());
        string connStr = ConfigurationManager.ConnectionStrings[
            "AppConnectionString1"].ConnectionString;
        SqlDependency.Start(connStr);
        SqlConnection connection = new SqlConnection(connStr);
        SqlCommand command = new SqlCommand("Select * FROM Customers", connection);
        SqlCacheDependency depends = new SqlCacheDependency(command);
        
        connection.Open();
        GridView1.DataSource = command.ExecuteReader();
        GridView1.DataBind();
 
        connection.Close();
 
        Response.AddCacheDependency(depends);
    }
</script>

SQL Server supports both programmatic and declarative techniques when caching. Use the string "CommandNotification" in the OutputCache directive to enable notification-based caching for a page as in this example. You can specify SQL caching options either programmatically or declaratively, but not both. Note that you must first call System.Data.SqlClient.SqlDependency.Start, passing in the connection string, to start the SQL notification engine:

<%@ OutputCache Duration="3600" VaryByParam="none"
    SqlDependency="CommandNotification"%>

Alternatively, if you’re using a SqlDataSource control from within your ASP.NET page, you would do the following:

<asp:SqlDataSource EnableCaching="true" SqlCacheDependency="CommandNotification"
    CacheDuration="2600" />

As data changes within SQL Server, SQL and ADO.NET automatically invalidate data cached on the web server.

CONFIGURING YOUR ASP.NET APPLICATION

After you enable a database for SQL Server cache invalidation and enable a couple of tables within this database, the next step is to configure your application for SQL Server cache invalidation.

To configure your application to work with SQL Server cache invalidation, the first step is to make some changes to the web.config file. In the web.config file, specify that you want to work with the Northwind database, and you want ASP.NET connected to it.

Listing 22-9 (code file web.config) shows an example of how you should change your web.config file to work with SQL Server cache invalidation. The pollTime attribute is not needed if you are using a recent version of SQL Server (2005 or higher) notification because it uses database events instead of the polling needed for earlier versions.

LISTING 22-9: Configuring the web.config file

<configuration>
        
   <connectionStrings>
      <add name="AppConnectionString1" connectionString="Data Source=localhost;
        User ID=username;Password=password;Database=Northwind;Persist Security Info=False"
        providerName="System.Data.SqlClient" />
   </connectionStrings>
        
   <system.web>
        
      <caching>
         <sqlCacheDependency enabled="true">
            <databases>
               <add name="Northwind" connectionStringName="AppConnectionString1"
                pollTime="500" />
            </databases>
         </sqlCacheDependency>
      </caching>
        
   </system.web>
</configuration>

From this listing, you can see that the first thing established is the connection string to the Northwind database using the <connectionStrings> element in the web.config file. Note the name of the connection string because it is utilized later in the configuration settings for SQL Server cache invalidation.

The SQL Server cache invalidation is configured using the <caching> element. This element must be nested within the <system.web> elements. Because you are working with a SQL Server cache dependency, you must use a <sqlCacheDependency> child node. You enable the entire process by using the enabled="true" attribute. After this attribute is enabled, you work with the <databases> section. You use the <add> element, nested within the <databases> nodes, to reference the Northwind database. Table 22-1 explains the attributes of the <add> element.

TABLE 22-1

ATTRIBUTE DESCRIPTION
Name Provides an identifier to the SQL Server database.
connectionStringName Specifies the name of the connection. Because the connection string in the preceding example is called AppConnectionString1, you use this value for the connectionStringName attribute as well.
pollTime Specifies the time interval from one SQL Server poll to the next. The default is .5 seconds or 500 milliseconds (as shown in Listing 22-9). This is not needed for SQL Server 2005, 2008, and 2012 notification.

Now that the web.config file is set up correctly, you can start using SQL Server cache invalidation on your pages. ASP.NET makes a separate SQL Server request on a completely different thread to the AspNet_SqlCacheTablesForChangeNotification table to see whether the changeId number has been incremented. If the number is changed, ASP.NET knows that an underlying change has been made to the SQL Server table and that a new resultset should be retrieved. When it checks to see whether it should make a SQL Server call, the request to the small AspNet_SqlCacheTablesForChangeNotification table has a single result. With SQL Server cache invalidation enabled, this is done so quickly that you really notice the difference.

TESTING SQL SERVER CACHE INVALIDATION

Now that the web.config file is set up and ready to go, the next step is to actually apply these capabilities to a page. Listing 22-10 is an example of a page using the SQL Server cache-invalidation process.

LISTING 22-10: An ASP.NET page utilizing SQL Server cache invalidation

VB

<%@ Page Language="C#" %>
<%@ OutputCache Duration="30" VaryByParam="none" 
    SqlDependency="Northwind:Customers"%>
 
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
    Label1.Text = "Page created at " & DateTime.Now.ToString()
End Sub
</script>
 
<!DOCTYPE html>
 
<html>
<head id="Head1" runat="server">
    <title>Sql Cache Invalidation</title>
</head>
<body>
    <form id="form1" runat="server">
        <asp:Label ID="Label1" runat="server"></asp:Label><br />
        <br />
        <asp:GridView ID="GridView1" Runat="server" DataSourceID="SqlDataSource1">
        </asp:GridView>
        <asp:SqlDataSource ID="SqlDataSource1" Runat="server" 
         SelectCommand="Select * From Customers"
         ConnectionString="<%$ ConnectionStrings:AppConnectionString1 %>" 
         ProviderName="<%$ ConnectionStrings:AppConnectionString1.providername %>">
        </asp:SqlDataSource>
    </form>
</body>
</html>

C#

<%@ Page Language="VB" %>
<%@ OutputCache Duration="30" VaryByParam="none" 
    SqlDependency="Northwind:Customers"%>
 
<script runat="server">
    protected void Page_Load(object sender, System.EventArgs e)
    {
        Label1.Text = "Page created at " + DateTime.Now.ToShortTimeString();
    }
</script>

The first and most important part of this page is the OutputCache page directive that is specified at the top of the file. Typically, the OutputCache directive specifies how long the page output is held in the cache using the Duration attribute. Next is the VaryByParam attribute. An addition here is the SqlDependency attribute. This enables a particular page to use SQL Server cache invalidation. The following line shows the format of the value for the SqlDependency attribute:

SqlDependency="database:table"

The value of Northwind:Customers specifies that you want the SQL Server cache invalidation enabled for the Customers table within the Northwind database. The Duration attribute of the OutputCache directive shows you that, typically, the output of this page is stored in the cache for a long time — but this cache is invalidated immediately if the Customers table has any underlying changes made to the data that it contains.

A change to any of the cells in the Customers table of the Northwind database invalidates the cache, and a new cache is generated from the result, which now contains a new SQL Server database request. Figure 22-6 shows an example of the page generated the first time it is run.

FIGURE 22-6

image

From this figure, you can see the contents of the customer with the CustomerID of ALFKI. For this entry, go to SQL Server and change the value of the ContactName from Maria Anders to Mary Anders. If you were not using SQL Server cache invalidation, this change would have done nothing to the output cache. The original page output in the cache would still be present and the end user would still see the Maria Anders entry for the duration specified in the page’s OutputCache directive. However, because you are using SQL Server cache invalidation, after the underlying information in the table is changed, the output cache is invalidated, a new resultset is retrieved, and the new resultset is cached. When a change has been made, you see the results as shown in Figure 22-7.

FIGURE 22-7

image

Notice also that the text “Page created at” includes an updated time indicating when this page was rendered.

Adding More Than One Table to a Page

The preceding example shows how to use SQL Server cache invalidation for a single table on the ASP.NET page. What do you do if your page is working with two or more tables?

To add more than one table, you use the OutputCache directive shown here:

SqlDependency="database:table;database:table"

From this example, you can see that the value of the SqlDependency attribute separates the databases and tables with a semicolon. If you want to work with both the Customers table and the Products table of the Northwind database, you construct the value of the SqlDependency attribute as follows:

SqlDependency="Northwind:Customers;Northwind:Products"

Attaching SQL Server Cache Dependencies to the Request Object

In addition to changing settings in the OutputCache directive to activate SQL Server cache invalidation, you can also set the SQL Server cache invalidation programmatically. To do so, use the SqlCacheDependency class, which is illustrated in Listing 22-11.

LISTING 22-11: Working with SQL Server cache invalidation programmatically

VB

Dim myDependency As SqlCacheDependency = _
   New SqlCacheDependency("Northwind", "Customers")
Response.AddCacheDependency(myDependency)
Response.Cache.SetValidUntilExpires(true)
Response.Cache.SetExpires(DateTime.Now.AddMinutes(60))
Response.Cache.SetCacheability(HttpCacheability.Public)

C#

SqlCacheDependency myDependency = new SqlCacheDependency("Northwind", "Customers");
Response.AddCacheDependency(myDependency);
Response.Cache.SetValidUntilExpires(true);
Response.Cache.SetExpires(DateTime.Now.AddMinutes(60));
Response.Cache.SetCacheability(HttpCacheability.Public);

You first create an instance of the SqlCacheDependency object, assigning it the value of the database and the table at the same time. The SqlCacheDependency class takes the following argument:

SqlCacheDependency(System.Data.SqlClient.SqlCommand sqlCmd)

After the SqlCacheDependency class is in place, you add the dependency to the Cache object and set some of the properties of the Cache object as well. You can do this either programmatically or through the OutputCache directive.

Attaching SQL Server Cache Dependencies to the Cache Object

In addition to attaching SQL Server cache dependencies to the Request object, you can attach them to the Cache object for data that can be cached much longer. The Cache object is contained within the System.Web.Caching namespace, and it enables you to work programmatically with the caching of any type of object. Listing 22-12 shows a page that utilizes the Cache object with the SqlDependency object.

LISTING 22-12: Using the Cache object with the SqlDependency object

VB

<%@ Page Language="VB" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
 
<!DOCTYPE html>
 
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
    Dim myCustomers As DataSet
    myCustomers = CType(Cache("firmCustomers"), DataSet)
 
    If myCustomers Is Nothing Then
       Dim conn As SqlConnection = 
         New SqlConnection(ConfigurationManager.ConnectionStrings( _
          "AppConnectionString1").ConnectionString)
       Dim da As SqlDataAdapter = _
        New SqlDataAdapter("Select * From Customers", conn)
 
       myCustomers = New DataSet
       da.Fill(myCustomers)
 
       Dim myDependency As SqlCacheDependency = _
          New SqlCacheDependency("Northwind", "Customers")
       Cache.Insert("firmCustomers", myCustomers, myDependency)
 
       Label1.Text = "Produced from database."
    Else
       Label1.Text = "Produced from Cache object."      
    End If
 
    GridView1.DataSource = myCustomers
    GridView1.DataBind()
End Sub
</script>
 
<html>
<head runat="server">
    <title>Sql Cache Invalidation</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Label ID="Label1" Runat="server"></asp:Label><br />
        <br />
        <asp:GridView ID="GridView1" Runat="server"></asp:GridView>    
    </div>
    </form>
</body>
</html>

C#

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
 
<script runat="server">
    protected void Page_Load(object sender, System.EventArgs e)
    {
        DataSet myCustomers;
        myCustomers = (DataSet)Cache["firmCustomers"];
 
        if (myCustomers == null)
        {
            SqlConnection conn = new SqlConnection(
              ConfigurationManager.ConnectionStrings[
                "AppConnectionString1"].ConnectionString);
            SqlDataAdapter da = new SqlDataAdapter("Select * from Customers", conn);
 
            myCustomers = new DataSet();
            da.Fill(myCustomers);
 
            SqlCacheDependency myDependency = new SqlCacheDependency(
              "Northwind", "Customers");
            Cache.Insert("firmCustomers", myCustomers, myDependency);
 
            Label1.Text = "Produced from database.";
        }
        else
        {
            Label1.Text = "Produced from Cache object.";
        }
 
        GridView1.DataSource = myCustomers;
        GridView1.DataBind();
    }
</script>

In this example, the SqlCacheDependency class associated itself to the Customers table in the Northwind database as before. This time, however, you use the Cache object to insert the retrieved data set along with a reference to the SqlCacheDependency object. The Insert() method of the Cache class is constructed as follows:

Cache.Insert(String key, Object value,
System.Web.Caching.CacheDependency dependencies)

You can also insert more information about the dependency using the following construct:

Cache.Insert(String key, Object value,
System.Web.Caching.CacheDependency dependencies
Date absoluteExpiration, System.TimeSpan slidingExpiration)

And finally:

Cache.Insert(String key, Object value,
System.Web.Caching.CacheDependency dependencies,
Date absoluteExpiration, System.TimeSpan slidingExpiration,
System.Web.Caching.CacheItemPriority priority,
System.Web.Caching.CacheItemRemovedCallback onRemoveCallback)

The SQL Server cache dependency created comes into action and does the same polling as it would have done otherwise. If any of the data in the Customers table has changed, the SqlCacheDependency class invalidates what is stored in the cache. When the next request is made, the Cache["firmCustomers"] is found to be empty and a new request is made to SQL Server. The Cache object again repopulates the cache with the new results generated.

Figure 22-8 shows the results generated when the ASP.NET page from Listing 22-12 is called for the first time.

FIGURE 22-8

image

Because this is the first time that the page is generated, nothing is in the cache. The Cache object is then placed in the resultset along with the association to the SQL Server cache dependency. Figure 22-9 shows the result of the second request. Notice that the HTML table is identical because it was generated from the identical DataSet, but the first line of the page has changed to indicate that this output was produced from cache.

FIGURE 22-9

image

On the second request, the data set is already contained within the cache; therefore, it is retrievable. You are not required to hit SQL Server to get the full results again. If any of the information has changed within SQL Server itself, however, the Cache object returns nothing; a new resultset is retrieved.

SUMMARY

ASP.NET provides several built-in mechanisms for caching. Most of these features can be achieved using attributes, so no actual coding is required. It is important that you understand the different types of caching, including partial page caching and post-cache substitution.

When working with a database, SQL Server cache invalidation is an outstanding feature of ASP.NET that enables you to invalidate items stored in the cache when underlying changes occur to the data in the tables being monitored. Post-cache substitution fills in an important gap in ASP.NET’s technology, enabling you to have both the best highly dynamic content and a high-performance website with caching.

When you are monitoring changes to the database, you can configure these procedures easily in the web.config file, or you can work programmatically with cache invalidation directly in your code. These changes are possible because the CacheDependency object has been unsealed. You can now inherit from this object and create your own cache dependencies. The SQL Server cache-invalidation process is the first example of this capability.