ASP.NET 2.0: Cache In on Performance Shortcuts

Boost your Web site performance by using new callback functions and enhanced caching options

William Sheldon

June 21, 2006

19 Min Read
data streaming across human eyeball

Whether you're an ASP .NET developer who works with data-based applications or a SQL Server DBA supporting ASP.NET applications, you'll find new capabilities in ASP.NET 2.0 to help you boost yourWeb site's performance.With the release of SQL Server 2005,Visual Studio 2005, and Microsoft .NET Framework 2.0, Microsoft continues to enhance its support at every tier of an ASP.NET application.At the client tier, ASP.NET 2.0 introduces callback functions that let you provide a dynamic, interactive application display without increasing roundtrips to the server. On the Web server tier, new page and partial-page caching options improve performance and give you more control over cache settings. And on the database tier, you can now create SQL Server dependencies to automatically invalidate cached data that the Web server is using.

Callback Functions

When a user makes changes within a Web page, the speed of your application's response affects that user's perception of your application's performance. But the amount of data you transmit to support those changes actually affects your application's scalability and performance.

In previous releases of ASP.NET, you enable a Web page's dynamic elements by sending the client a set of Java or JScript functions along with all the data those scripts should act on. For example, if you want to change the content of DropDownListBox B based on the user's selection in Drop-DownListBox A without making a server roundtrip, you send the client all possible options for DropDownListBox B along with the JScript code that will be called whenever a user selects something in Drop-DownListBox A. However, this approach has some significant performance limitations when you have a large list of potential options. You can't provide a truly interactive UI if you must download potentially thousands of bytes that the user might never access.

Instead, your client-side script needs to retrieve additional data into the client's desktop-based browser without needing to refresh the entire Web page.This technique, called Asynchronous JavaScript And XML (AJAX), originated years ago as part of custom enhancements to Microsoft Internet Explorer (IE). Over time, all major browsers implemented in some fashion the XMLHTTP object that Microsoft introduced, and Microsoft is now working on a robust set of AJAX-based tools called Atlas to support high-performance, interactive Web pages. The Web sidebar "Atlas Takes On the World," introduces Atlas, but as powerful as this toolset promises to be, it didn't ship with ASP.NET 2.0— callback functions did.

Callback functions in ASP.NET 2.0 let you request from your server additional data based on a user action. And rather than writing the JScript code to perform this functionality, you can leverage built-in ASP.NET 2.0 code that lets you retrieve data directly from the server without reposting the entire page to the client.

Callback functions use ASP.NET's ICallbackEventHandler interface. This interface defines two server-side methods that your ASP .NET page needs to implement to handle calls from a Web client back to your server page. Your page calls the first method, RaiseCallbackEvent(ByVal eventArgument As String), when the client's browser raises a callback event to your server.The code you use to call Raise-CallbackEvent must use the RaiseCallback-Event method name because ASP.NET looks only for a method with this name. In addition, the ASP.NET-generated code accepts a single string as the input parameter to this method, which then passes the string to the XMLHttpRequest object for processing. This method doesn't return anything—meaning that in C#, it's a void method, and in Visual Basic (VB), it's a Sub method.

YourWeb page then needs to implement the GetCallbackResult method, which the page calls to retrieve the result of the Raise-CallbackEvent method.This second method doesn't accept an input parameter, but it returns a string value, so you need some client code to parse and react to the returned string. For more complex operations—for example, those that populate a DataGrid—you want the returned string to contain XML that you can parse into the rows and columns of your grid.

You might think you need to add JScript functions to your page to implement these server-side methods for ASP.NET callback functions. But your page needs to perform only four steps to make a callback to the server and retrieve the results—and ASP.NET generates code for the first three steps for you.The four steps are

  1. Call back to the server with the data formatted as a string to trigger the server processing.

  2. Call back to the server to get the result of that processing.

  3. Call the client-side function you define, passing it the result so that the function can process it and display it on the page.

  4. Process the result in JScript or another client scripting language.

Let's look first at Step 4, which you need to have in place to generate the code for steps 1-3. In your Web page, you need a function that accepts two parameters: the result and a context (i.e., the name of the client script making the callback). For this example, let's call this function ProcessCallbackResult.The result parameter will contain the string returned from the callback.

Next, to generate the code for steps 1-3, you need to define as part of your page's loadevent-code the key end points for the callback processing. Step 1 is to get, at runtime, the address of your RaiseCallbackEvent method in your server page and pass it to the client page.This processing starts with a call to the shared (or static) methodPage.ClientScript .GetCallbackEventReference(). This overloaded method accepts the name of the ProcessCallbackResult function on the client along with three to five additional parameters that define your callback function.

The call to Page.ClientScript. GetCallbackEventReference() is important because you pass it the name of the client function you want called, and it returns a string that makes the callback, gets the callback result, then passes that result to your function.This code generation works because the two methods you define to implement the ICallbackEventHandler in your server code are limited by name and parameter types, letting .NET generate the necessary client code to call these server-side methods. I encourage you to review this functionality in more detail on the Microsoft Developer Network (MSDN) to see how to define error handlers on your client and other robust capabilities that are beyond the scope of this article. A good starting point—beyond the GetCallbackEventReference() method's Help page—is the MSDN article "Implementing Client CallbacksWithout Postbacks in ASP.NET Web Pages."

After you call GetCallbackEventReference(), you combine the result of the call with a simple client-side script to wrap the generated function logic with a name that you can recognize and reference.You create the function by combining the string result from the GetCallbackEventReference() call with a JScript function header. For example, if you assigned the result of the GetCallbackEventReference() call to a local variable called stringDelegate, your clientFunction string could be generated as

clientFunction = "function   GetData(strParam, context)    { " + stringDelegate + "; }"

This code defines a complete client-side JScript function, GetData, which implements the full callback interface defined on the server, then passes the results to Process-CallbackResult, which you defined earlier. To call this new client function, you must simply include it in your Web page by calling the Page.ClientScript.Register-ClientScriptBlock() method.

The last step is to reference the GetData client function. For example, in an onchange() event for a client control, you can call the GetData function, passing it two parameters: strParam and context.The result of that call will call your custom Process-CallBackResult function, which will implement code to parse the returned string and update the controls on the page. Because of space constraints, I don't include an example of this logic. But in the QuickStart guides on the ASP.NET and MSDN Web sites, you can find valuable examples of implementing callback functions.

All this processing will occur without the user seeing a complete refresh of the page and without the server needing to retransmit the entire page.Callback functions also provide performance advantages compared to using only cached page data. For example, with a callback function, you can take an otherwise cached page and, as the page loads into a user's browser, call back to your server and retrieve real-time data without needing to recompile or change any of your cached data.

Output Page Caching

Output page caching, introduced in ASP.NET 1.0, lets you specify that the dynamic content generated as a response for a given page request be kept in memory for future requests for the same page.You can keep this data in memory for a limited number of requests or for a limited amount of time. And if any parameters change the data that's requested, the cache will store different copies of the data based on those parameters.

Output page caching is a powerful scalability and performance tool but can raise concerns about data freshness.Although you can let some data get reasonably stale, in most cases, applications expect up-to-date information. New ASP.NET 2.0 features at the database tier can help resolve concerns about stale data. But note that even if you set your cache to only a 1-second duration, you'll still see a significant increase in a busy site's scalability and performance.

ASP.NET 2.0 introduces master pages, which provide a single source file that acts as a template for common data within your display.This feature requires a way to let this common data be cached for all pages that reference the master page without requiring that the content inside of each page also be cached. For example, if the caching declaration was controlled by the master page, which is used across multiple pages on your site, all your pages could potentially contain the same data. I mention this to stress that output caching doesn't need to occur solely at the page level. You can also implement output caching for user controls. The only difference for user controls is that the declaration is located in the user control file instead of theWeb form. Note that for output caching, the duration is measured in seconds (not milliseconds, as you'll see in the discussion of SQL-based caching). With that in mind,output caching in ASP.NET 2.0 uses the same declaration style you're familiar with:

<%@ OutputCache Duration="120"   VaryByParam="none" %>

The most important enhancement that ASP.NET 2.0 brings to output page caching is that you can customize your cache, thanks to two new capabilities for better managing the details of your server's cache. ASP.NET 2.0 lets you customize some cache settings by using the web.config and system.config files on your server. By using these files, you can predefine cache sizes, set whether certain features are disabled, and so on. For example, if you're a hosting provider and want to limit the resources that individual sites use on your hosting server, you can modify the machine.config file and set limitations across the hosted sites.

As part of the cache-configuration process, you can leverage a second new capability in ASP.NET 2.0: cache profiles. The idea behind a cache profile is that you probably have many different pages or parts of pages that you want cached using the same settings. Rather than spreading these settings across different locations in your application, you can name a set of cacheconfiguration settings inside the cache settings in your configuration file.You can then reference this group of settings by name and modify them in one location.

For example, if you declared a profile for a 60-second cache in a web.config file called "CacheFor60," your web.config file would contain the profile that Listing 1 shows.The Output Cache declaration in your page might look like the following statement:

<%@ OutputCache    CacheProfile="CacheFor60"    VaryByParam="prodgroup" %>

This code leverages the profile, then has the cache vary by the parameter "prodgroup." For more information about these cacheconfiguration settings, see the Microsoft article "Cache Configuration in ASP.NET."

The Cache Object

I've focused mainly on page-oriented caching so far, but now it's time to look at your data. It's common for everyone accessing a Web application to be referencing the same set of products, courses, or other set of key data elements that are common across users. By using the Cache object, a shared (or static) class that's available to your ASP.NET application, you can declare an entry in the data referenced by the object.Then, similar to session data, the data will be available on your next call.

However, unlike session data, which is user-specific, data in the Cache object is available on the next call to your application by any user of your application. Thus, the Cache object isn't a good place to put userspecific information. Still, it's an excellent place to hold data that you'll use across all users of your application.

The Cache object isn't new with ASP.NET 2.0, so I don't want to spend too much time on it. But I do want to mention one change that resolves a glaring hole in the original .NET Framework 1.* implementations. As part of a Cache entry, you can define a dependency on an XML or other system file,but you can't declare a dependency on a database. ASP.NET 2.0 extends the Cache object so that you can now create a custom class that inherits from System .Web.Caching.CacheDependency to define logic for a custom dependency. Although you can use this capability to define a custom dependency on your database,ASP.NET 2.0 also provides an updated implementation that handles database dependencies for you, as you'll see next.

Data-Dependency Refresh

A new ASP.NET 2.0 caching feature is the ability to create a cache dependency based on SQL Server.You can base the dependency on both time and table updates. If you want a page generally to be cached for 5 minutes but want it to refresh sooner if key data changes, you would use the SQL Cache dependency.ASP.NET supports two modes for this caching model: one that is backwardcompatible to SQL Server 2000 (and, by default, 7.0) and one specific to SQL Server 2005. Let's look first at the SQL Server 2000-compatible model, called the polling method.

With the polling method, you configure ASP.NET to automatically poll the database on a specified interval and refresh the cache if any changes occur to the table you're interested in.This model lets you provide a nominal refresh time for your Web page but still react to key changes in your database more frequently. The disadvantage is that this method is still a time-based refresh. And configuring this type of database dependency requires more effort than leveraging SQL Server 2005's automated change notifications.

However, even if you've already created a custom database polling solution, you still might be interested in using this model to leverage Microsoft's coding and reduce the amount of custom code in your application. More important, your custom solution might invalidate the full cache to check whether the data has changed, whereas the SQL Cache dependency model checks the database for changes without invalidating the cache unless there's new data to include.And, you can associate SQLCacheDependency as a property on the DataSource object you're using in your ASP.NET page instead of writing any actual code—but more about this after we walk through the configuration.

To configure this model, you need to use the aspnet_regsql.exe utility, which lets you configure your database to support this polling-based solution. Aspnet_regsql.exe is a command-line tool, which requires you to define a string of command-line parameters. The first set of parameters defines the database-connection string for the database on which you want to create the automated SQL dependency.

You use either three or four parameters to build the connection string. The first parameter, —S followed by the name of your server, defines the server. If you're referencing a named instance, include the instance name as part of this parameter.The second parameter, —D followed by the name of the database, is the database you'll reference on that server. Finally, you need to specify credentials. If the database is in your current domain or on your current machine, you can use —E, which tells aspnet_regsql.exe to use integrated security and your current account to reference the database. Alternatively, you can supply a username and password, preceded by the —U and —P parameter labels, respectively.

The example command in Listing 2 defines the database by using the above connection parameters and some additional parameters to create the database dependency. For a database dependency to work, you need to define two additional items beyond the connection string. The first is an ASP .NET-specific table that your database updates when a table of interest is updated.The database needs only one common table to record possible updates to any of the application tables.The —ED (think "Enable Database") parameter in Listing 2 tells aspnet_regsql.exe to create this table, called AspNet_SqlCache-TablesForChangeNotification, and the five stored procedures (prefaced in the AspNet_ namespace) that ASP.NET uses to access it.

The second item you must define is which table should be tracked for updates. The —ET parameter tells aspnet_regsql.exe that you want to "Enable a Table" for a SQL Cache dependency.The next step is to name the table on which this trigger will be created to register updates into the database's changetracking table. The —T parameter in Listing 2's example tells aspnet_regsql.exe which table's definition to modify, and the utility will then add a trigger called

_ AspNet_SqlCacheNotification_Trigger to that table.The process of creating a new table, stored procedures, and a trigger on an existing table requires elevated permissions, which is why I used the sa account in the example.

These steps have configured your database to support a polling-based SQL dependency. The next step is to tell your application about the dependency by using its web.config file. Within the web.config file for your application, you add a new caching section as part of the System.Web section. This new section, which Listing 3 shows, tells ASP.NET the name of the database connection, the name of the connection string (also defined in your web.config file), and the polling frequency, measured in milliseconds.

Notice that you don't have to write any code or duplicate any specific database connection information; you're just registering your dependency with ASP.NET.The declaration in Listing 3 doesn't include a table name, just the database name, which introduces a limitation:A given dependency will poll as frequently as the table you want to most frequently invalidate. This example is querying only one table in the database, so you wouldn't have a performance problem. But if a problem arises,you could have trouble determining which table has changed.

To solve this problem, you can include the table of interest as part of the dependency definition. To use that dependency within your application code, you need to modify your cache declaration.You can add a SqlDependency as part of any of the previous forms of caching, and most important, you can directly associate it with the data source you're using to populate controls.To associate your dependency within a cache declaration, simply add the following declaration:

For a DataSource object, you add the same statement within your data source declaration:

In this declaration, I could use the dependency to check for data on a different table from the one previously described.And even if one table was updated, it wouldn't invalidate the second cache. However, at its core, the polling mode is still a timer-based technology: Whether you invalidate the cache after 5 seconds or just check the database for an update every 5 seconds, the check is still occurring at a predefined interval.The alternative is to have SQL Server notify ASP.NET when a change occurs.

The second model for setting up SQL dependencies is notification based.When I see the word notification related to SQL Server 2005, I immediately think of SQL Server Notification Services. But the focus here is on SQL Server's internal change notifications.When ASP.NET executes a command against SQL Server 2005, it can register for any changes related to that command.These notifications are like automatic transactions in that you start the process of listening for them by calling the System.Data.Sql-Client.SqlDependency.Start() method.

Because ASP.NET is a multi-threaded environment that calls individual pages repeatedly, you can put this declaration in only one place—the Application.Start method of your application's Global.ASAX page.Your application runs the code in this file based on application events, such as the application starting.When you tell ASP.NET and, by proxy,ADO.NET to listen for SQL Server's change notifications, you can start creating dependencies.

In this case, dependencies reference a generic dependency handler called command notification.To reference this handler, either in a cache declaration or as part of a SQLDataSource object, simply add the following property to your attribute:

Note that no table name is associated with this registration; you also don't need an entry in your web.config file or a custom aspnet_ table or access method. But there are other requirements. First, you must grant the account used for database access permission to subscribe to and send query notifications within SQL Server.You grant the permissions by using the T-SQL commands that Listing 4 shows.You don't need to execute these commands if you're using the sa account in your development environment. However, in production, where you're following security best practices and using an account with less permission, you'll need to run these commands to let your database user leverage query notification information.

When the account has permission to track query notification information, there are two primary limitations that restrict which changes it will detect. But first, let's look at what's tracked.Within either a Data-Source or Cache object, you execute queries to populate the data that's cached. As these queries execute and data accumulates, ASP.NET tracks the queries you used. It registers the results of the queries with SQL Server, then as other statements are executed, SQL Server determines whether the results of your queries of interest have changed. When SQL Server detects a change that affects one of these registered database queries, it notifies ASP.NET, which can trigger the invalidation of the Cache object associated with that query.

The good news is that this processing is all automatic—you don't have to do anything to reap the benefit. However, just how many queries can your ASP.NET application be registered for? This is where the two limitations I mentioned come into play. For starters, any query that uses the wildcard character (*) to retrieve all fields in a table (which is a poor practice, anyway) is always considered invalid. You can't register for notification of changes to that table and will instead retrieve the data with every request, eliminating caching. The second limitation is that to register for a query, you must use the owner-qualified name of the table within the SELECT statement, which can be embedded in a stored procedure. So for the example above, your query would need to reference dbo.orders as opposed to just orders. For more information about the potential limitations on query notifications, see the SQL Server 2005 Books Online (BOL) topic "Using Query Notifications" or the MSDN article "Creating a Query for Notification."

ASP.NET 2.0 and SQL Server 2005 let you fully optimize your site's performance. In today's business world, it's not just about having a Web presence—it's about using the Web for business activity and profit. By taking advantage of performance-enhancing tools at every level of your application—display, business logic, and data store—you can create a dynamic Web application that minimizes the hardware you need to support hundreds or even thousands of users.

Sign up for the ITPro Today newsletter
Stay on top of the IT universe with commentary, news analysis, how-to's, and tips delivered to your inbox daily.

You May Also Like