Caching





Caching

Do you have a page on your Web site that is built from the database, but the data changes only once a day? Perhaps you have a busy order page that must populate a list of states and countries out of the database several thousand times a day. How often do you need to add a new state? Clearly, these sorts of updates happen very infrequently. Are you hitting the limits of how far you can scale your database server? Are you looking for ways to reduce the load instead of spending $100,000 to upgrade the server? These are the types of scenarios where it makes sense to take a hard look at caching.

Output Caching: Caching an Entire Page

The simplest type of caching in ASP.NET is called output caching. Output caching takes the results from an ASP.NET page request and stores it in memory. Future requests for the same page are then served out of memory instead of being served by re-creating the page from scratch. (See Figure.) This can yield enormous savings in processor utilization on both the Web server and the database server, depending on the work the page performs. Page load times are decreased because after the first request, the page behaves as though it is a static HTML page.

Figure. Page requests are intercepted and served from cache.

graphics/04fig06.gif

Solutions are out there that compile ASP pages into static HTML. These solutions are great for producing CD-ROMs to send with salespeople, but they become a management nightmare when used for caching. The ideal cache solution will dynamically build the cached page when the first request is made and then refresh it in the cache when it gets too old to be useful. Output caching behaves in this fashion.

Turning on output caching requires a single directive to the top of any page that will be cached. See Listing 4.11 for an example of a very simple page that utilizes output caching. This page defines the OutputCache directive at the top to indicate that the page should be cached.

Listing 4.11 Page with Cache Directive to Cache for 5 Minutes—Cache5.aspx
<%@ OutputCache Duration="300" VaryByParam="None" %>
<html>
    <head>
        <title>Cached Time</title>
    </head>
    <body bgcolor="#FFFFFF" text="#000000">
        Time = <% = DateTime.Now %>
    </body>
</html>

Loading a page into cache and never refreshing it runs counter to one of the criteria for a good caching solution. The Duration attribute of the OutputCache directive is the solution. The Duration attribute indicates the number of seconds this page should remain in cache. In Listing 4.11, the 300 indicates that the page should stay in the cache for 300 seconds (that is, 5 minutes). This value does not appear to have an upper limit, so 31,536,000 seconds is a valid value, even though it represents a cache duration of a year!

The cache cooperates with the downstream client, such as proxy servers, browsers, or other caching devices located in the network, to manage the content that exists in the cache. By setting the Location attribute on the output cache header, you indicate where the cached copy of the page is allowed to exist. This attribute works by cooperating with the downstream clients. The first time ASP.NET returns a cached page to the client, it includes two new headers in the response: Cache-Control and Last-Modified. The Cache-Control directive is related to the Location attribute. If the Location attribute is left off the OutputCache directive, the Cache-Control is set to public. This flag marks the output as cacheable by any downstream client. If the Location attribute is set to Client, the Cache-Control directive is set to private. This setting indicates that the page in cache should not be stored in any public caching device such as a proxy server but may be stored in a cache private to the user—for example, the browser cache. With the Location directive set to Server, the Cache-Control header is set to no-cache, and an additional Pragma: no-cache header is added. This setting should prevent any downstream device that is following the HTTP 1.1 specification from caching the page. Instead, the page is cached on the server.

Location Cache-Control Description
Omitted or Any or Downstream public Any downstream client may cache.
Client private May not be stored in public caches, only private.
Server no-cache Caches content on the server.
None no-cache Cached nowhere.

After a page gets into the cache, how is it expired? This is where the Last-Modified header comes into play. If the Location attribute was set to Client, Any, or Downstream, the client checks to see if the cached content is still valid with the server. It does this by sending another GET request that includes the header If-Modified-Since. The date after the header is the date that was sent to the client with the Last-Modified header in the previous response. ASP.NET then takes a look at the page in cache. If the cached page is still valid, it returns an HTTP/1.1 304 Not Modified response. This response indicates to the client that it should use the version of the page that it has in its local cache. If the page has expired, the entire contents of the page are returned as part of an HTTP/1.1 200 OK response. However, if the Location is set to server, this conversation doesn't take place because the client doesn't have a cached copy of the page. Only the server does. From the client's perspective, the server is running the page each and every time. In Internet Explorer, the user can manually bypass this conversation by pressing Ctrl+F5 to refresh the page. The conversation is bypassed and the page is loaded directly from the server.

What type of a performance gain is expected with caching? On the first page request, no performance gain occurs; the page must always execute the first time around before caching. On subsequent page requests, however, response can be almost instantaneous versus the time required to execute. If the first page request takes 8 seconds and the subsequent requests take approximately 2 seconds, the application has realized a 25% performance gain. Extrapolating beyond the initial two requests, that percentage rapidly rises. As you can see, this technique can yield large performance gains. Figure shows a chart of cumulative request times for a cached versus a noncached page. The performance gain is quite evident.

Figure. Cached versus noncached cumulative request time.

graphics/04fig07.gif

Dealing with Browser Differences

Although caching yields huge performance gains, it can cause problems. Take, for example, an ASP page that detects browser types using the HttpBrowserCapabilities class. It is pretty common for applications to detect whether the page is going to be sent to Internet Explorer or Netscape and then send slightly different content to the client. Listing 4.12 shows a sample page that is cached and outputs the browser string. This page is cached for 5 minutes. If the first page hit comes from Internet Explorer, the page is customized for Internet Explorer. If subsequent hits to the page occur within 5 minutes, the cached page is served, still customized for Internet Explorer no matter what browser is used for those subsequent hits. Clearly wrong! ASP.NET has cached the page and even though the page contains browser-specific code, the same page is rendered to all clients.

Listing 4.12 Cached Page That Uses Browser Capabilities—CachedBrowser.aspx
<%@ OutputCache Duration="300" VaryByParam="None" %>
<html>
    <head>
        <title>Cached Browser Problem</title>
    </head>
    <body bgcolor="#FFFFFF" text="#000000">
        <%
            Response.Write("Browser: " + Request.Browser.Browser);
        %>
    </body>
</html>

The ASP.NET validation controls rely on this browser-sniffing capability to determine how to render the controls. If an application includes them in a page using output caching, it can display some unexpected behavior. Listing 4.13 shows a simple form that includes a RequiredFieldValidation control. This control relies on downloading some JavaScript on Internet Explorer but reverts to a server-side validation under Netscape. When this page is first hit from Internet Explorer, it displays correctly. However, subsequent hits to the cached page from Netscape yield a JavaScript error, which is not the intended result.

Listing 4.13 Cached Form that Uses RequiredFieldValidator—VaryBrowserBroken.aspx
<%@ OutputCache Duration="300" VaryByParam="None" %><html>
<head>
<title>Validate a Name - Broken</title>
</head>
<body bgcolor="#FFFFFF" text="#000000">
    <form id="frmName" runat=server>
        <asp:RequiredFieldValidator ControlToValidate="txtName"
             ErrorMessage="You must enter a name." runat=server /><br>
        Enter a name:<asp:textbox id="txtName" runat=server />
        <br><asp:button id="btnSubmit" Text="Validate" runat=server />
    </form>
</body>
</html>
</html>

The OutputCache directive has an additional parameter, VaryByCustom, to correct this problem. If you specify VaryByCustom="browser", ASP.NET creates a different cached copy of the page for each browser it detects. Modifying the example as shown in Listing 4.14 causes ASP.NET to create a different version of the page for Internet Explorer and Netscape, correcting the problem with the RequiredFieldValidator control.

Listing 4.14 Cached Form That Creates a Copy for Each Browser—VaryBrowserFixed.aspx
<%@ OutputCache Duration="300" VaryByParam="None" VaryByCustom="browser" %><html>
<head>
<title>Validate a Name - Fixed</title>
</head>

<body bgcolor="#FFFFFF" text="#000000">
    <form id="frmName" runat=server>
        <asp:RequiredFieldValidator ControlToValidate="txtName"
             ErrorMessage="You must enter a name." runat=server /><br>
        Enter a name:<asp:textbox id="txtName" runat=server />
        <br><asp:button id="btnSubmit" Text="Validate" runat=server />
    </form>
</body>
</html>

Dealing with Other Differences

VaryByCustom can behave like a specialized form of another attribute available as part of the OutputCache directive, VaryByHeader. VaryByHeader enables you to specify that a new page should be created in cache for each new value of an HTTP header that is included in the request. VaryByHeader="User-Agent" performs a similar function as VaryByCustom="browser". One difference is how many cached pages they will create. VaryByHeader will create one for each unique User-Agent. Since the User-Agent header includes a number of items, such as operating system, you could end up with multiple cached pages just for Internet Explorer 5.0. VaryByCustom, however, looks at Request.Browser.Type and the major version number only. VaryByCustom also allows you to override the GetVaryByCustomString() to allow you to customize the behavior of GetVaryByCustom to follow rules you define. In the GetVaryByCustom(), you essentially create the key that is used to uniquely identify each cached page. VaryByHeader allows the application to do additional things, such as create a cached copy of the page for each host header. Listing 4.15 shows a page that creates a cached copy of the page for each request that uses a different host header.

Listing 4.15 Creates a Cached Page for Each Page Header—CachedHost.aspx
<%@ OutputCache Duration="300" VaryByParam="None" VaryByHeader="Host" %>
<html>
<head>
<title>Cached Host</title>
</head>
<body bgcolor="#FFFFFF" text="#000000">
    TimeStamp = <% = DateTime.Now %>
</body>
</html>

Caching the Result of a Form

What happens when the output on a cached page changes based on arguments in the command line or input from a form? In this case, nothing is in the headers to differentiate the requests, so VaryByHeader will detect no differences in the page requests. The VaryByCustom attribute might help if the application implements a custom algorithm. Listing 4.16 shows a page that exhibits this problem. This page queries the Northwind database and looks up employees by last name. When output caching is added to this page, anything typed into the last name box always gets the cached page!

Listing 4.16 Page That Caches Form Output Incorrectly—CachedSearchBroken.aspx
<%@ Page Language="C#" %>
<%@ OutputCache Duration="300" VaryByParam="None" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>

<script runat="server">
    
    void Page_Load(Object sender, EventArgs e)
    {
        if(Request["txtLastName"] != null)
        {
            SqlConnection cn;
            SqlCommand cmd;
            SqlDataReader dr;
            StringBuilder sb;

            // Open the database
            cn = new SqlConnection("server=localhost;uid=sa;pwd=;database= Northwind;");
            cn.Open();
            // Search the database
            cmd = new SqlCommand("SELECT * FROM Employees WHERE LastName LIKE '" + Request["txtLastName"].ToString().Trim() + "%'", cn);
            dr = cmd.ExecuteReader();
            // Start building the output table
            sb = new StringBuilder("<table><tr><th>LastName</th><th>Phone</th></tr>");

            while(dr.Read())
            {
                sb.Append("<tr><td>");
                sb.Append(dr["LastName"].ToString());
                sb.Append("</td><td>");
                sb.Append(dr["HomePhone"].ToString());
                sb.Append("</td></tr>");
            }

            sb.Append("</table>");

            output.InnerHtml = sb.ToString();

            dr.Close();
            cn.Close();
        }
    }
</script>

<html>
    <head>
        <title>Cached Search - Broken</title>
    </head>
    <body bgcolor="#FFFFFF" text="#000000">
        <form method="post" id="Search">
            Last Name:
            <input type="text" name="txtLastName">
            <input type="submit">
        </form>
        <hr>
        <span id="output" runat="server"></span>
    </body>
</html>

Wanting to cache the output of a form is such a common thing that another attribute is provided on the OutputCache directive, called VaryByParam. VaryByParam allows the application to specify a parameter in a GET request so that a separate copy of the cached page will be kept for each combination of the parameters specified.

Modifying the previous example to include the new attribute yields the code in Listing 4.17. When you specify the VaryByParam attribute and indicate txtLastName, ASP.NET creates a cached copy of the page for each txtLastName typed in.

Listing 4.17 Page That Correctly Caches Results from a Form GET—CachedSearchFixed.aspx
<%@ Page Language="C#" %>
<%@ OutputCache Duration="300" VaryByParam="txtLastName" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>

<script runat="server">
    void Page_Load(Object sender, EventArgs e)
    {
        if(Request["txtLastName"] != null)
        {
            SqlConnection cn;
            SqlCommand cmd;
            SqlDataReader dr;
            StringBuilder sb;

            // Open the database
            cn = new SqlConnection("server=localhost;uid=sa;pwd=;database= Northwind;");
            cn.Open();

            // Search the database
            cmd = new SqlCommand("SELECT * FROM Employees WHERE LastName LIKE '" + Request["txtLastName"].ToString().Trim() + "%'", cn);
            dr = cmd.ExecuteReader();
            // Start building the output table
            sb = new StringBuilder("<table><tr><th>LastName</th><th>Phone</th></tr>");

            while(dr.Read())
            {
                sb.Append("<tr><td>");
                sb.Append(dr["LastName"].ToString());
                sb.Append("</td><td>");
                sb.Append(dr["HomePhone"].ToString());
                sb.Append("</td></tr>");
            }

            sb.Append("</table>");

            output.InnerHtml = sb.ToString();

            dr.Close();
            cn.Close();


        }
    }

</script>

<html>
    <head>
        <title>Cached Search - Broken</title>
    </head>
    <body bgcolor="#FFFFFF" text="#000000">
        <form method="post" id="Search">
            Last Name:
            <input type="text" name="txtLastName">
            <input type="submit">
        </form>
        <hr>
        <span id="output" runat="server"></span>
    </body>
</html>

Caching Part of a Page

What happens when you have mixed types of content on the same page? The OutputCache directive affects the entire page and doesn't allow the application to specify regions on the page to cache or to exclude from the cache. Figure shows a diagram of a page containing two sections, which are outlined. The first section contains data that changes constantly. The second section contains data that changes very infrequently. You can probably think of many examples of pages like this.

Figure. A page with two regions that need to be cached differently. The sales data is updated in real time, whereas the phone list needs to be updated only once a day.

graphics/04fig08.jpg

The solution to this issue is to take the data that needs to be cached and place it in a user control. A user control is allowed to define its own OutputCache directive and thereby affect its cache lifetime independent of the page as a whole.

When talking about caching user controls, we are speaking only about Location="Server" style caching. Because the user control ultimately becomes part of the page, it cannot cache itself in any downstream devices. Instead, when the server inserts the user control into the page at runtime, it grabs a copy of the output from the page cache and inserts it into the page. Listing 4.18 shows a user control using the OutputCache directive, and Listing 4.19 shows a page that consumes the user control.

Listing 4.18 A User Control That Generates a Phone List—SalesPeople.ascx
<%@ Control Language="C#" %>
<%@ OutputCache duration="300" VaryByParam="None" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<B>Sales Phone List</B>
<BR>
<%
            SqlConnection cn;
            SqlCommand cmd;
            SqlDataReader dr;

            // Open the database
            cn = new SqlConnection("server=localhost;uid=sa;pwd=;database= Northwind;");
            cn.Open();

            // Search the database
            cmd = new SqlCommand("SELECT * FROM Employees", cn);
            dr = cmd.ExecuteReader();

            // Start building the output table
            Response.Write("<table border=1><tr><th>LastName</th><th>Phone</th></tr>");
            // Build the body of the table
            while(dr.Read())
                Response.Write("<tr><td>" + dr["LastName"].ToString() +
                     "</td><td>" + dr["HomePhone"].ToString() + "</td></tr>");
            // finish the table
            Response.Write("</table>");

            dr.Close();
            cn.Close();
%>
<br>
<i>Updated:<%=DateTime.Now%></i>
Listing 4.19 A Page That Utilizes the SalesPeople.ascx User Control—SalesByEmployee.aspx
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Register TagPrefix="ASPBOOK" TagName="SalesPhoneList" Src="SalesPeople.ascx" %>

<script runat="server">
    void Page_Load(Object sender, EventArgs e)
    {
        SqlConnection cn;
        SqlCommand cmd;
        SqlDataReader dr;
        StringBuilder sb;

        // Open a connection
        cn = new SqlConnection("server=localhost;uid=sa;pwd=;database=Northwind");
        cn.Open();
        // Create an adapter
        cmd = new SqlCommand("select LastName, sum(UnitPrice * Quantity) Total from Employees e, Orders o, [Order Details] d WHERE e.EmployeeID = o.EmployeeID AND o.OrderID = d.OrderID GROUP BY LastName ORDER BY Total DESC", cn);
        dr = cmd.ExecuteReader();

        // Start building the output table
        sb = new StringBuilder("<table><tr><th>LastName</th><th>Sales</th></tr>");

        while(dr.Read())
        {
            sb.Append("<tr><td>");
            sb.Append(dr["LastName"].ToString());
            sb.Append("</td><td>");
            sb.Append(dr["Total"].ToString());
            sb.Append("</td></tr>");
        }

        sb.Append("</table>");

        SalesPeople.InnerHtml = sb.ToString();

        dr.Close();
        cn.Close();

    }
</script>

<html>
    <head>
        <title>Sales By Employee</title>
    </head>
    <body text="#000000" bgcolor="#ffffff">
        <h1>Sales By Employee</h1>
        <table
             width="75%"
             border="0">
            <tbody>
                <tr>
                    <td>
                        <span id="SalesPeople" runat="server"></span>
                    </td>
                    <td valign="top">
                        <aspbook:SalesPhoneList id="UserControl1" runat="server">
                        </aspbook:SalesPhoneList>
                    </td>
                </tr>
            </tbody>
        </table>
    </body>
</html>

All the attributes for the OutputCache directive, with the exception of Location, will work inside a user control. VaryByParam can be very useful if the user control relies on form or request parameters to create its content. ASP.NET will create a cached copy of the user control for each combination of parameters in the VaryByParam attribute. Listing 4.20 shows a user control that takes the value of a request parameter and saves it into an ArrayList to create a crumb trail of previous search terms. This user control has the OutputCache directive set to VaryByParam="txtLastName". This is the search term on the parent page, which is shown in Listing 4.21.

Listing 4.20 User Control That Creates a Breadcrumb Trail of Previous Search Terms—LastFiveSearchTerms.ascx
<%@ Control Language="C#" %>
<%@ OutputCache duration="60" VaryByParam="txtLastName" %>
<%
    ArrayList al;

    // Get the list from the session
    al = (ArrayList)Session["alSearchTerms"];

    // Did we get it
    if(al == null)
        al = new ArrayList();

    // Add the item to the array
    al.Add(Request["txtLastName"]);
    // Store the array
    Session["alSearchTerms"] = al;

    Response.Write("<b>Past Terms:</b><br><table border=1>");
    for(int iRow = 0;iRow<=(al.Count - 1);iRow++)
        {
        Response.Write("<tr><td>");
        Response.Write(al[iRow]);
        Response.Write("</td></tr>");
        }
    Response.Write("</table>");
%>
Listing 4.21 Search Page That Includes Breadcrumb Control—BreadCrumbSearch.aspx
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Register TagPrefix="ASPBOOK" TagName="LastFiveSearchItems"  Src="LastFiveSearch.ascx" %>

<script runat="server">
   void Page_Load(Object sender, EventArgs e)
           {
               if(Request["txtLastName"] != null)
                   {
                   SqlConnection cn;
                   SqlCommand cmd;
                   SqlDataReader dr;
                   StringBuilder sb;

                   // Open the database
                   cn = new SqlConnection("server=localhost;uid=sa;pwd=;database=Northwind;");
                   cn.Open();

                   // Search the database
                   cmd = new SqlCommand("SELECT * FROM Employees WHERE LastName LIKE '" + Request["txtLastName"].ToString().Trim() + "%'", cn);
                   dr = cmd.ExecuteReader();

                   // Start building the output table
                   sb = new StringBuilder(@"
   <table>
       <tr>
           <th>
               LastName
           </th>
           <th>
               Phone
           </th>
       </tr>
       ");

                           while(dr.Read())
                               {
                                   sb.Append(@"
       <tr>
           <td>
               ");
                                                   sb.Append(dr["LastName"].

ToString());
                                                   sb.Append(@"
           </td>
           <td>
               ");
                                                   sb.Append(dr["HomePhone"]. ToString());
                                                   sb.Append(@"
           </td>
       </tr>
       ");
                               }

                           sb.Append(@"
   </table>
   ");

                   output.InnerHtml = sb.ToString();

                   dr.Close();
                   cn.Close();


                   }
           }
</script>


<html>
    <head>
        <title>Cache Search - Fixed</title>

    </head>
    <body text="#000000" bgcolor="#ffffff">
        <form id="Search" method="post">
            Last Name:
            <input type="text" name="txtLastName">
            <input type="submit" value="Submit Query">
        </form>
        <hr>
        <span id="output" runat="server"></span>
        <hr>
        <aspbook:lastfivesearchitems id="UserControl1" runat="server">
        </aspbook:lastfivesearchitems>
    </body>
</html>

Some combinations of user control and page caching can yield unexpected results. We have shown examples where the containing page does not have a cache directive but the user control does, and examples where both have cache directives. What about the case where the containing page has a cache directive but the user control does not? In this case, the entire page is cached, including the user control, and delivered to the client. A user control cannot override the caching behavior of its container. Figure shows the valid and invalid combinations.

Figure. The permitted combinations for user control and output caching.

graphics/04fig09.gif

If the container is cached, the control will also be cached. Listings 4.22 and 4.23 show examples of this behavior. The containing page in Listing 4.22 defines a cache directive. The user control in Listing 4.23 does not; therefore, the entire page is cached.

Listing 4.22 Containing Page Uses OutputCacheContainerPage.aspx
<%@ Page Language="C#" %>
<%@ Register TagPrefix="ASPBOOK" TagName="Inside" Src="Inside.ascx" %>

<script runat="server">

</script>

<%@ OutputCache Duration="300" VaryByParam="None" %>
<html>
    <head>
        <title>Container Page</title>
    </head>
    <body text="#000000" bgcolor="#ffffff">
        Page TimeStamp = <% = DateTime.Now %>
        <hr>
        <aspbook:inside id="UserControl1" runat="server">
        </aspbook:inside>
        <hr>
    </body>
</html>
Listing 4.23 User Control Does Not Use OutputCacheInside.ascx
<%@ Control Language="C#" %>
User Control TimeStamp = <% = DateTime.Now %>

If you need more control over caching behavior than the OutputCache directive provides, it is time to take a look at the HttpCachePolicy class. This class allows you to dynamically set any of the properties contained in the OutputCache directive.

Caching a Database Query

Caching entire pages is simple—just add the OutputCache directive. Let's take a look at a different caching technique that is a little more complicated. Unlike the previous technique, this one is likely to require changes to your source code.

Output caching eliminates two factors that contribute to the amount of time it takes to load a page: code execution and query execution. What if you still want the code on the page to execute, but you want to maintain the advantage of eliminating query execution? ASP.NET provides a group of caching classes that can be used to cache frequently used resources such as the results from a query on a server.

You may wonder, why this new Cache object? Why not just use the Application or Session objects? The Application or Session objects make good sense for settings that must persist for the life of the application or for the life of the currently connected user. They make much less sense to cache the output of a commonly run query. If the output of a query goes into the application object, it remains there until the application shuts down. With a large number of queries, what happens as memory gets low?

The cache class deals with each of these issues and more. The visibility of items in the cache is very similar to the visibility of items in the application object. The cache is private to each application root but is shared among all users. The cache can be treated like a dictionary using keys paired with values, just like the application object. The similarities end there.

If an object is placed in the cache and isn't used for a while, the cache is smart enough to drop it out of memory to conserve valuable server resources. This process, called scavenging, takes into account when the object was last accessed, as well as the priority assigned to the object when determining which objects to drop from the cache.

Let's take a look at a simple example. Listing 4.24 shows a page that queries the Northwind database for a list of territories and places them into a drop-down list box.

Listing 4.24 Page That Queries Northwind for a List of Territories—CacheTerritories.aspx
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>

<script runat="server">

    void Page_Load(Object sender, EventArgs e)
    {
        SqlConnection cn;
        SqlDataAdapter cmd;
        DataSet ds;

        // Open a connection
        cn = new SqlConnection("server=localhost;uid=sa;pwd=;database=Northwind");
        // Create an adapter
        cmd = new SqlDataAdapter("select * from territories", cn);

        // Fill the dataset
        ds = new DataSet();
        cmd.Fill(ds, "Territories");

        // Bind the data
        lstTerritory.DataSource = ds.Tables[0].DefaultView;
        lstTerritory.DataBind();


    }
</script>
<html>
    <head>
        <title>Territories</title>
    </head>
    <body text="#000000" bgcolor="#ffffff">
        <form id="Territory" runat="server">
            Territory:
            <asp:dropdownlist id="lstTerritory" runat="server" datatextfield= "TerritoryDescription" datavaluefield="TerritoryID">
            </asp:dropdownlist>
        </form>
    </body>
</html>

The list of territories rarely changes, so it is an ideal candidate to place into the cache. When using cached objects, a familiar design pattern emerges—check the cache before creating an object. Instead of immediately building the object of interest, in this case a DataSet, the application first attempts to retrieve it from the cache. Next, it checks the reference to see if anything was successfully retrieved from the cache. If the reference is null, it does the work required to create the object from scratch and saves the newly created object into the cache. Finally, the application does the work to create the page.

Listing 4.25 takes the previous example and modifies it to store the list of territories in cache. It first attempts to retrieve the DataSet containing the territories from cache. If this fails, it will then connect to the database, rebuild the dataset from scratch, and save it into the cache. Finally, the drop-down list box is bound to the dataset.

Listing 4.25 Modification to Store DataSet Containing Territories in Cache—CacheExpiration.aspx
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>

<script runat="server">
    void Page_Load(Object sender, EventArgs e)
    {
        DataSet ds;
        // Attempt to retrieve the dataset from cache
        ds = (DataSet)Cache["Territories"];

        // Did we get it
        if(ds == null)
        {
            SqlConnection cn;
            SqlDataAdapter cmd;

            // Open a connection
            cn = new SqlConnection("server=localhost;uid=sa;pwd=;database= Northwind");
            // Create an adapter
            cmd = new SqlDataAdapter("select * from territories", cn);

            // Fill the dataset
            ds = new DataSet();
            cmd.Fill(ds, "Territories");

            // Cache the dataset
            Cache["Territories"] = ds;

            // Indicate we didn't use the cache
            Message.Text = "Territories loaded from database." + DateTime.Now;
        }

        // Bind the data
        lstTerritory.DataSource = ds.Tables[0].DefaultView;
        lstTerritory.DataBind();
    }

</script>

<html>
    <head>
        <title>Territories Sliding Expiration</title>
    </head>
    <body text="#000000" bgcolor="#ffffff">
        <form id="Territory" runat="server">
            Territory:
            <asp:dropdownlist id="lstTerritory" runat="server" datatextfield= "TerritoryDescription" datavaluefield="TerritoryID">
            </asp:dropdownlist>
        </form>
        <asp:label id="Message" runat="server" />
    </body>
</html>

This technique is not limited to ADO.NET objects. Any object can be placed into the cache and utilized in the same fashion. The cache does consume resources, however, so carefully consider what is placed into it. Candidates for caching are items that can be re-created when needed but whose creation extracts a performance penalty.

Expiring Items from the Cache

The cache is not intended to be a shared memory space for communication between pages. Using the cache as a shared memory space, an application can run into the same types of problems as attempting to use the Application object as a communication mechanism. Objects placed in the cache should be treated as read-only and updated only when they expire.

So how does expiration work? To use expiration, items must be placed into the cache using Cache.Insert or Cache.Add instead of treating the cache like a dictionary. Insert and Add differ only in that Add returns a reference to the cached item and Insert has a few more overloads to control the life of the item in the cache.

Expiring Cached Items Based on Time

Two options are available for expiring an item from the cache: AbsoluteExpiration and SlidingExpiration. AbsoluteExpiration specifies the date and time when an item should expire from the cache. When that date and time is reached, the item is dropped. This may not always be the appropriate action. In most instances it would be better to drop an item from cache if it has not been used for a certain amount of time. With this method, frequently used items would remain in cache while less frequently used items would eventually drop out of the cache. The SlidingExpiration argument of Insert provides this type of functionality. SlidingExpiration also takes a time span, but in this case, the time is measured from the last time the item was accessed, not from the time that the item was placed in the cache. Behind the scenes, this method sets the AbsoluteExpiration value equal to Now + SlidingExpiration. Each time the item is accessed, this "sliding" process is repeated, moving the AbsoluteExpiration further into the future. So on initial load, a page can put a dataset into cache with an expiration of 10 minutes. As long as that page is accessed at least once every 10 minutes, the object will remain in cache indefinitely. If no more requests for the page are made, the dataset will drop out of cache 10 minutes after the last request.

When both a SlidingExpiration and an AbsoluteExpiration are specified, the SlidingExpiration will overwrite the AbsoluteExpiration with Now + SlidingExpiration. To specify only one or the other, special constants are made available to provide a default setting for the unspecified method. Cache.NoAbsoluteExpiration is used for the AbsoluteExpiration parameter only when specifying SlidingExpiration. Cache.NoSlidingExpiration is used for the SlidingExpiration parameter only when specifying the AbsoluteExpiration parameter. The SlidingExpiration parameter of the Cache object is very similar in behavior to the Duration attribute of the OutputCache directive. Cache.NoAbsoluteExpiration is equal to DateTime.MaxValue and Cache.NoSlidingExpiration is equal to TimeSpan.Zero.

Taking the earlier example from Listing 4.25 and updating it to use a SlidingExpiration of 60 seconds, we get the code shown in Listing 4.26. A message will be printed at the bottom of the screen whenever the DataSet is refreshed from the database. If this page is refreshed at least once every 60 seconds, the results will always be returned from cache. Wait 61 seconds and then refresh it, and the results will be reloaded from the database.

Listing 4.26 Example Updated to Use a SlidingExpiration of 60 Seconds—SlidingExpiration.aspx
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>

<script runat="server">
    void Page_Load(Object sender, EventArgs e)
    {
        DataSet ds;

        // Attempt to retrieve the dataset from cache
        ds = (DataSet)Cache["Territories"];

        // Did we get it
        if(ds == null)
        {
            SqlConnection cn;
            SqlDataAdapter cmd;

            // Open a connection
            cn = new SqlConnection("server=localhost;uid=sa;pwd=;database= Northwind");
            // Create an adapter
            cmd = new SqlDataAdapter("select * from territories", cn);

            // Fill the dataset
            ds = new DataSet();
            cmd.Fill(ds, "Territories");

            // Cache the dataset
            Cache.Insert("Territories", ds, null, Cache.NoAbsoluteExpiration, TimeSpan.FromSeconds(60));

            // Indicate we didn't use the cache
            Message.Text = "Territories loaded from database." + DateTime.Now;
        }

        // Bind the data
        lstTerritory.DataSource = ds.Tables[0].DefaultView;
        lstTerritory.DataBind();
    }
</script>

<html>
    <head>
        <title>Territories Sliding Expiration</title>
    </head>
    <body text="#000000" bgcolor="#ffffff">
        <form id="Territory" runat="server">
            Territory:
            <asp:dropdownlist id="lstTerritory" runat="server" datatextfield= "TerritoryDescription" datavaluefield="TerritoryID">
            </asp:dropdownlist>
        </form>
        <asp:label id="Message" runat="server" />
    </body>
</html>
Removing Cached Items Based on Priority

The Cache object has a limit on the number of items it will accept. Cache.MaxItems is used to obtain or set this limit. Cache.Count can be used to determine the number of items currently in the cache. When a Cache.Insert is performed and the Cache.Count = Cache.MaxItems, the cache must remove an item before inserting the new item. When removing items, the cache looks at the CacheItemPriority of each item. Items with a lower CacheItemPriority are removed before items with a higher CacheItemPriority. Unless specified, all items inserted carry a Normal priority. Other priorities can be assigned using an overloaded form of Cache.Insert or Cache.Add. It makes sense to give a higher priority to items that take longer to create and a lower priority to items that can be quickly re-created. In this fashion, the maximum benefit is achieved with the cache.

Notifying the Next of Kin

When an item is dropped out of cache, a notification can be fired to let your program know that the item has been removed from cache. The callback routine can then pre-emptively re-add the item back into the cache before the page is requested again. This prevents the first page that attempts to use the item after it has been dropped out of cache from experiencing a delay. Listing 4.27 shows how to use another variant of the Insert() method to specify a callback that should be invoked when the item is removed from cache.

Listing 4.27 Page That Caches a List of Territories; When the List Is Dropped Out of Cache, It Is Replaced Proactively—CacheCallback.aspx
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>

<script runat="server">

    void Page_Load(Object sender, EventArgs e)
    {
        DataSet ds;

        // Attempt to retrieve the dataset
        ds = (DataSet)Cache["Territories"];

        // Did we get it
        if(ds == null)
        {
            // Cache the territories
            ds = CacheTerritories();
            // Indicate we did not use the cache
            Message.Text = "Territories loaded from database. " + DateTime.Now;
        }
        // Check for the callback
        if(Cache["Callback"] != null)
            Message.Text = (string)Cache["Callback"];

        // Bind the data
        lstTerritory.DataSource = ds.Tables[0].DefaultView;
        lstTerritory.DataBind();
    }

    public void CacheItemRemoved(string strKey, Object oValue,  CacheItemRemovedReason reason)
    {
        Cache["Callback"] = "Recached at " + DateTime.Now;

        // Recache the territories
        CacheTerritories();
    }

    public DataSet CacheTerritories()
    {
        SqlConnection cn;
        SqlDataAdapter cmd;
        DataSet ds;

        // Open a connection
        cn = new SqlConnection("server=localhost;uid=sa;pwd=;database=Northwind");
        // Create an adapter
        cmd = new SqlDataAdapter("select * from territories", cn);

        // Fill the dataset
        ds = new DataSet();
        cmd.Fill(ds, "Territories");

        // Cache the dataset
        Cache.Insert("Territories", ds, null, Cache.NoAbsoluteExpiration,  TimeSpan.FromSeconds(60), CacheItemPriority.Normal, new CacheItemRemovedCallback(this.CacheItemRemoved));

        return ds;
    }
</script>

<html>
    <head>
        <title>Territories Sliding Expiration</title>
    </head>
    <body text="#000000" bgcolor="#ffffff">
        <form id="Territory" runat="server">
            Territory:
            <asp:dropdownlist id="lstTerritory" runat="server" datatextfield= "TerritoryDescription" datavaluefield="TerritoryID">
            </asp:dropdownlist>
        </form>
        <asp:label id="Message" runat="server" />
    </body>
</html>

Cache Dependencies

Sometimes items that you place into the cache may need to be refreshed because of an external event other than the passage of time. Cache dependencies allow a cache item to be based on a file, a directory, or the key of another item in the cache. Listing 4.28 shows a new version of the Territory example in which the list of territories is placed in the cache until the page is changed, at which time the results are recached.

Listing 4.28 Page That Caches a List of Territories Until the Page Is Changed—CacheDependency.aspx
<%@ Page Language="C#" %>

<script runat="server">
    void Page_Load(Object sender, EventArgs e)
    {
        DataSet ds;

        // Attempt to retrieve the dataset from cache
        ds = (DataSet)Cache["Territories"];

        // Did we get it
        if(ds == null)
        {
            SqlConnection cn;
            SqlDataAdapter cmd;

            // Open a connection
            cn = new SqlConnection("server=localhost;uid=sa;pwd=;database= Northwind");
            // Create an adapter
            cmd = new SqlDataAdapter("select * from territories", cn);

            // Fill the dataset
            ds = new DataSet();
            cmd.Fill(ds, "Territories");

            // Cache the dataset
            Cache.Insert("Territories", ds, new CacheDependency(Server.MapPath("CacheDependency.aspx")));

            // Indicate we didn't use the cache
            Message.Text = "Territories loaded from database." + DateTime.Now;
        }

        // Bind the data
        lstTerritory.DataSource = ds.Tables[0].DefaultView;
        lstTerritory.DataBind();
    }
</script>

<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<html>
    <head>
        <title>Territories</title>
    </head>
    <body text="#000000" bgcolor="#ffffff">
        <form id="Territory" runat="server">
            Territory:
            <asp:dropdownlist id="lstTerritory" runat="server" datatextfield= "TerritoryDescription" datavaluefield="TerritoryID">
            </asp:dropdownlist>
        </form>
        <asp:label id="Message" runat="server" />
    </body>
</html>

Listing 4.29 shows an example that pulls together all these cache concepts. This code adds items to the cache, specifying an AbsoluteExpiration, SlidingExpiration, Priority, and a Cache Dependency. Items may also be removed from the cache. All items currently in the cache are displayed. Iterating the items in the cache to display their data type resets their sliding expiration. If you would like to see the results of the sliding expiration, you must wait the requisite amount of time before refreshing the page. When running this sample, you will notice that quite a few items in the cache start with System.Web. ASP.NET uses the cache to store a number of items during execution. Be aware that this raises the possibility of namespace collisions, so name your cache items accordingly.

Listing 4.29 An Example of Removing and Inserting Items from Cache with Expirations—WorkingWithCache.aspx
<%@ Page Language="C#" %>

<script runat="server">
   void ListCache()
   {
      StringBuilder sbOutput;
      string szType;

      // Show the number of items in the cache
     sbOutput = new StringBuilder("Items in Cache = " + Cache.Count.ToString() + "<Br>");

     // Start the table
     sbOutput.Append("<table border=1><tr><th>Key</th><th>Data Type</th></tr>");

     // Clear the list
     lstCache.Items.Clear();

     // Loop through the cache
     foreach(System.Collections.DictionaryEntry oItem in Cache)
         {
         // Add To List
         lstCache.Items.Add(oItem.Key.ToString());
         // Have to watch for a null object during expiration
         if(Cache[oItem.Key.ToString()] == null)
             szType = "Nothing";
         else
             szType = Cache[oItem.Key.ToString()].GetType().ToString();

         // Add to table
         sbOutput.Append("<tr><td>" + oItem.Key + "</td><td>" + szType + "</td></tr>");
         }

     // Close the table
     sbOutput.Append("</table>");

     // Place in page
     tblCache.InnerHtml = sbOutput.ToString();
   }

   void Page_Load(Object sender, EventArgs e)
   {
      // Only run it if no other event is happening
      if(!IsPostBack)
         ListCache();
   }

   void btnRemove_Click(Object sender, EventArgs e)
   {
      // Remove the item
      Cache.Remove(lstCache.SelectedItem.Text);
      // Re list the cache
      ListCache();
   }

   void btnAdd_Click(Object sender, EventArgs e)
   {
      CacheItemPriority p = CacheItemPriority.NotRemovable;
      switch(ddPriority.SelectedItem.Text)
      {
         case "High":
            p = CacheItemPriority.High;
            break;
         case "Above Normal":
            p = CacheItemPriority.AboveNormal;
            break;
         case "Normal":
             p = CacheItemPriority.Normal;
             break;
         case "Below Normal":
             p = CacheItemPriority.BelowNormal;
             break;
         case "Low":
             p = CacheItemPriority.Low;
             break;
         case "Not Removable":
             p = CacheItemPriority.NotRemovable;
             break;
      }


      if(txtSliding.Text != "")
      {
         // Save the value with a sliding expiration
         Cache.Insert(txtKey.Text, txtValue.Text, null, Cache.NoAbsoluteExpiration, TimeSpan.FromSeconds(Double.Parse(txtSliding.Text)), p, null);
      }
      else
         if(txtAbsolute.Text != "")
         {
            // Save the value with an absolute expiration
            Cache.Insert(txtKey.Text, txtValue.Text, null, DateTime.Now. AddSeconds(Double.Parse(txtAbsolute.Text)), Cache.NoSlidingExpiration, p, null);
         }
         else
         {
            // Save the value
            Cache.Insert(txtKey.Text, ((object)txtValue.Text), null, DateTime. MinValue, TimeSpan.Zero, p, null);
         }

      // List out the cache
      ListCache();
   }
</script>

<html>
<head>
<title>Working with the Cache</title>
</head>

<body bgcolor="#FFFFFF" text="#000000">
    <form id="CacheWork" runat=server>
        <table>
            <tr>
                <td>Key:</td>
                <td><asp:textbox id="txtKey" runat=server /></td>
                <td>Value: </td>
                <td><asp:textbox id="txtValue" runat=server /></td>
                <td><asp:button id="btnAdd" Text="Add" runat=server
                  OnClick="btnAdd_Click" /></td>
            </tr>
            <tr>
                <td>Absolute:</td>
                <td><asp:textbox id="txtAbsolute" runat=server /></td>
                <td>Sliding:</td>
                <td><asp:textbox id="txtSliding" runat=server /></td>
                <td>&nbsp;</td>
            </tr>
            <tr>
                <td>Priority:</td>
                <td><asp:dropdownlist id="ddPriority" runat=server>
                        <asp:listitem>High</asp:listitem>
                        <asp:listitem>Above Normal</asp:listitem>
                        <asp:listitem Selected="true">Normal</asp:listitem>
                        <asp:listitem>Below Normal</asp:listitem>
                        <asp:listitem>Low</asp:listitem>
                        <asp:listitem>Not Removable</asp:listitem>
                    </asp:dropdownlist></td>
                <td colspan=3>&nbsp;</td>
            </tr>
        </table>
        <hr>
            <asp:listbox id="lstCache" runat=server />
            <asp:button id="btnRemove" Text="Remove" runat=server
               OnClick="btnRemove_Click" />
    </form>
    <hr>
    <span id="tblCache" runat=server></span>
</body>
</html>

 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows