While much of ASP.NET was enhanced for performance with the 2.0 release, the caching-related features are the most visible and are likely to have the most impact on your applications. The most visible new caching feature, data source caching, provides a simple model for adding caching to data sources, making it much easier to add caching to Web applications. Next, SQL cache dependencies give you the ability to tie a cache entry to a database result, effectively eliminating cache coherency problems. Then, post-cache substitution solves the unique problem of needing to cache everything except a few small pieces. Finally, the output caching feature introduced in the first release is now enhanced with new configuration settings for making global cache policy changes easy to accomplish. Each of these features is a welcome addition and should make adding and managing caching in your Web applications much easier and more effective. Data Source CachingOne of the big advantages of using declarative data sources in ASP.NET 2.0 is the ease with which you can enable caching. By setting the EnableCaching property to true on a data source control, logic is in place to store data retrieved with the first call to the SelectCommand property in the application-wide cache. Subsequent calls to the select command for that data source will pull the data directly from the cache instead of going back to the data source. When you enable caching on a data source control, you can also specify settings for the cache entry by using the CacheDuration, CacheExpirationPolicy (whether it's sliding or absolute), and CacheKeyDependency properties. Listing 8-1 shows a sample SqlDataSource control with its EnableCaching property set to true and its CacheDuration set to one hour (3,600 seconds). The GridView bound to this data source will load its data from the cache after the first request, avoiding a round trip to the database server for each subsequent request. Listing 8-1. Enabling caching on a SqlDataSource control
The behavior of a cached data source is exactly what you would see if you populated a DataSet and inserted it into the Page.Cache object yourself, using the cached instance to manually bind data to a control on subsequent access. The caching feature of the SqlDataSource class only works when the data source has its DataSourceMode set to DataSet (the default), since it makes no sense to cache a DataReader that is a streaming data access mechanism only. An important question to ask is how the cache key is generated for a cached data source, since that is no longer in your hands with data source control caching. For example, if you have two data source controls on separate pages returning the same result set, should they index into the same cache entry? What if you have parameters to your select commandshould each unique parameter value point to a different entry in the cache? The data source controls answer both of these questions in the affirmativedata sources returning the exact same result sets will index into the same cache entry, and any variation in parameters for the select command will result in a new cache entry. The cache key itself is generated internally, taking into account all of these factors. For example, the cache key for the data retrieved by the data source in Listing 8-1 is: u914027403600:0::server=.;integrated security=SSPI;database=pubs:SELECT au_id, au_lname, au_fname FROM authors:0:-1 Note that as Table 8-1 shows, any variation in the cache settings, connection string, or text of the select command will result in a unique cache key and corresponding cache entry. This is important to know if you want to try and share cached data across pages, since all of the attributes of the data sources must be essentially identical for sharing to occur. As with all caching, you want to be sure that you are using the cache entries effectively and not just wasting space in the cache with entries that are never hit. For example, if you are passing in a userid or some other value that may change on a per-request or per-user basis as a parameter to a data source's select command, your cache will quickly be polluted with entries that are rarely used and take up unnecessary space in the cache.
ObjectDataSource CachingIf you are working with a data access layer using the ObjectDataSource instead of SqlDataSource, you still have complete support for caching. All of the same attributes are available in the ObjectDataSource class, and as long as your data classes don't return IDataReader results, the enumerable collections will be saved in the cache whenever caching is enabled. The only difference is that the cache key no longer contains a connection string or a select command, but instead uses the TypeName and SelectMethod properties to ensure unique cache entries for each data request. Listing 8-2 shows a sample data class with a GetPeople method that returns a collection of Person objects (in this case artificially generated), and Listing 8-3 shows a sample page with a GridView being fed data from an ObjectDataSource mapped onto the sample data class' GetPeople method. Listing 8-2. Sample data class with Person entity class
Listing 8-3. Caching an object data source
Data Source Caching and ViewStateIn both Listings 8-2 and 8-3, the EnableViewState flag was set to false in the GridView, which should be done in almost all scenarios where declarative data source caching is enabled. The combination of using a cached data source with a control whose ViewState has been disabled is a "sweet spot" for building fast, scalable, data-driven pages. As covered in Chapter 3, the new suite of data-bound controls continue to function properly even when ViewState is disabled (through a separate state transfer mechanism called ControlState), so disabling ViewState only has the effect of not propagating the data for the control to the client and back using the hidden __VIEWSTATE field. This combined with data source caching provides an extremely efficient, low-bandwidth page-serving dynamic content. The only remaining issue is dealing with stale data, which we will address next with SQL cache dependencies. Cache DependenciesOne of the most requested features for this release of ASP.NET was to add the ability to invalidate a cache entry when results from a SQL query changed. It was added in the form of the SqlCacheDependency class. This class (and its associated infrastructure) lets you flush a cache entry whenever the table (or result set) on which it depends changes in the underlying database. It is implemented in SQL Server 7 and 2000 using a custom "change" table, a database trigger, and a polling mechanism from the ASP.NET worker process. In SQL Server 2005, it is implemented using the service broker feature, which does not have to resort to polling and does not need any instrumentation (since it is supported natively by the database engine). This new dependency system is also completely pluggable so that if you needed to have cache entries flushed when some other external event occurs, you can write your own custom class that can be associated with any cache entry, as we will explore shortly. SQL cache dependencies can be used in three different caching contexts. First, you can associate a SQL cache dependency with a cached declarative data source by populating the SqlCacheDependency property with the name of the dependency. You can also use SQL cache dependencies to flush output-cached pages from the cache whenever associated data changes with the OutputCache directive's SqlDependency property. Finally, you can directly specify a SqlCacheDependency class in the call to the Cache object's Insert method by specifying the dependence in the constructor. All three of these mechanisms work with both the SQL Server 2000 polling mechanism as well as the SQL Server 2005 callback mechanism; only the names of the dependencies change. SQL Server 7 and 2000 Cache DependenciesTo set up a SQL cache dependency in SQL Server 7 or 2000, you must first populate the database to be used with a change notification table so that records of table changes can be recorded and detected. To do this, use the aspnet_regsql.exe command-line utility with -ed as an option. The database to instrument is specified with the -d option if it is a local database, or with -C to provide the full connection string. To use integrated authentication, use E; otherwise, specify the SQL credentials in the connection string. For example, the following command will instrument the "pubs" database to work with notifications: aspnet_regsql.exe d pubs ed E Once you have instrumented the target database with the change notification table, you must next enable change detection on the table (or tables) from which data is being retrieved. Use the aspnet_regsql.exe utility again, this time with the -et option to enable table change notification, and use the -t option to specify the table name. This will add a trigger that is invoked any time the table in question is modified. It will also add a row to the change notification table with the table name and an associated changeid field. Whenever the trigger is invoked, it will increment the value in the changeid field. For example, the following command will enable the authors table in the pubs database for change detection: aspnet_regsql.exe d pubs et t authors E The last steps are to register your SQL cache dependency in your configuration file, and then reference the configured dependency wherever you would like it to take effect. Listing 8-4 shows an example of registering a sqlCacheDependency with an existing connection string, and Listings 8-5, 8-6, and 8-7 show examples of using this dependency referencing the authors table in a declarative data source, an output cache directive, and a manual cache insertion, respectively. You can also specify multiple cache dependencies by listing them with a comma delimiter (pubs:authors, pubs:publishers). Listing 8-4. Configuring a SQL cache dependency for SQL Server 7/2000
Listing 8-5. Specifying a SQL Server 7/2000 cache dependency in a declarative data source
Listing 8-6. Specifying a SQL Server 7/2000 cache dependency in an OutputCache directive
Listing 8-7. Specifying a SQL Server 7/2000 cache dependency in a manual cache insertion
Once the SqlCacheDependency is set up in ASP.NET, it will poll the database and invoke a stored procedure every n milliseconds (as specified in the configuration file entry). This is performed on a dedicated worker thread inside of the ASP.NET worker process and is not executed from a request thread. If the stored procedure detects that the changeid column for a particular table has changed, ASP.NET will flush the associated entries for that SQL dependency. Figure 8-1 shows all of the elements of SQL cache dependencies interacting when using SQL Server 7 or 2000. Figure 8-1. SQL cache dependencies in SQL Server 7/2000SQL Server 2005 Cache DependenciesSQL Server 2005 uses a completely different implementation of the SqlCacheDependency class (although it technically is the same class, it operates in two distinct states). The SQL Server 2005 implementation uses the query notifications feature of SQL Server 2005 to mark a particular command as generating a notification when it changes. When a command is issued with a request for change notification, the database creates an indexed view (which is essentially a physical copy of the results of the command) and monitors anything that happens in the database that may affect the results (inserts, updates, deletes). When it detects a change, a query notification event is triggered, at which point the database uses the Service Broker feature of SQL Server 2005 to send a message back to the ASP.NET worker process indicating that the cache entry associated with that command needs to be flushed. This callback mechanism is generally much more efficient and generates much less network traffic than the polling mechanism used for SQL 7 and 2000. Figure 8-2 shows the general architecture of SQL Server 2005 cache dependencies. Figure 8-2. SQL cache dependencies in SQL Server 2005The setup involved with using SQL Server 2005 cache dependencies is also much less than the polling mechanism just described, primarily because the entire infrastructure to issue the notifications is already built into the databaseall you have to do in your ASP.NET application is request the notification. Before you can receive notifications, however, you must signal that your application is going to be monitoring service broker notifications using the static Start method of the SqlDependency class for each database connection you intend to receive notifications from. This call sets up a connection to the database and issues an asynchronous command to wait for notifications (using WAITFOR and RECEIVE). You only need to call this method once before you issue any requests to the database that include notifications, so it is common practice to place the call in the Application_Start event of the global application class, usually defined in the application-wide global.asax file, as shown in Listing 8-8. Listing 8-8. Calling SqlDependency.Start in Application_Start
With the worker process ready to receive notifications, you can now use the SqlCacheDependency class to create dependencies on commands sent to the database. Unlike the polling mechanism described earlier, this dependency mechanism is enabled on a per-command basis, not a per-table basis. This means that you can associate the results of almost any command that returns results, including a stored procedure (there are limitations to the types of commands, however, which we will cover shortly). To create a dependency programmatically, you must first prepare the SqlCommand you would like to set up notifications for, and then pass the command object in as a parameter to the constructor of the SqlCacheDependency class. Listing 8-9 shows an example of programmatically inserting the results of a query into the cache with an associated SQL cache dependency. Listing 8-9. Programmatically specifying a cache dependency with SQL Server 2005
To set up cache dependencies declaratively that rely on SQL Server 2005 notifications, you use the same SqlCacheDependency property of the SqlDataSource control as we did with the SQL 7/2000 dependencies, but instead of specifying a list of table names, you just use the CommandNotification string. This tells the data source to prepare a SqlCacheDependency initialized with the select command it uses to retrieve data, and to add that to the cache insertion, as shown in Listing 8-10. Listing 8-10. Specifying a SQL Server 2005 cache dependency in a declarative data source
Similarly, you can use the CommandNotification string in the SqlDependency attribute of the OutputCache directive of a page. It's not obvious how simply adding the CommandNotification string to your OutputCache directive would enable any dependencies, since the OutputCache directive itself is not associated with any commands. What it will do is set a flag in the call context as the page executes, which the SqlCommand class checks internally as it is preparing a command. If the flag is set, it will implicitly associate a dependency with the command and associate it with the cache entry for the page itself. The end result is that if any of the queries are made on the page, the page will be flushed from the cache and reevaluated. This can have side effects that may not be obvious, for example, if one of the queries on the page doesn't adhere to the constraints for using SQL Server 2005 dependencies, that dependency will behave like it is always invalid, effectively invalidating the output caching for that page entirely. If you do decide to enable CommandNotification dependencies on an output cached page, take care that all of your queries work with SQL Server 2005 notifications. Listing 8-11 shows an example of an output cached page with CommandNotification specified as the SqlDependency. This page has two data sources populating two separate GridViews, and because the queries used by each data source are compatible with notifications, the page will be cached until either of the results of the two queries changes. Listing 8-11. Specifying a SQL Server 2005 cache dependency in an OutputCache directive
As simple as it sounds to use SQL Server 2005 dependencies, in practice they can be somewhat tricky to get working because of all the pieces that have to be properly in place, and all of the queries used must adhere to the limitations of indexed views. The most common symptom you will see if a dependency isn't working is that the data associated with that dependency will be retrieved from the database every time and never drawn from the cache. After enabling a SQL Server 2005 cache dependency, it is usually wise to verify that it is indeed caching by watching a trace of the database as you access the page that references the cached data. Some problems will show themselves in the form of an exception, which will usually have good information on how to address the problem. If you find that a SQL cache dependency is not working, the following list of common issues and resolutions may help you diagnose why:
If you can work through these potential pitfalls when working with SQL Server 2005 cache dependencies, the results are definitely worth the effort. The ability to cache data that is frequently accessed for efficiency without having to worry about cache coherency ever being an issue is truly an impressive achievement, and taking advantage of it will increase the scalability and responsiveness of your application without compromising data integrity. Custom Cache DependenciesIf none of the prebuilt cache dependency classes fits the bill for the type of caching you are doing, there is always the possibility of creating your own custom cache dependency. In this release of ASP.NET the cache dependency system is now completely pluggable, opening the door for third-party cache dependency classes (possibly for compatibility with other data stores) or completely custom dependencies that you write yourself. Any class that derives from the common CacheDependency base class can be used to tie flushing logic to any cache entry. As an example, consider a site that uses a collection of Web services to supply data to its pages. It might make sense to build a custom cache dependency class that invoked a Web service periodically, to find out whether the results of other (more data-laden) Web service calls have changed since the last time they were called. The first step is to create a new class that inherits from CacheDependency (in the System.Web.Caching namespace). Your primary task when creating a custom cache dependency class is to identify when cache entries associated with your class should be expelled from the cache, and to fire the inherited NotifyDependencyChanged event when this happens. Listing 8-12 shows a sample implementation of a custom cache dependency class called WSDataDependency. This example uses a timer to periodically poll a Web service (much like the SQL Server 2000 cache dependency class) to find out whether data has changed or not. This implementation assumes that the Web service contains a single method for checking the validity of the data with a signature of bool IsValid(). It also lets users of the class set the URL of the Web service, and thus this would work with any Web service that supported a method called IsValid with the correct signature. You can also customize the interval at which the polling timer will fire with the constructor's second parameter. Finally, in the timer handler implementation, it invokes the Web service's IsValid method to detect a change, and if it finds one it fires the NotifyDependencyChanged event inherited from the base CacheDependency class. Listing 8-12. Custom cache dependency class WSDataDependency
With this class in place, you could then use it like any of the other cache dependency classes, by creating a new instance, initializing it, and passing it in as the third parameter to the Cache.Insert method. Listings 8-13 and 8-14 show a sample use of the class applied to a Web service call that returns a simple DateTime class as its response. Once inserted into the cache, the time shown in the rendered Label would only update whenever the IsValid Web method invoked by the WSDataDependency class returned false, or the hard-coded limit of 60 seconds was reached. Listing 8-13. Sample use of the custom WSDataDependency class
Listing 8-14. Sample use of the custom WSDataDependency class (codebehind)
Programmatic Fragment CachingIn ASP.NET 1.1 (and 2.0), it is possible to programmatically configure a page's output cache settings by using the Response.Cache property of the Page class, which references an instance of the HttpCachePolicy class. This means that you can make decisions about how long a page should stay in the cache, whether it has a sliding expiration, whether it has a dependency, and so on programmatically, which opens up many possibilities for controlling the output caching of your pages. On the other hand, while it has always been possible to cache user controls as well (often called page fragment caching), it was not possible in ASP.NET 1.1 to programmatically modify the cache control settings for an output cached control. This changes in 2.0, as the UserControl class has a new property, CachePolicy, that is an instance of the ControlCachePolicy class. You can now use this to manipulate the output caching behavior of a user control in your site. The ControlCachePolicy exposes all of the attributes of an output cached control, as shown in Listing 8-15. Listing 8-15. The ControlCachePolicy class
As an example, consider a user control that displays the results of a database query in a GridView control. If this user control is marked with an OutputCache directive, you could then modify any of the settings associated with the output cache entry through the CachePolicy property. The sample user control shown in Listings 8-16 and 8-17 programmatically alters the duration of the cache entry for this user control from 120 seconds (two minutes) to 60 (one minute). Listing 8-16. Output cached user control
Listing 8-17. Output cached user control (codebehind)
Post-Cache SubstitutionIn ASP.NET 1.1, the only way to cache everything on a page except one small piece was to encapsulate the entire page into user controls marked with OutputCache directives. Post-cache substitution provides a cleaner way of accomplishing this by letting you mark a page as output cached, but placing a substitution control at the location you would like to keep dynamic. This substitution control is initialized with a static callback method that is called to return a string to populate that portion of the page when a request is made, and it then retrieves the rest of the page from the cache. Figure 8-3 shows the general architecture of post-cache substitution. Figure 8-3. Post-cache substitutionThere are two ways to specify post-cache substitution. One way is to use the Response.WriteSubstitution method at the appropriate location in the response stream, which specifies a callback method that will be invoked whenever the page is requested (even when it is cached). Listing 8-18 shows an example of using WriteSubstitution to dynamically insert a substitution block into an output cached page. In this example, the entire page will be cached (in this case, the <h1> title and <h2> footer), but when a request comes in, it will invoke the GetServerTimeStamp method to populate the "substitution block" used as a placeholder in the cached page. Note that the Page class is never instantiated on subsequent requests when the page is cached, which is why the callback method must be static. It does have access to the HttpContext object, so it can use information from Request/Response/Session, and so on, but it cannot access elements of the Page itself. Listing 8-18. Using Response.WriteSubstitution
The second way to use post-cache substitution is to use the Substitution control. This control gives you a declarative way of accomplishing the same thing that Response.WriteSubstitution does, and it has the advantage of integrating into a page's declarative control layout. To use the control, place it in the page where you want the substitution block, assign it the static callback method name, and mark the page as output cached. Listing 8-19 shows an example of using the Substitution control. Listing 8-19. Using the Substitution control
Cache ProfilesCache profiles give you a way to define output cache parameters for collections of pages. Instead of hard-coding values in each of your pages scattered throughout your site, you can create output cache profiles and have each page draw its settings from the profile in your configuration file. This consolidates your cache settings, making it much easier to try out different caching strategies with minimal effort. Listings 8-20 and 8-21 show an example of specifying three different output cache profiles and how to reference a profile with an output cache directive. Listing 8-20. Using output cache profiles
Listing 8-21. Referencing an output cache profile
In addition to controlling output cache settings from the configuration file, you can also control some global settings associated with both the output cache and the data cache. For the data cache, you can completely disable memory collection and/or expiration, and you can also put a limit on the total percentage of physical memory used by the data cache. Output caching can be disabled or enabled at an application-wide scope, which is very useful for debugging purposes, as well as fixing a live server that has cache coherency problems. Listing 8-22 shows the various caching configuration file settings in use. Listing 8-22. Configuration file control over cache settings
General Performance EnhancementsIn addition to the performance-specific features discussed earlier, a significant amount of work was put into optimizing the core of ASP.NET as well in this release. Significant performance gains on 8-CPU multiprocessor machines, specifically, will be noticed on sites with this type of hardware. In general, the efficiency of the pipeline and the interaction between ASP.NET and HTTP.SYS has been optimized, achieving up to 30% performance improvements over ASP.NET 1.1 alone. Also improved is the startup time, as all of the system assemblies are now precompiled (using NGen) and no longer have to be JIT-compiled on first access. Finally, many memory optimizations were made, so the overall working set of the worker process will be lower. |