Caching


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 Caching

One 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

<asp:GridView  runat="server"               AutoGenerateColumns="True" DataKeyNames="au_id"               DataSource EnableViewState="false" /> <asp:SqlDataSource  runat="server"    EnableCaching="true" CacheDuration="3600"    ConnectionString="<%$ ConnectionStrings:pubs %>"    SelectCommand="SELECT au_id, au_lname, au_fname FROM authors" /> 

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.

Table 8-1. Elements of the cache key generated by the SqlDataSource control with sample values

Cache Key Element

Sample Value

Marker character "u"

u

Hash code of the data source control's Type object

91402740

Value of CacheDuration

3600

Value of CacheExpirationPolicy

0

Value of SqlCacheDependency

 

Value of ConnectionString

server=.;integrated security . . .

Value of SelectCommand

SELECT au_id, au_lname, . . .

Parameter values in xx=vv format separated by an "&"

 


ObjectDataSource Caching

If 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

public class Person {     private int _age;     private string _name;     private bool _isMarried;     private DateTime _birthDay;     public int Age     {         get { return _age; }         set { _age = value; }     }     public string Name     {         get { return _name; }         set { _name = value; }     }     public bool IsMarried     {         get { return _isMarried; }         set { _isMarried = value; }     }     public DateTime BirthDay     {         get { return _birthDay; }         set { _birthDay = value; }     }     public Person() { }     public Person(int age, string name, bool isMarried, DateTime birthDay)     {         _age = age;         _name = name;         _isMarried = isMarried;         _birthDay = birthDay;     } } public static class SampleData {     public static ICollection<Person> GetPeople()     {         List<Person> ret = new List<Person>();         for (int i = 0; i < 10; i++)             ret.Add(new Person(i + 20, "Person " + i.ToString(),                                (i % 2) == 0,                                DateTime.Now.AddYears(i-40)));         return ret;     } } 

Listing 8-3. Caching an object data source

<asp:GridView  runat="server"               DataSource               EnableViewState="False" /> <asp:ObjectDataSource  runat="server"                       CacheDuration="120"                       EnableCaching="True" SelectMethod="GetPeople"                       DataObjectTypeName="Person"                       TypeName="SampleData" /> 

Data Source Caching and ViewState

In 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 Dependencies

One 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 Dependencies

To 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

<system.web>   <!-- ... -->   <caching>     <sqlCacheDependency enabled="true" pollTime="3000">        <databases>           <add name="pubs" connectionStringName="pubs" />        </databases>      </sqlCacheDependency>   </caching> </system.web> 

Listing 8-5. Specifying a SQL Server 7/2000 cache dependency in a declarative data source

<asp:GridView  runat="server"               AutoGenerateColumns="True" DataKeyNames="au_id"               DataSource EnableViewState="false" /> <asp:SqlDataSource  runat="server"    EnableCaching="true" CacheDuration="3600"    SqlCacheDependency="pubs:authors"    ConnectionString="<%$ ConnectionStrings:pubsConnectionString %>"    SelectCommand="SELECT au_id, au_lname, au_fname FROM authors" /> 

Listing 8-6. Specifying a SQL Server 7/2000 cache dependency in an OutputCache directive

<%@ OutputCache Duration="3600" SqlDependency="pubs:authors"           VaryByParam="none" %> 

Listing 8-7. Specifying a SQL Server 7/2000 cache dependency in a manual cache insertion

string sql, dsn; dsn = ConfigurationManager.ConnectionStrings["pubs"].ConnectionString; sql = "SELECT au_id, au_lname, au_fname FROM Authors"; SqlDataAdapter da = new SqlDataAdapter(sql, dsn); DataSet authorsDs = new DataSet(); da.Fill(authorsDs); Cache.Insert("authors", authorsDs,              new SqlCacheDependency("pubs", "authors")); 

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/2000


SQL Server 2005 Cache Dependencies

SQL 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 2005


The 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

<%--Global.asax--%> <%@ Application Language="C#" %> <%@ Import Namespace="System.Data.SqlClient" %> <script runat="server">   void Application_Start(object sender, EventArgs e)   {     string dsn =          ConfigurationManager.ConnectionStrings["pubs"].ConnectionString;     SqlDependency.Start(dsn);   } </script> 

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

using (SqlConnection conn = new SqlConnection(dsn)) {   SqlCommand cmd = new SqlCommand(           "SELECT au_id, au_lname, au_fname FROM dbo.[Authors]", conn);   SqlCacheDependency scd = new SqlCacheDependency(cmd);   SqlDataAdapter da = new SqlDataAdapter(cmd);   DataSet authorsDs = new DataSet();   da.Fill(authorsDs, "authors");   Cache.Insert("au", authorsDs, scd); } 

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

<asp:GridView  runat="server"               AutoGenerateColumns="True" DataKeyNames="au_id"               DataSource EnableViewState="false" /> <asp:SqlDataSource  runat="server"    EnableCaching="true" CacheDuration="3600"    SqlCacheDependency="CommandNotification"    ConnectionString="<%$ ConnectionStrings:pubsConnectionString %>"    SelectCommand="SELECT au_id, au_lname, au_fname FROM dbo.[authors]" /> 

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

<%@ Page Language="C#" %> <%@ OutputCache Duration="120" SqlDependency="CommandNotification"                 VaryByParam="none" %> <html xmlns="http://www.w3.org/1999/xhtml" > <body>   <form  runat="server">   <div>     <asp:GridView  runat="server"          AutoGenerateColumns="True" DataKeyNames="au_id"          DataSource          EnableViewState="False" />     <asp:SqlDataSource  runat="server"             ConnectionString="<%$ ConnectionStrings:pubs %>"             SelectCommand="SELECT [au_id], [au_lname], [au_fname] FROM dbo.[authors]"  />     <asp:GridView  runat="server"            AutoGenerateColumns="True" DataKeyNames="pub_id"            DataSource            EnableViewState="False" />     <asp:SqlDataSource  runat="server"             ConnectionString="<%$ ConnectionStrings:pubs %>"             SelectCommand="SELECT [pub_id], [pub_name], [city] FROM dbo.[publishers]"  />   </div> </body> </html> 

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:

  • Verify that you have called SqlDependency.Start with each connection string you are using dependencies for (just once over the lifetime of the application, typically in Application_Start).

  • The database you are using in SQL Server 2005 must have the SQL service broker feature enabled. If it is not enabled, you should see an exception when you run a page that uses cached data, which indicates that the broker service is not enabled. To enable the service broker feature for a particular database, run the following command in the master database (replacing <dbname> with the name of your database):

    ALTER DATABASE <dbname> SET ENABLE_BROKER 

  • The identity used to access the database must have permissions to register for notifications. If you are using integrated authentication and running under IIS 6.0, this identity will be the local Network Service account, unless you have changed the identity of the Application Pool in which the application runs. To grant notification permissions to a particular identity in the database, you can use the following T-SQL command (replacing <username> with the name of the identity):

    GRANT SUBSCRIBE QUERY NOTIFICATIONS TO <username> 

  • The queries you use in the commands on which you want to receive notifications of change must be constrained in the following ways (these are the same set of restrictions in place when creating indexed views in SQL Server 2005).

    - Column names must be named explicitly (no "*" queries).

    - Table and user-defined function names must be referenced using their two-part names (schema.object), like dbo.Authors and Sales.SalesPerson.

    - Queries must not use any aggregation functions (like SUM, AVG, COUNT, and so on). One exception to this is the COUNT_BIG function, which is compatible (SUM is also allowed in some cases, but not always).

    - CLR-based user-defined functions must not access SQL Server (they can't perform SELECT and may not have EXTERNAL_ACCESS).

    - Queries must not use any windowing or ranking functions (which unfortunately includes the extremely useful ROW_NUMBER() function so commonly used in efficient paging queries).

    - Queries must not reference any temporary tables or views (again unfortunate, as using temporary tables is a common alternative mechanism for implementing paging in a query).

    - Queries can't include subqueries, outer joins, or self joins.

    - Queries can't return fields of type text, ntext, or image.

    - Queries can't use DISTINCT, HAVING, CONTAINS, or FREETEXT keywords.

    - Stored procedures are restricted to all of these same rules, but in addition they may not use the SET NOCOUNT ON statement.

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 Dependencies

If 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

namespace EssentialAspDotNet2.Performance {   public class WSDataDependency : CacheDependency   {     private string _wsUrl;     private int    _pollInterval;     private Timer  _timer;     public string WsUrl     {       get { return _wsUrl; }       set { _wsUrl = value; }     }     public int PollInterval     {       get { return _pollInterval; }       set { _pollInterval = value; }     }     public WSDataDependency()            : this("http://localhost/DataService/DataValidService.asmx",                    3000)     {     }     public WSDataDependency(string wsUrl, int pollInterval)     {       _wsUrl = wsUrl;       _pollInterval = pollInterval;       // Create timer and initiate poll       _timer = new Timer(new TimerCallback(OnTimerCallback),                          null, 0, _pollInterval);     }     public void OnTimerCallback(object state)     {         // Call IsValid method of Web service to check for data validity         // Call this.NotifyDependencyChanged(this, EventArgs.Empty)         // when change is detected         localhost.DataValidService dvs =                   new localhost.DataValidService();         dvs.Url = _wsUrl;         if (!dvs.IsValid())            NotifyDependencyChanged(this, EventArgs.Empty);     }   } } 

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

<%@ Page Language="C#" AutoEventWireup="true"          CodeFile="CustomDependency.aspx.cs" Inherits="CustomDependency" %> <html xmlns="http://www.w3.org/1999/xhtml" > <body>     <form  runat="server">     <div>     <h2>Custom dependency sample</h2>     The following timestamp is generated by a Web service call:     <br />     <asp:Label runat="server"  EnableViewState="false" />     </div>     </form> </body> </html> 

Listing 8-14. Sample use of the custom WSDataDependency class (codebehind)

public partial class CustomDependency : Page {   protected void Page_Load(object sender, EventArgs e)   {     DateTime data;     if (Cache["data"] == null)     {       localhost.DataService ds = new localhost.DataService();       data = ds.GetData();       WSDataDependency wsdd = new WSDataDependency(         ConfigurationManager.AppSettings["localhost.DataValidService"],            3000);       Cache.Insert("data", data, wsdd,                    DateTime.Now + new TimeSpan(0, 0, 60),                    Cache.NoSlidingExpiration);     }     else       data = (DateTime)Cache["data"];     _dataLabel.Text = data.ToLongTimeString();   } } 

Programmatic Fragment Caching

In 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

public sealed class ControlCachePolicy  {    public void SetExpires(DateTime expirationTime);    public void SetSlidingExpiration(bool useSlidingExpiration);    public void SetVaryByCustom(string varyByCustom);    public bool Cached { get; set; }    public CacheDependency Dependency { get; set; }    public TimeSpan Duration { get; set; }    public bool SupportsCaching { get; }    public string VaryByControl { get; set; }    public HttpCacheVaryByParams VaryByParams { get; }  } 

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

<%@ Control Language="C#" AutoEventWireup="true"     CodeFile="AuthorsControl.ascx.cs"  Inherits="AuthorsControl" %> <%@ OutputCache VaryByParam="none" Shared="true" Duration="120" %> <asp:Label runat="server"             EnableViewState="false" /> <hr /> <asp:GridView  runat="server"            AutoGenerateColumns="True" DataKeyNames="au_id"            DataSource EnableViewState="False" /> <asp:SqlDataSource  runat="server"     ConnectionString="<%$ ConnectionStrings:pubs %>"     SelectCommand="SELECT [au_id], [au_lname], [au_fname] FROM dbo.[authors]"  /> 

Listing 8-17. Output cached user control (codebehind)

public partial class AuthorsControl : System.Web.UI.UserControl {     protected void Page_Load(object sender, EventArgs e)     {         // Enable output caching programmatically on this control,         // setting the expiration for 1 minute from now         //         CachePolicy.Cached = true;        CachePolicy.SetExpires(DateTime.Now + new TimeSpan(0, 1, 0));         _timestampLabel.Text = "This control generated at " +                                DateTime.Now.ToLongTimeString();     } } 

Post-Cache Substitution

In 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 substitution


There 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

// within your page class public static string GetServerTimeStamp(HttpContext ctx) {   return DateTime.Now.ToLongTimeString(); } protected void Page_Load(object src, EventArgs e) {   Response.Write("<h1>title</h1>");   Response.WriteSubstitution(          new HttpResponseSubstitutionCallback(GetServerTimeStamp));   Response.Write("<h2>footer</h2>"); } 

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

<%@ Page Language="C#" %> <%@ OutputCache VaryByParam="none" Duration="120" %> <script runat="server"> public static string GetServerTimeStamp(HttpContext ctx) {   return DateTime.Now.ToLongTimeString(); } protected void Page_Load(object sender, EventArgs e) {   _timeStampLabel.Text = DateTime.Now.ToLongTimeString(); } </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server" /> <body>     <form  runat="server">     <div>        Substitution: <asp:Substitution                 runat="server" MethodName="GetServerTimeStamp" />        <br />    Static label: <asp:Label runat="server"  />        <br />     </div>     </form> </body> </html> 

Cache Profiles

Cache 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

<configuration>   <system.web>     <caching>       <outputCacheSettings>         <outputCacheProfiles>           <add name="Default" duration="60" varyByParam="user"/>           <add name="Aggressive" duration="3600" varyByParam="user"/>           <add name="Default" duration="60" varyByParam="user"/>         </outputCacheProfiles>       </outputCacheSettings>     </caching>   </system.web> </configuration> 

Listing 8-21. Referencing an output cache profile

<%@ OutputCache CacheProfile="Aggressive" %> ... 

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

<caching>   <cache disableMemoryCollection="false"          disableExpiration="false"          percentagePhysicalMemoryUsedLimit="90" />   <outputCache enabled="true"                enableFragmentCache="true"                sendCacheControlHeader="true"                omitVaryStar="false" /> </caching> 

General Performance Enhancements

In 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.




Essential ASP. NET 2.0
Essential ASP.NET 2.0
ISBN: 0321237706
EAN: 2147483647
Year: 2006
Pages: 104

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net