Chapter 23. Caching

ASP.NET applications are a bit of a contradiction. On the one hand, because they're hosted over the Internet, they have unique requirements—namely, they need to be able to serve hundreds of clients as easily and quickly as they deal with a single user. On the other hand, ASP.NET includes some remarkable tricks that let you design and code a web application in the same way you program a desktop application. These tricks are useful, but they can lead developers into trouble. The problem is that ASP.NET makes it easy to forget you're creating a web application—so easy, that you might introduce programming practices that will slow or cripple your application when it's used by a large number of users in the real world.

Fortunately, a middle ground exists. You can use the incredible timesaving features such as view state, web controls, and session state that you've spent the last 20-odd chapters learning about and still create a robust web application. But to finish the job properly, you'll need to invest a little extra time to profile and optimize your website's performance. One of the easiest ways to improve perform is to use caching, a technique that temporarily stores valuable information in server memory so it can be reused. Unlike the other types of state management you learned about in Chapter 8, caching includes some built-in features that ensure good performance.

Understanding Caching

ASP.NET has taken some dramatic steps forward with caching. Many developers who first learn about caching see it as a bit of a frill, but nothing could be further from the truth. Used intelligently, caching can provide a twofold, threefold, or even tenfold performance improvement by retaining important data for just a short period of time.

Caching is often used to store information that's retrieved from a database. This makes sense—after all, retrieving information from a database takes time. With careful optimization, you can reduce the time and lessen the burden imposed on the database to a certain extent, but you can never eliminate it. But with a system that uses caching, some data requests won't require a database connection and a query. Instead, they'll retrieve the information directly from server memory, which is a much faster proposition.

Of course, storing information in memory isn't always a good idea. Server memory is a limited resource; if you try to store too much, some of that information will be paged to disk, potentially slowing down the entire system. That's why ASP.NET caching is self-limiting. When you store information in a cache, you can expect to find it there on a future request, most of the time. However, the lifetime of that information is at the discretion of the server. If the cache becomes full or other applications consume a large amount of memory, data will be selectively evicted from the cache, ensuring that the application continues to perform well. It's this self-sufficiency that makes caching so powerful (and would make it extremely complicated to implement on your own).

When to Use Caching

The secret to getting the most out of caching is choosing the right time to use it. A good caching strategy identifies the most frequently used pieces of data that are the most time-consuming to create and stores them. If you store too much information, you risk filling up the cache with relatively unimportant data and forcing out the content you really want to keep.

Here are two caching guidelines to keep you on the right track:

Cache data (or web pages) that are expensive:

In other words, cache information that's time-consuming to create. The results of a database query or contents of a file are good examples. Not only does it take time to open a database connection or a file, but it can also delay or lock out other users who are trying to do the same thing at the same time.

Cache data (or web pages) that are used frequently:

There's no point setting aside memory for information that's never going to be needed again. For example, you might choose not to cache product detail pages, because there are hundreds of different products, each with its own page. But it makes more sense to cache the list of product categories, because that information will be reused to serve many different requests.

If you keep these two rules in mind, you can get two benefits from caching at once—you can improve both performance and scalability.

Performance is a measure of how quickly a web page works for a single user. Caching improves performance, because it bypasses bottlenecks like the database. As a result, web pages are processed and sent back to the client more quickly.

Scalability measures how the performance of your web application degrades as more and more people use it at the same time. Caching improves scalability, because it allows you to reuse the same information for requests that happen in quick succession. With caching, more and more people can use your website, but the number of trips to the database won't change very much. Therefore, the overall burden on the system will stay relatively constant, as shown in Figure 23-1.

The effect of good caching

Figure 23.1. The effect of good caching

Many optimization techniques enhance scalability at the cost of performance, or vice versa. Caching is remarkable because it gives you the best of both worlds.

Caching in ASP.NET

ASP.NET really has two types of caching. Your applications can and should use both types, because they complement each other:

  • Output caching: This is the simplest type of caching. It stores a copy of the final rendered HTML page that is sent to the client. The next client that submits a request for this page doesn't actually run the page. Instead, the final HTML output is sent automatically. The time that would have been required to run the page and its code is completely reclaimed.

  • Data caching: This is carried out manually in your code. To use data caching, you store important pieces of information that are time-consuming to reconstruct (such as a DataSet retrieved from a database) in the cache. Other pages can check for the existence of this information and use it, thereby bypassing the steps ordinarily required to retrieve it. Data caching is conceptually the same as using application state, but it's much more server-friendly because items will be removed from the cache automatically when it grows too large and performance could be affected. Items can also be set to expire automatically.

Also, two specialized types of caching build on these models:

  • Fragment caching: This is a specialized type of output caching—instead of caching the HTML for the whole page, it allows you to cache the HTML for a portion of it. Fragment caching works by storing the rendered HTML output of a user control on a page. The next time the page is executed, the same page events fire (and so your page code will still run), but the code for the appropriate user control isn't executed.

  • Data source caching: This is the caching that's built into the data source controls, including the SqlDataSource and ObjectDataSource. Technically, data source caching uses data caching. The difference is that you don't need to handle the process explicitly. Instead, you simply configure the appropriate properties, and the data source control manages the caching storage and retrieval.

In this chapter, you'll learn about all these types of caching. You'll begin by learning the basics of output caching, fragment caching, and data caching. Next, you'll examine the caching in the data source controls. Finally, you'll explore one of ASP.NET's hottest features—linking cached items to tables in a database with SQL cache dependencies.

Output Caching

With output caching, the final rendered HTML of the page is cached. When the same page is requested again, the control objects are not created, the page life cycle doesn't start, and none of your code executes. Instead, the cached HTML is served. Clearly, output caching gets the theoretical maximum performance increase, because all the overhead of your code is sidestepped.

To see output caching in action, you can create a simple page that displays the current time of day. Figure 23-2 shows this page.

Displaying the time a page is served

Figure 23.2. Displaying the time a page is served

The code for this task is elementary:

Public Partial Class OutputCaching
    Inherits System.Web.UI.Page

    Protected Sub Page_Load(ByVal sender As Object, _
      ByVal e As System.EventArgs) Handles Me.Load
        lblDate.Text = "The time is now:<br />"
        lblDate.Text &= DateTime.Now.ToString()
    End Sub

End Class

You can cache an ASP.NET page in two ways. The most common approach is to insert the OutputCache directive at the top of your .aspx file, just below the Page directive, as shown here:

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

The Duration attribute instructs ASP.NET to cache the page for 20 seconds. The VaryByParam attribute is also required—but you'll learn about its effect later in the "Caching and the Query String" section.

When you run the test page, you'll discover some interesting behavior. The first time you access the page, you will see the current time displayed. If you refresh the page a short time later, however, the page will not be updated. Instead, ASP.NET will automatically send the cached HTML output to you, until it expires in 20 seconds. If ASP.NET receives a request after the cached page has expired, ASP.NET will run the page code again, generate a new cached copy of the HTML output, and use that for the next 20 seconds.

Twenty seconds may seem like a trivial amount of time, but in a high-volume site, it can make a dramatic difference. For example, you might cache a page that provides a list of products from a catalog. By caching the page for 20 seconds, you limit database access for this page to three operations per minute. Without caching, the page will try to connect to the database once for each client and could easily make dozens of requests in the course of 20 seconds.

Of course, just because you request that a page should be stored for 20 seconds doesn't mean that it actually will be. The page could be evicted from the cache early if the system finds that memory is becoming scarce. This allows you to use caching freely, without worrying too much about hampering your application by using up vital memory.

Tip

When you recompile a cached page, ASP.NET will automatically remove the page from the cache. This prevents problems where a page isn't properly updated, because the older, cached version is being used. However, you might still want to disable caching while testing your application. Otherwise, you may have trouble using variable watches, breakpoints, and other debugging techniques, because your code will not be executed if a cached copy of the page is available.

Caching on the Client Side

Another option is to cache the page exclusively on the client side. In this case, the browser stores a copy and will automatically use this page if the client browses back to the page or retypes the page's URL. However, if the user clicks the Refresh button, the cached copy will be abandoned, and the page will be rerequested from the server, which will run the appropriate page code once again. You can cache a page on the client side using the Location attribute in the OutputCache directive, which specifies a value from the System.Web.UI.OutputCacheLocation enumeration, as shown here:

<%@ OutputCache Duration="20" VaryByParam="None" Location="Client" %>

Client-side caching is less common than server-side caching. Because the page is still re-created for every separate user, it won't reduce code execution or database access nearly as dramatically as server-side caching (which shares a single cached copy among all users). However, client-side caching can be a useful technique if your cached page uses some sort of personalized data. For example, imagine a page that displays a user-specific greeting. In this situation, server-side caching isn't ideal. The problem is that the page will be created just once and reused for all clients, ensuring that most will receive the wrong greeting. In this situation, you can either use fragment caching to cache the generic portion of the page or use client-side caching to store a user-specific version on each client's computer.

Caching and the Query String

One of the main considerations in caching is deciding when a page can be reused and when information must be accurate up to the latest second. Developers, with their love of instant gratification (and lack of patience), generally tend to overemphasize the importance of real-time information. You can usually use caching to efficiently reuse slightly stale data without a problem and with a considerable performance improvement.

Of course, sometimes information needs to be dynamic. One example is if the page uses information from the current user's session to tailor the user interface. In this case, full page caching just isn't appropriate, because the same page can't be reused for requests from different users (although fragment caching may help). Another example is if the page is receiving information from another page through the query string. In this case, the page is too dynamic to cache—or is it?

The current example sets the VaryByParam attribute on the OutputCache directive to None, which effectively tells ASP.NET that you need to store only one copy of the cached page, which is suitable for all scenarios. If the request for this page adds query string arguments to the URL, it makes no difference—ASP.NET will always reuse the same output until it expires. You can test this by adding a query string parameter manually in the browser window. For example, try tacking ?a=b on to the end of your URL. The cached output is still used.

Based on this experiment, you might assume that output caching isn't suitable for pages that use query string arguments. But ASP.NET actually provides another option. You can set the VaryByParam attribute to * to indicate that the page uses the query string and to instruct ASP.NET to cache separate copies of the page for different query string arguments:

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

Now when you request the page with additional query string information, ASP.NET will examine the query string. If the string matches a previous request and a cached copy of that page exists, it will be reused. Otherwise, a new copy of the page will be created and cached separately.

To get a better idea of how this process works, consider the following series of requests:

  1. You request a page without any query string parameter and receive page copy A.

  2. You request the page with the parameter ProductID=1. You receive page copy B.

  3. Another user requests the page with the parameter ProductID=2. That user receives copy C.

  4. Another user requests the page with ProductID=1. If the cached output B has not expired, it's sent to the user.

  5. The user then requests the page with no query string parameters. If copy A has not expired, it's sent from the cache.

You can try this on your own, although you might want to lengthen the amount of time that the cached page is retained to make it easier to test.

Note

Output caching works well if the pages depend only on server-side data (for example, the data in a database) and the data in the query string. However, output caching doesn't work if the page output depends on user-specific information such as session data or cookies, because there's no way to vary caching based on these criteria. Output caching also won't work with dynamic pages that change their content in response to control events. In these situations, use fragment caching instead to cache a portion of the page, or use data caching to cache specific information. Both techniques are discussed later in this chapter.

Caching with Specific Query String Parameters

Setting VaryByParam to the wildcard asterisk (*) is unnecessarily vague. It's usually better to specifically identify an important query string variable by name. Here's an example:

<%@ OutputCache Duration="20" VaryByParam="ProductID" %>

In this case, ASP.NET will examine the query string, looking for the ProductID parameter. Requests with different ProductID parameters will be cached separately, but all other parameters will be ignored. This is particularly useful if the page may be passed additional query string information that it doesn't use. ASP.NET has no way to distinguish the "important" query string parameters without your help.

You can specify several parameters as long as you separate them with semicolons:

<%@ OutputCache Duration="20" VaryByParam="ProductID;CurrencyType" %>

In this case, ASP.NET will cache separate versions, provided the query string differs by ProductID or CurrencyType.

A Multiple Caching Example

The following example uses two web pages to demonstrate how multiple versions of a web page can be cached separately. The first page, QueryStringSender.aspx, isn't cached. It provides three buttons, as shown in Figure 23-3.

Three page options

Figure 23.3. Three page options

A single event handler handles the Click event for all three buttons. The event handler navigates to the QueryStringRecipient.aspx page and adds a Version parameter to the query string to indicate which button was clicked—cmdNormal, cmdLarge, or cmdSmall.

Protected Sub cmdVersion_Click(ByVal sender As Object, _
  ByVal e As System.EventArgs) _
  Handles cmdNormal.Click, cmdLarge.Click, cmdSmall.Click

    Response.Redirect("QueryStringRecipient.aspx" & _
      "?Version=" & CType(sender, Control).ID)
End Sub

The QueryStringRecipient.aspx destination page displays the familiar date message. The page uses an OutputCache directive that looks for a single query string parameter (named Version):

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

In other words, this has three separately maintained HTML outputs: one where Version equals cmdSmall, one where Version equals cmdLarge, and one where Version equals cmdNormal.

Although it isn't necessary for this example, the Page.Load event handler in QueryRecipient.aspx tailors the page by changing the font size of the label accordingly. This makes it easy to distinguish the three versions of the page and verify that the caching is working as expected.

Protected Sub Page_Load(ByVal sender As Object, _
  ByVal e As System.EventArgs) Handles Me.Load

    lblDate.Text = "The time is now:<br />" & DateTime.Now.ToString()

    Select Case Request.QueryString("Version")
        Case "cmdLarge"
            lblDate.Font.Size = FontUnit.XLarge
        Case "cmdNormal"
            lblDate.Font.Size = FontUnit.Large
        Case "cmdSmall"
            lblDate.Font.Size = FontUnit.Small
    End Select
End Sub

Figure 23-4 shows one of the cached outputs for this page.

One page with three cached outputs

Figure 23.4. One page with three cached outputs

Fragment Caching

In some cases, you may find that you can't cache an entire page, but you would still like to cache a portion that is expensive to create and doesn't vary frequently (like a list of categories in a product catalog). One way to implement this sort of scenario is to use data caching to store just the underlying information used for the page. You'll examine this technique in the next section. Another option is to use fragment caching.

To implement fragment caching, you need to create a user control for the portion of the page you want to cache. You can then add the OutputCache directive to the user control. The result is that the page will not be cached, but the user control will.

Fragment caching is conceptually the same as page caching. It has only one catch—if your page retrieves a cached version of a user control, it cannot interact with it in code. For example, if your user control provides properties, your web page code cannot modify or access these properties. When the cached version of the user control is used, a block of HTML is simply inserted into the page. The corresponding user control object is not available. To avoid potential problems in your code, don't attempt to interact with it in your code or check that it's not a null reference (Nothing) before you do.

Cache Profiles

One problem with output caching is that you need to embed the instruction into the page—either in the .aspx markup portion or in the code of the class. Although the first option (using the OutputCache directive) is relatively clean, it still produces management problems if you create dozens of cached pages. If you want to change the caching for all these pages (for example, moving the caching duration from 30 to 60 seconds), you need to modify every page. ASP.NET also needs to recompile these pages.

ASP.NET includes a feature called cache profiles that makes it easy to apply the same caching settings to a group of pages. With cache profiles, you define a group of caching settings in the web.config file, associate a name with these settings, and then apply these settings to multiple pages using the name. That way, you have the freedom to modify all the linked pages at once simply by changing the caching profile in the web.config file.

To define a cache profile, you use the <add> tag in the <outputCacheProfiles> section, as follows. You assign a name and a duration.

<configuration>
  <system.web>
    <caching>
      <outputCacheSettings>
        <outputCacheProfiles>
          <add name="ProductItemCacheProfile" duration="60" />
        </outputCacheProfiles>
      </outputCacheSettings>
    </caching>
  ...
  </system.web>
</configuration>

You can now use this profile in a page through the CacheProfile attribute:

<%@ OutputCache CacheProfile="ProductItemCacheProfile" VaryByParam="None" %>

Interestingly, if you want to apply other caching details, such as the VaryByParam behavior, you can set it either as an attribute in the OutputCache directive or as an attribute of the <add> tag for the profile. Just make sure you start with a lowercase letter if you use the <add> tag, because the property names are camel case, as are all configuration settings, and case is important in XML.

Data Caching

Data caching is the most flexible type of caching, but it also forces you to take specific additional steps in your code to implement it. The basic principle of data caching is that you add items that are expensive to create to a built-in collection object called Cache. Cache is a property of the Page class, and it returns an instance of the System.Web.Caching.Cache class. This object works much like the Application object you saw in Chapter 8. It's globally available to all requests from all clients in the application. But it has three key differences:

The Cache object is thread-safe:

This means you don't need to explicitly lock or unlock the Cache object before adding or removing an item. However, the objects in the Cache object will still need to be thread-safe themselves. For example, if you create a custom business object, more than one client could try to use that object at once, which could lead to invalid data. You can code around this limitation in various ways—one easy approach that you'll see in this chapter is to just make a duplicate copy of the object if you need to work with it in a web page.

Items in the Cache object are removed automatically:

ASP.NET will remove an item if it expires, if one of the objects or files it depends on changes, or if the server becomes low on memory. This means you can freely use the cache without worrying about wasting valuable server memory, because ASP.NET will remove items as needed. But because items in the cache can be removed, you always need to check whether a cache object exists before you attempt to use it. Otherwise, you could generate a null reference exception.

Items in the cache support dependencies:

You can link a cached object to a file, a database table, or another type of resource. If this resource changes, your cached object is automatically deemed invalid and released.

Adding Items to the Cache

You can insert an object into the cache in several ways. You can simply assign it to a new key name (as you would with the Session or Application collection):

Cache("KeyName") = objectToCache

However, this approach is generally discouraged because it doesn't give you any control over the amount of time the object will be retained in the cache. A better approach is to use the Insert() method.

The Insert() method has four overloaded versions. The most useful one requires five parameters:

Cache.Insert(key, item, dependencies, absoluteExpiration, slidingExpiration)

Table 23-1 describes these parameters.

Table 23.1. Cache.Insert() Parameters

Parameter

Description

key

A string that assigns a name to this cached item in the collection and allows you to look it up later.

item

The actual object you want to cache.

dependencies

A CacheDependency object that allows you to create a dependency for this item in the cache. If you don't want to create a dependent item, just specify a null reference (Nothing) for this parameter.

absoluteExpiration

A DateTime object representing the date and time at which the item will be removed from the cache.

slidingExpiration

A TimeSpan object representing how long ASP.NET will wait between requests before removing a cached item. For example, if this value is 20 minutes, ASP.NET will evict the item if it isn't used by any code for a 20-minute period.

Typically, you won't use all of these parameters at once. Cache dependencies, for example, are a special tool you'll consider a little later in the "Caching with Dependencies" section. Also, you cannot set both a sliding expiration and an absolute expiration policy at the same time. If you want to use an absolute expiration, set the slidingExpiration parameter to TimeSpan.Zero:

Cache.Insert("MyItem", obj, Nothing, _
  DateTime.Now.AddMinutes(60), TimeSpan.Zero)

Absolute expirations are best when you know the information in a given item can be considered valid only for a specific amount of time (such as a stock chart or a weather report). Sliding expiration, on the other hand, is more useful when you know that a cached item will always remain valid (such as with historical data or a product catalog) but should still be allowed to expire if it isn't being used. To set a sliding expiration policy, set the absoluteExpiration parameter to DateTime.MaxValue, as shown here:

Cache.Insert("MyItem", obj, Nothing, _
  DateTime.MaxValue, TimeSpan.FromMinutes(10))

Tip

Don't be afraid to cache for a long time. For example, Microsoft's case studies often store cached data for 100 minutes or more.

A Simple Cache Test

The following page presents a simple caching test. An item is cached for 30 seconds and reused for requests in that time. The page code always runs (because the page itself isn't cached), checks the cache, and retrieves or constructs the item as needed. It also reports whether the item was found in the cache.

Public Partial Class SimpleDataCache
    Inherits System.Web.UI.Page

     Protected Sub Page_Load(ByVal sender As Object, _
       ByVal e As System.EventArgs) Handles Me.Load

        If Me.IsPostBack Then
            lblInfo.Text &= "Page posted back.<br />"
        Else
            lblInfo.Text &= "Page created.<br />"
        End If

        If Cache("TestItem") Is Nothing Then
            lblInfo.Text &= "Creating TestItem...<br />"
            Dim testItem As DateTime = DateTime.Now

            lblInfo.Text &= "Storing TestItem in cache "
            lblInfo.Text &= "for 30 seconds.<br />"
            Cache.Insert("TestItem", testItem, Nothing, _
              DateTime.Now.AddSeconds(30), TimeSpan.Zero)
        Else
            lblInfo.Text &= "Retrieving TestItem...<br />"
            Dim testItem As DateTime = CType(Cache("TestItem"), DateTime)
            lblInfo.Text &= "TestItem is '" & testItem.ToString()
            lblInfo.Text &= "'<br />"
        End If

        lblInfo.Text &= "<br />"
    End Sub

End Class

Figure 23-5 shows the result after the page has been loaded and posted back several times in the 30-second period.

A simple cache test

Figure 23.5. A simple cache test

Caching to Provide Multiple Views

The next example shows a more interesting demonstration of caching, which includes retrieving information from a database and storing it in a DataSet. This information is then displayed in a GridView. However, the output for the web page can't be efficiently cached because the user is given the chance to customize the display by hiding any combination of columns. Note that even with just ten columns, you can construct more than a thousand different possible views by hiding and showing various columns. These are far too many columns for successful output caching!

Figure 23-6 shows the page. To hide a column, you simply click the corresponding check box.

Filtering information from a cached DataSet

Figure 23.6. Filtering information from a cached DataSet

Instead of attempting to use output caching, this page caches the DataSet object that holds the full information. This DataSet is constructed in the dedicated RetrieveData() function shown here. (In order to use this code as written, you must import the System.Data, System.Data.SqlClient, and System.Web.Configuration namespaces in the web page.)

Private Function RetrieveData() As DataSet
    Dim connectionString As String = _
      WebConfigurationManager.ConnectionStrings("Northwind").ConnectionString
    Dim SQLSelect As String = "SELECT * FROM Customers"
    Dim con As New SqlConnection(connectionString)
    Dim cmd As New SqlCommand(SQLSelect, con)
    Dim adapter As New SqlDataAdapter(cmd)
    Dim ds As New DataSet()

    Try
        con.Open()
        adapter.Fill(ds, "Customers")
    Finally
        con.Close()
End Try

    Return ds
End Function

The RetrieveData() method handles the work of contacting the database and creating the DataSet. You need another level of code that checks to see whether the DataSet is in the cache and adds it when needed. The best way to write this code is to add another method. This method is called GetDataSet().

The GetDataSet() method attempts to retrieve the DataSet from the cache. If it cannot retrieve the DataSet, it calls the RetrieveData() method and then adds the DataSet to the cache. It also reports on the page whether the DataSet was retrieved from the cache or generated manually.

Private Function GetDataSet() As DataSet
    Dim ds As DataSet = CType(Cache("Customers"), DataSet)

    ' Contact the database if necessary.
    If ds Is Nothing Then
        ds = RetrieveData()
        Cache.Insert("Customers", ds, Nothing, _
          DateTime.MaxValue, TimeSpan.FromMinutes(2))
        lblCacheStatus.Text = "Created and added to cache."
    Else
        lblCacheStatus.Text = "Retrieved from cache."
    End If

    Return ds
End Function

The advantage of this approach is that you can call GetDataSet() in any event handler in your web page code to get the DataSet when you need it. You don't need to worry about checking the cache first and calling RetrieveDataSet() when needed—instead, GetDataSet() handles the whole process transparently.

Tip

This two-step approach (with one method that creates the data object you need and another that manages cache) is a common, time-tested design. It's always a good strategy to ensure that you deal with the cache consistently. If you want to use the same cached object in multiple web pages, you can take this design one step further by moving the GetDataSet() and RetrieveDataSet() methods into a separate class. In this case, you'd probably make the RetrieveDataSet() method private and the GetDataSet() method public—that way, web pages can request the DataSet whenever they need it but don't determine when to contact the database or whether you've implemented caching.

When the page is first loaded, it calls GetDataSet() to retrieve the DataSet. It then gets the DataTable with the customer records and binds the DataTable.Columns collection to a CheckBoxList control named chkColumns:

Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) _
  Handles Me.Load
    If Not Me.IsPostBack Then
        Dim ds As DataSet = GetDataSet()
        chkColumns.DataSource = ds.Tables("Customers").Columns
        chkColumns.DataTextField = "ColumnName"
        chkColumns.DataBind()
    End If
End Sub

As you learned in Chapter 14, the DataTable.Columns collection holds one DataColumn object for each column in the DataTable. Each DataColumn specifies details such as data type and column name. In this example, the DataColumn.ColumnName property is used to display the name of each column (as configured by the DataTextField property of the CheckBoxList control).

Every time the Filter button is clicked, the page calls GetDataSet() to retrieve the DataSet. To provide the configurable grid, the code loops through the DataTable, removing all the columns that the user has selected to hide. The code then binds the data by calling GridView.DataBind().

The full code for the Filter button is as follows:

Protected Sub cmdFilter_Click(ByVal sender As Object, _
  ByVal e As System.EventArgs) Handles cmdFilter.Click

    Dim ds As DataSet = GetDataSet()

    ' Copy the DataSet so you can remove columns without
    ' changing the cached item.
    ds = ds.Copy()

    For Each item As ListItem in chkColumns.Items
        If item.Selected Then
            ds.Tables(0).Columns.Remove(item.Text)
        End If
    Next

    gridCustomers.DataSource = ds.Tables(0)
    gridCustomers.DataBind()

End Sub

This example demonstrates an important fact about the cache. When you retrieve an item, you actually retrieve a reference to the cached object. If you modify that object, you're actually modifying the cached item as well. For the page to be able to delete columns without affecting the cached copy of the DataSet, the code needs to create a duplicate copy before performing the operations using the DataSet.Copy() method.

Caching with the Data Source Controls

The SqlDataSource (Chapter 15) and ObjectDataSource (Chapter 22) support built-in data caching. Using caching with these controls is highly recommended, because they can be more inefficient than handwritten data access code. For example, they query the data source once for every bound control, so if you have three controls bound to the same data source, three separate queries are executed against the database just before the page is rendered. Even a little caching can reduce this overhead dramatically.

To support caching, the SqlDataSource and ObjectDataSource controls use the same properties, which are listed in Table 23-2.

Table 23.2. Caching Properties of the Data Source Controls

Property

Description

EnableCaching

If True, switches caching on. It's False by default.

CacheExpirationPolicy

Uses a value from the DataSourceCacheExpiry enumeration—Absolute for absolute expiration (which times out after a fixed interval of time), or Sliding for sliding expiration (which resets the time window every time the data object is retrieved from the cache).

CacheDuration

Determines the number of seconds to cache the data object. If you are using sliding expiration, the time limit is reset every time the object is retrieved from the cache. The default value, 0, keeps cached items perpetually.

CacheKeyDependency and SqlCacheDependency

Allow you to make a cached item dependent on another item in the data cache (CacheKeyDependency) or on a table in your database (SqlCacheDependency). Dependencies are discussed in the "Cache Dependencies" section.

Caching with SqlDataSource

When you enable caching for the SqlDataSource control, you cache the results of the SelectCommand. However, if you create a select query that takes parameters, the SqlDataSource will cache a separate result for every set of parameter values.

For example, imagine you create a page that allows you to view employees by city. The user selects the desired city from a list box, and you use a SqlDataSource control to fill in the matching employee records in a grid (see Figure 23-7).

Retrieving data from the cache

Figure 23.7. Retrieving data from the cache

There are two SqlDataSource controls at work in this example. The first SqlDataSource gets the list of cities for the drop-down list. These results don't change often, and so they are cached for one hour (3600 seconds):

<asp:SqlDataSource ID="sourceEmployeeCities" runat="server"
 ProviderName="System.Data.SqlClient"
 EnableCaching="True" CacheDuration="3600"
 ConnectionString="<%$ ConnectionStrings:Northwind %>"
 SelectCommand="SELECT DISTINCT City FROM Employees">
</asp:SqlDataSource>

<asp:DropDownList ID="lstCities" runat="server"
 DataSourceID="sourceEmployeeCities"
 DataTextField="City" AutoPostBack="True">
</asp:DropDownList>

The second SqlDataSource gets the employees in the currently selected city. These results are cached for 600 seconds and bound to a GridView:

<asp:SqlDataSource ID="sourceEmployees" runat="server"
 ProviderName="System.Data.SqlClient"
 EnableCaching="True" CacheDuration="600"
 ConnectionString="<%$ ConnectionStrings:Northwind %>"
 SelectCommand="SELECT EmployeeID, FirstName, LastName, Title, City
FROM Employees WHERE City=@City">
  <SelectParameters>
    <asp:ControlParameter ControlID="lstCities" Name="City"
     PropertyName="SelectedValue" />
  </SelectParameters>
</asp:SqlDataSource>

<asp:GridView ID="GridView1" runat="server"
 DataSourceID="sourceEmployees" ... >
  ...
</asp:GridView>

This SqlDataSource is a bit more sophisticated because it uses a parameter. Each time you select a city, a separate query is performed to get just the matching employees in that city. The query is used to fill a DataSet, which is then cached for up to ten minutes (600 seconds). If you select a different city, the process repeats, and the new DataSet is cached separately. However, if you pick a city that you or another user has already requested, the appropriate DataSet is fetched from the cache (provided it hasn't yet expired).

Thus, this single SqlDataSource can result in a surprisingly large number of cache entries. If there are 20 different cities in your list (and therefore 20 different possible parameter values), you can end up with as many as 20 different DataSet objects in the cache at once.

Note

SqlDataSource caching works only when the DataSourceMode property is set to DataSet (the default). It doesn't work when the mode is set to DataReader, because the DataReader object maintains a live connection to the database and can't be efficiently cached. If you try to use caching with the DataReader mode, you'll receive a NotSupportedException when you bind the grid.

On the other hand, if the parameter values are all used with similar frequency, this approach isn't as suitable. One of the problems it imposes is that when the items in the cache expire, you'll need multiple database queries to repopulate the cache (one for each combination of parameter values), which isn't as efficient as getting the combined results with a single query.

If you fall into the second situation, you can change the SqlDataSource so it retrieves a DataSet with all the employee records and caches that. The SqlDataSource can then extract just the records it needs to satisfy each request from the DataSet. This way, a single DataSet with all the records is cached, which can satisfy any parameter value.

To use this technique, you need to rewrite your SqlDataSource to use filtering. First, the select query should return all the rows and not use any SelectParameters:

<asp:SqlDataSource ID="sourceEmployees" runat="server"
 SelectCommand=
"SELECT EmployeeID, FirstName, LastName, Title, City FROM Employees"
 ...>
</asp:SqlDataSource>

Second, you need to define the filter expression. This is the portion that goes in the WHERE clause of a typical SQL query. However, this has a catch—if you're supplying the filter value from another source (such as a control), you need to define one or more placeholders, using the syntax {0} for the first placeholder, {1} for the second, and so on. You then supply the filter values using the <FilterParameters> section, in much the same way you supplied the select parameters in the first version.

Here's the completed SqlDataSource tag:

<asp:SqlDataSource ID="sourceEmployees" runat="server"
 ProviderName="System.Data.SqlClient"
 ConnectionString="<%$ ConnectionStrings:Northwind %>"
 SelectCommand=
"SELECT EmployeeID, FirstName, LastName, Title, City FROM Employees"
 FilterExpression="City='{0}'" EnableCaching="True">
  <FilterParameters>
    <asp:ControlParameter ControlID="lstCities" Name="City"
     PropertyName="SelectedValue" />
  </FilterParameters>
</asp:SqlDataSource>

Tip

Don't use filtering unless you are using caching. If you use filtering without caching, you are essentially retrieving the full result set each time and then extracting a portion of its records. This combines the worst of both worlds—you have to repeat the query with each postback, and you fetch far more data than you need each time.

Caching with ObjectDataSource

The ObjectDataSource caching works on the data object returned from the SelectMethod. If you are using a parameterized query, the ObjectDataSource distinguishes between requests with different parameter values and caches them separately. Unfortunately, the ObjectDataSource caching has a significant limitation—it works only when the select method returns a DataSet or a DataTable. If you return any other type of object, you'll receive a NotSupportedException.

This limitation is unfortunate, because there's no technical reason you can't cache custom objects in the data cache. If you want this feature, you'll need to implement data caching inside your method by manually inserting your objects into the data cache and retrieving them later. In fact, caching inside your method can be more effective, because you have the ability to share the same cached object in multiple methods. For example, you could cache a DataTable with a list of products and categories and use that cached item in both the GetProductCategories() and GetProductsByCategory() methods.

Tip

The only consideration you should keep in mind is to make sure you use unique cache key names that aren't likely to collide with the names of cached items that the page might use. This isn't a problem when using the built-in data source caching, because it always stores its information in a hidden slot in the cache.

If your custom class returns a DataSet or DataTable and you do decide to use the built-in ObjectDataSource caching, you can also use filtering as discussed with the SqlDataSource control. Just instruct your ObjectDataSource to call a method that gets the full set of data, and set the FilterExpression to retrieve just those items that match the current view.

Caching with Dependencies

As time passes, the information in your data source may change. If your code uses caching, you may remain unaware of the changes and continue using out-of-date information from the cache. To help mitigate this problem, ASP.NET supports cache dependencies. Cache dependencies allow you to make a cached item dependent on another resource, so that when that resource changes, the cached item is removed automatically.

ASP.NET includes three types of dependencies:

  • Dependencies on files or folders

  • Dependencies on other cached items

  • Dependencies on a database query

You'll see all these types of dependencies in the following section.

File Dependencies

To use a cache dependency, you need to create a CacheDependency object. You then need to supply the CacheDependency object when you add the dependent cached item.

For example, the following code creates a CacheDependency that depends on an XML file named ProductList.xml. When the XML file is changed, the CacheDependency will be invalidated, and the dependent cached item will be evicted from the cache immediately.

' Create a dependency for the ProductList.xml file.
Dim prodDependency As New CacheDependency( _
  Server.MapPath("ProductList.xml"))

' Add a cache item that will be dependent on this file.
Cache.Insert("ProductInfo", prodInfo, prodDependency)

Monitoring begins as soon as the CacheDependency object is created. If the XML file changes before you have added the dependent item to the cache, the item will expire immediately as soon as it's added.

Figure 23-8 shows a simple test page that is included with the samples for this chapter. It sets up a dependency, modifies the file, and allows you to verify that the cached item has been dropped from the cache.

Testing cache dependencies

Figure 23.8. Testing cache dependencies

The CacheDependency object provides several constructors. You've already seen how it can make a dependency based on a file by using the file name constructor. You can also specify a directory that needs to be monitored for changes, or you can use a constructor that accepts an array of strings that represent multiple files and directories.

Cache Item Dependencies

The CacheDependency class provides another constructor that accepts an array of file names and an array of cache keys. Using the array of cache keys, you can create a cached item that's dependent on another item in the cache. (If you don't want to use file dependencies at all, you simply supply a null reference (Nothing) for the first parameter.)

Here's an example that makes one item dependent on another cached item, without using file dependencies:

Cache("Key1") = "Cache Item 1"

' Make Cache("Key2") dependent on Cache("Key1").
Dim dependencyKey(0) As String
dependencyKey(0) = "Key1"
Dim dependency As New CacheDependency(Nothing, dependencyKey)

Cache.Insert("Key2", "Cache Item 2", dependency)

Now, when the first cached item changes or is removed from the cache, the second cached item will automatically be dropped from the cache as well.

SQL Server Cache Dependencies

A more complex kind of cache dependency is the SQL Server cache dependency. In a nutshell, SQL cache dependencies provide the ability to automatically invalidate a cached data object (such as a DataSet) when the related data is modified in the database.

Although this feature is technically supported in SQL Server 2000, it's a better idea to use SQL Server 2005 or later. That's because these versions of SQL Server have a built-in notification system, which makes tasks like these much more efficient.

Note

The instructions in this chapter apply to SQL Server 2005 and later versions. They work just as well with the free editions of SQL Server, such as SQL Server 2008 Express.

To understand how database dependencies work, you first need to know a bit about SQL Server's built-in messaging system, which is called the Service Broker. The Service Broker manages queues, which are database objects that have the same standing as tables, stored procedures, or views.

Thanks to queues, you can instruct SQL Server to send notifications for specific events using the CREATE EVENT NOTIFICATION command. But ASP.NET offers a more convenient, higher-level model—you register a query, and ASP.NET automatically instructs SQL Server to send notifications for any operations that would affect the results of that query. Every time you perform an operation, SQL Server determines whether your operation affects a registered command. If it does, SQL Server sends a notification message and stops the notification process. Figure 23-9 shows an overview of how this cache invalidation system works.

Monitoring a database for changes in SQL Server

Figure 23.9. Monitoring a database for changes in SQL Server

Notifications work with SELECT queries and stored procedures. However, some restrictions exist for the SELECT syntax you can use. To properly support notifications, your command must adhere to the following rules:

  • You must fully qualify table names in the form [Owner].table, as in dbo.Employees (not just Employees).

  • Your query cannot use an aggregate function, such as COUNT(), MAX(), MIN(), or AVERAGE().

  • You cannot select all columns with the wildcard * (as in SELECT * FROM Employees). Instead, you must specifically name each column so that SQL Server can properly track changes that do and do not affect the results of your query.

Here's an acceptable command:

SELECT EmployeeID, FirstName, LastName, City FROM dbo.Employees

These are the most important rules, but the SQL Server Books Online has a lengthy list of caveats and exceptions. If you break one of these rules, you won't receive an error. However, the notification message will be sent as soon as you register the command, and the cached item will be invalidated immediately.

Enabling the Service Broker

SQL Server is often installed with carefully locked-down settings for optimum security. To use SQL Server notifications, you may have to enable features that are currently switched off.

First, you need to enable the Service Broker, which watches for changes in the database and delivers the notifications to the appropriate queue. The Service Broker must be specifically enabled for each database that you want to use with cache dependencies.

If the Service Broker isn't currently enabled for your database (or if you're just not sure), there's an easy solution. First, launch the Visual Studio 2010 Command Prompt window (click the Start button and choose All Programs

Enabling the Service Broker
SqlCmd -S localhostSQLEXPRESS

This connects to SQL Server Express on the current computer. If you're using the full version of SQL Server, you won't need to supply the instance name (you can use just localhost instead of localhostSQLEXPRESS). If your database is installed on another server, use its computer name instead of localhost.

The SqlCmd.exe utility provides a command prompt where you can enter SQL commands. Use it to enter the following SQL statements:

USE Northwind
ALTER DATABASE Northwind SET ENABLE_BROKER
GO

Of course, if you want to enable the Service Broker for a different database (other than Northwind), you can modify this SQL accordingly. You can enable the Service Broker for as many databases as you'd like.

Once you're finished, type quit to exit the SqlCmd tool.

Initializing the Caching Service

Before you can use SQL cache dependencies with SQL Server, you need to call the shared SqlDependency.Start() method. This initializes the listening service on the web server.

Dim connectionString As String = _
  WebConfigurationManager.ConnectionStrings("Northwind").ConnectionString
SqlDependency.Start(connectionString)

You need to call the Start() method only once over the lifetime of your web application, so it often makes sense to place the call in the Application_Start() method of the global.asax file so it's triggered automatically. It's safe to call the Start() method even if the listener is already started, as this won't cause an error. You can also use the Stop() method to halt the listener.

Creating the Cache Dependency

When you create the dependency object, you need to supply the command that you're using to retrieve your data. That way, SQL Server knows what range of records you want to monitor.

To specify the command, you create the SqlCacheDependency using the constructor that accepts a SqlCommand object. Here's an example:

' Create the ADO.NET objects.
Dim con As New SqlConnection(connectionString)
Dim query As String = _
 "SELECT EmployeeID, FirstName, LastName, City FROM dbo.Employees"
Dim cmd As New SqlCommand(query, con)
Dim adapter As New SqlDataAdapter(cmd)

' Fill the DataSet.
Dim ds As New DataSet()
adapter.Fill(ds, "Employees")

' Create the dependency.
Dim empDependency As New SqlCacheDependency(cmd)

' Add a cache item that will be invalidated if one of its records changes
' (or a new record is added in the same range).
Cache.Insert("Employees", ds, empDependency)

Now, when you change the data in the table, the notification will be delivered, and the item will be removed from the cache. The next time you create the DataSet, you'll need to add it back to the cache with a new SqlCacheDependency. To try a page that uses this technique, check out the sample code for this chapter.

The Last Word

The most performance-critical area in most web applications is the data layer. But many ASP.NET developers don't realize that you can dramatically reduce the burden on your database and increase the scalability of all your web applications with just a little caching code.

However, with any performance-optimization strategy, the best way to gauge the value of a change is to perform stress testing and profiling. Without this step, you might spend a great deal of time perfecting code that will achieve only a minor improvement in performance or scalability, at the expense of more effective changes.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.179.225