CHAPTER 12

image

Caching Content

Caching content is a natural extension of caching data, which I described in the previous chapter. Caching data allows you to generate content without having to calculate or load the data that a controller and view requires, which can be a good thing—but if the data hasn’t changed, then often the content produced by rendering the view won’t have changed either. And yet, the ASP.NET platform has to go through the entire request’s life cycle, which involves the MVC framework locating controllers, executing action methods, and rendering views—only to produce the same output as for the last request.

The ASP.NET platform provides a content caching feature that controls caching in several ways, including a server-side cache that is used to service requests and setting headers in responses to direct the client to cache content. In a complex, high-volume web application, reusing content generated for earlier requests can reduce the load on the application servers and, when the content is cached by the client, the amount of network capacity required. Table 12-1 summarizes this chapter.

Table 12-1. Chapter Summary

Problem

Solution

Listing

Cache content generated by an action method.

Apply the OutputCache attribute to the action method or the controller that contains it.

14

Cache content at the server.

Set the OutputCache.Location property to Server.

5

Cache variations of the content generated by an action method.

Use caching variations.

69

Define caching policies that can be used throughout an application.

Create cache profiles in the Web.config file.

10, 11

Cache the output from a child action.

Apply the OutputCache to the child action method.

1214

Adapt caching policy for individual requests.

Control caching using C# statements inside the action method.

15

Validate cached content before it is used.

Define a validation callback method and use the HttpCachePolicy.AddValidationCallback method.

16

Preparing the Example Project

For this chapter, I created a new project called ContentCache, following the pattern I have been using in earlier chapters. I used the Visual Studio ASP.NET Web Application template, selected the Empty option, and added the core MVC references. I’ll be using Bootstrap in this chapter, so enter the following command into the Package Manager Console:

Install-Package -version 3.0.3 bootstrap

I am going to use simple counters to illustrate when a request is served with cached content, and I will store the counters as application state data. As I explained in Chapter 10, I prefer to work with the application state feature through a helper class. I created a folder called Infrastructure and added to it a class file called AppStateHelper.cs, the contents of which are shown in Listing 12-1.

Listing 12-1.  The Contents of the AppStateHelper.cs File

using System;
using System.Web;
using System.Web.SessionState;
 
namespace ContentCache.Infrastructure {
 
    public enum AppStateKeys {
        INDEX_COUNTER
    }
 
    public static class AppStateHelper {
 
        public static int IncrementAndGet(AppStateKeys key) {
            string keyString = Enum.GetName(typeof(AppStateKeys), key);
            HttpApplicationState state = HttpContext.Current.Application;
            return (int)(state[keyString] = ((int)(state[keyString] ?? 0) + 1));
        }
    }
}

The helper class contains a method called IncrementAndGet, which retrieves an int value from the application state collection and increments it. If there is no stored value, then one is created.

I added a controller called Home to the project, the definition of which is shown in Listing 12-2.

Listing 12-2.  The Contents of the HomeController.cs File

using System.Diagnostics;
using System.Threading;
using System.Web.Mvc;
using System.Web.UI;
using ContentCache.Infrastructure;
 
namespace ContentCache.Controllers {
    public class HomeController : Controller {
 
        public ActionResult Index() {
            Thread.Sleep(1000);
            int counterValue = AppStateHelper.IncrementAndGet(
                AppStateKeys.INDEX_COUNTER);
            Debug.WriteLine(string.Format("INDEX_COUNTER: {0}", counterValue));
            return View(counterValue);
        }
    }
}

The controller defines the Index action method, which uses the helper class to increment and retrieve a counter called INDEX_COUNTER and passes it to the View method so that it is available as the model data in the view. The action method also writes a message to the Visual Studio Output window using the System.Diagnostics.Debug class to display the value of the counter.

You will notice that the first statement in the Index action method is a call to the Thread.Sleep method, which adds a one-second delay to processing the request. In many of the examples in this chapter, it is important to know when the action method is being executed and when cached content is being used, and a delay helps make this more obvious.

image Caution  Do not add deliberate delays to real ASP.NET projects. I have only called the Thread.Sleep method to make the effect of caching more obvious.

To create the view, I right-clicked the Index action method in the Visual Studio code editor and selected Add View from the pop-up menu. I set View Name to be Index, selected Empty (without model) for the Template option, and unchecked the View Options boxes. When I clicked the Add button, Visual Studio created the Views/Home/Index.cshtml file, which I edited with the content shown in Listing 12-3.

Listing 12-3.  The Contents of the Index.cshtml File

@model int
@{ Layout = null; }
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Output Caching</title>
    <link href="∼/Content/bootstrap.min.css" rel="stylesheet" />
    <link href="∼/Content/bootstrap-theme.min.css" rel="stylesheet" />
    <style>
        body { padding-top: 10px; }
    </style>
</head>
<body class="container">
    <div class="alert alert-info">
        The counter value: @Model
    </div>
    @Html.ActionLink("Update", "Index", null, new { @class = "btn btn-primary" })
</body>
</html>

The view displays the value of the counter. Start the application, and when you refresh the page or click the Update button, the counter is incremented, as shown in Figure 12-1.

9781430265412_Fig12-01.jpg

Figure 12-1. Incrementing the counter when refreshing the page

A good way to get insight into the effect of content caching is to use the browser F12 tools, which can capture information about the requests sent to the server and the responses they generate. To get a baseline without caching, press the F12 key to open the tools window, select the Network tab, and click the green arrow button; then reload the browser page. The request I am interested in is the first one in the list, which will be for the /, /Home, or /Home/Index URL, depending on what you asked the browser to display, as shown in Figure 12-2.

9781430265412_Fig12-02.jpg

Figure 12-2. The baseline request displayed in the F12 browser tools

I have reordered the columns in the F12 window to make it easier to see the details in which I am most interested for this chapter, namely, the Result column, which shows the HTTP status code returned from the server, and the Taken column, which shows how long the request took.

image Tip  Tracing tools such as Glimpse are not especially helpful when it comes to studying the effect of content caching. Glimpse can only report on the request that the server receives and executes, but, as you will learn, a big part of content caching is to help the browser avoid making requests at all.

Using the Content Caching Attribute

There are two ways to control content caching. The first—and simplest—is to apply an attribute to action methods or controllers. This is the technique I describe in this section. Table 12-2 puts content caching into context.

Table 12-2. Putting Content Caching by Attribute in Context

Question

Answer

What is it?

Applying the OutputCache attribute to action methods and controllers sets the caching policy, both in the ASP.NET server and in the HTTP clients, such as browsers and proxies.

Why should I care?

A careful caching policy can significantly reduce the amount of work required to service requests, allowing you to increase the throughput of your ASP.NET servers.

How is it used by the MVC framework?

The content cache isn’t used directly by the MVC framework, but the feature is available for use in controllers and action methods.

image Note  Later in the chapter, I describe a second approach, which is to configure the caching programmatically from within an action method, which allows the action to adapt the way that content is cached based on the current request. This is more flexible and provides support for fine-grain control over caching options, but it is more complicated and requires careful testing to ensure that everything works as expected. I recommend you start with the attribute in your projects because it is simpler to work with and creates a consistent effect. Use the programmatic effect only if you need to cache the same content in different ways.

The easiest way to cache content is to apply the OutputCache attribute to a controller or to individual action methods. Applying the attribute to the controller allows you to specify a single cache policy for all of the action methods it contains, while applying the attribute to individual action methods allows you to vary the caching configuration for each one. The OutputCache attribute defines the properties described in Table 12-3.

Table 12-3. The Properties Defined by the OutputCache Attribute

Name

Description

CacheProfile

This specifies a predefined caching configuration. See the “Creating Cache Profiles” section.

Duration

This specifies the number of seconds that the content will be cached for. See the following text for an example. A value for this property must be specified.

Location

This specifies where the content can be cached. See the “Controlling the Cache Location” section.

NoStore

When set to true, the response will set the no-store flag on the Cache-Control HTTP header. This asks clients and proxies to avoid storing the content on persistent media, such as disks.

SqlDependency

This specifies a dependency between the cached content and a SQL database table. This property doesn’t fit well with most MVC framework applications, whose models are typically defined through an intermediate layer such as the Entity Framework.

VaryByCustom

This specifies a custom value by which different versions of the content will be cached. See the “Creating Custom Cache Variations” section.

VaryByHeader

This specifies the set of HTTP headers by which different versions of the contentwill be cached. See the “Varying Caching Using Headers” section.

VaryByParam

This specifies the set of form and query string values by which different versions of the content will be cached. See the “Varying Caching Using Form or Query String Data” section.

Controlling the Cache Location

The only sensible place to cache application data is at the server, as demonstrated in Chapter 11. Raw data doesn’t become useful outside the server until it is processed and rendered by a view. There is no point sending cached application data to the browser, for example, because it doesn’t know anything about how your application transforms the data into content that can be displayed to the user.

Caching content, the topic of this chapter, is different because it is the finished product, produced in a format that is understood by the browser and that can be displayed to a user. If a browser has a cached copy of the content it requires, it doesn’t need to send a request to the server.

The ASP.NET content caching feature includes a server-side memory cache but can also be used to control the caching by clients and proxies through the use of HTTP headers.

The OutputCache attribute defines the Location property, which specifies where the output from an action method can be cached. Listing 12-4 shows the OuputCache applied to the Home controller.

Listing 12-4.  Adding Content Caching to the HomeController.cs File

using System.Diagnostics;
using System.Threading;
using System.Web.Mvc;
using System.Web.UI;
using ContentCache.Infrastructure;
 
namespace ContentCache.Controllers {
    public class HomeController : Controller {
 
        [OutputCache(Duration = 30, Location = OutputCacheLocation.Downstream)]
        public ActionResult Index() {
            Thread.Sleep(1000);
            int counterValue = AppStateHelper.IncrementAndGet(
                AppStateKeys.INDEX_COUNTER);
            Debug.WriteLine(string.Format("INDEX_COUNTER: {0}", counterValue));
            return View(counterValue);
        }
    }
}

image Tip  The Duration property must always be set when using the OutputCache attribute. I have set the cache duration to 30 seconds, which is too short for most real-life applications but makes it easy to test the examples without having to wait too long to see the effect of a change.

The Location property is set to one of the values from the OutputCacheLocation enumeration, which I have described in Table 12-4.

Table 12-4. The Values Defined by the OutputCacheLocation Enumeration

Name

Description

Any

The Cache-Control header is set to public, meaning that the content is cacheable by clients and proxy servers. The content will also be cached using the ASP.NET output cache at the server.

Client

The Cache-Control header is set to private, meaning that the content is cacheable by clients but not proxy servers. The content will also be cached using the ASP.NET output cache at the server.

Downstream

The Cache-Control header is set to public, meaning that the content is cacheable by clients and proxy servers. The content will not be cached using the ASP.NET output cache.

None

The Cache-Control header is set to no-cache, meaning that the content is not cacheable by clients and proxy servers. The ASP.NET output cache will not be used.

Server

The Cache-Control header is set to no-cache, but the content will be cached using theASP.NET output cache.

ServerAndClient

The Cache-Control header is set to private, meaning that the content is cacheable by clients but not proxy servers. The content will also be cached using the ASP.NET output cache.

I specified the Downstream value in the listing, which means that the Cache-Control response HTTP header is used tell the browser (and any caching proxies) how to cache the data. The Downstream value doesn’t put the content from the Index method in the server-side cache, which means that the action method will be executed if a request is made by the browser.

To see the effect of the OutputCache attribute, start the application, make a note of the counter value displayed, and then click the Update button. The counter value will not change as long as you clicked the button within 30 seconds of the original request. Wait 30 seconds and then click the Update button again, and the counter will be incremented.

You can see what is happening by using the F12 tools to record the network requests and looking at the response headers. The first time that the browser requests the content from the Index action method, ASP.NET adds the Cache-Control header to the response, like this:

...
Cache-Control:    public, max-age=30
...

This tells the browser that it should cache the content for 30 seconds. The public value indicates that intermediaries between the client and server, such as proxies, can also cache the data. If you look at the timings on the F12 Network tab, you will see results similar to those in Figure 12-3.

9781430265412_Fig12-03.jpg

Figure 12-3. Caching content in the browser

image Tip  If you don’t see the results shown in the figure, make sure that the Always Refresh from Server option on the F12 Network tab isn’t enabled. When enabled, the browser ignores caching instructions and sends requests to the server every time.

Internet Explorer shows that the result code for the request is 304, meaning that the content hasn’t changed since it was last obtained. This is misleading because it implies that the server sent the 304 code; however, IE obtained the content from its local cache. The effect is that clicking the Update button causes the browser to display the cached data without making a server request at all.

image Tip  Other browsers, including Google Chrome, make it more obvious when content has been retrieved from the browser cache. If you want to be sure that Internet Explorer isn’t sending requests to the server, then I recommend using Fiddler, which allows you to track all of the network activity on a machine. Fiddler is available without charge from www.telerik.com/fiddler.

Having content cached by the browser can improve performance for an individual user, but it has only a modest impact on the overall application because each user’s browser has to make an initial request to the server in order to get the content to cache.

A further limitation of client caching is that the browser has no insight into how the web application is structured, and that means that even the slightest variation in URL can cause the browser to bypass its cached data and make a request to the server. In the example application, the URLs /, /Home, and /Home/Index all target the Index action method on the Home controller, but the browser has no way of knowing this is the case and will not use cached data from one URL for another. For this reason, you should ensure that the URLs that your application generates are consistent, which is done most readily by using the URL helper methods to create URLs using an application’s routing policy.

Caching at the Server

The ASP.NET server has to perform a lot of work to generate content for a request, including locating the controller and action method and rendering the view to produce the content that will be sent to the client.

Caching the content at the server allows the ASP.NET platform to use content generated by earlier requests without having to go through the normal generation process, allowing the work performed for one request to be amortized across several. Listing 12-5 shows how I enabled server content caching in the Home controller.

Listing 12-5.  Enabling Server Content Caching in the HomeController.cs File

using System.Diagnostics;
using System.Threading;
using System.Web.Mvc;
using System.Web.UI;
using ContentCache.Infrastructure;
 
namespace ContentCache.Controllers {
    public class HomeController : Controller {
 
        [OutputCache(Duration = 30, Location = OutputCacheLocation.Server)]
        public ActionResult Index() {
            Thread.Sleep(1000);
            int counterValue = AppStateHelper.IncrementAndGet(
                AppStateKeys.INDEX_COUNTER);
            Debug.WriteLine(string.Format("INDEX_COUNTER: {0}", counterValue));
            return View(counterValue);
        }
    }
}

I have changed the value of the Location property to Server. This disables the browser caching but enables the server cache. To see the effect, start the application, start monitoring the network requests with the F12 tools, and then click the Refresh button.

If you look at the response headers from the server, you will see that the Cache-Control header is set as follows:

...
Cache-Control:  no-cache
...

This tells the browser not to cache the content and to request it fresh from the server every time. In the summary view, you will see the results illustrated by Figure 12-4.

9781430265412_Fig12-04.jpg

Figure 12-4. The effect of server content caching

The 200 status code shows that the content was requested from the server, but the time taken for the request is only 16 milliseconds, which indicates that the action method was not executed (because, as you will recall, it contains a one-second delay).

Server caching is implemented by a module defined in the OutputCacheModule class from the System.Web.Caching namespace. This module listens for the UpdateRequestCache event and uses it to populate its cache with content. The module listens for the ResolveRequestCache event and handles it by using its cached content as the response and terminates the request life cycle. (I described the life-cycle events in Chapter 3, modules in Chapter 4, and terminating the request handling process in Chapter 6.)

The result is that the ASP.NET platform is able to respond to requests engaging the MVC framework, avoiding all of the work required to execute an action and render a view—and that’s why the duration shown in Figure 12-4 is so much shorter than when the MVC framework produced the content originally.

REAL-WORLD CACHE SETTINGS

In the previous examples, I showed you the Downstream and Server caching modes because they allowed me to isolate the way that ASP.NET implements client and server caching. Most real web applications, however, require only two caching settings: Any and None.

The Any setting enables caching at the client and the server and offers the best performance. The None setting disables all caching options and requires the client to make requests for the content, which in turn are always passed on to the controller, action method, and view.

When using the Any setting, make sure you understand what the impact of caching will be on the liveliness of your application. If you set the caching period to be too long, then your users will be working with stale data. If you set the caching period to be too short, then it is possible that users won’t be requesting the data often enough to benefit from the cached content. I recommend disabling caching until you understand the performance profile of your application and are in a position to perform solid testing of the impact—both positive, in terms of reduced server load, and negative, in terms of liveliness.

The None setting is useful when the content must be generated fresh for every request. Applications that generate this content are few and far between, so the first thing to check before you apply the None setting is to make sure you are not trying to paper over a design or implementation issue in the application. If you find that you have a real need to disable caching, make sure you scale up your server infrastructure so that it can cope with clients always making requests to the server.

Managing Data Caching

The previous examples treated all requests as being equal, meaning that all requests for /Home/Index, for example, will receive the same cached content. You can also cache multiple versions of content and ASP.NET will match the cached content to the request using a caching variation, such as a request header or form data values.

Caching variations allow you to increase the flexibility of your application by generating cached content for a range of requests for the same URL, getting the performance increase that caching offers without being tied to a single version of the cached content for all requests. I’ll demonstrate the way that you can vary caching in the sections that follow. Table 12-5 puts caching variations in context.

Table 12-5. Putting Caching Variations in Context

Question

Answer

What is it?

Caching variations allow caching of multiple copies of the content produced by an action method. The ASP.NET platform will match a request to the variations and deliver the right version of the content.

Why should I care?

Caching variations increase the flexibility of the content cache, allowing it to be useful in a wider range of applications.

How is it used by the MVC framework?

This feature is not used directly by the MVC framework, but it is available for use in controllers and action methods.

Varying Caching Using Headers

The simplest form of caching variation is to deliver different versions of cached content based on the headers in the HTTP request. To demonstrate how this works, I have modified configuration of the OutputCache applied to the Index action method on the Home controller, as shown in Listing 12-6.

Listing 12-6.  Varying Cached Content Based on Headers in the HomeController.cs File

using System.Diagnostics;
using System.Threading;
using System.Web.Mvc;
using System.Web.UI;
using ContentCache.Infrastructure;
 
namespace ContentCache.Controllers {
    public class HomeController : Controller {
 
        [OutputCache(Duration = 30, VaryByHeader="user-agent",
            Location = OutputCacheLocation.Any)]
        public ActionResult Index() {
            Thread.Sleep(1000);
            int counterValue = AppStateHelper.IncrementAndGet(
                AppStateKeys.INDEX_COUNTER);
            Debug.WriteLine(string.Format("INDEX_COUNTER: {0}", counterValue));
            return View(counterValue);
        }
    }
}

The VaryByHeader property of the OutputCache attribute allows me to specify one or more headers that will be used to differentiate between requests. I selected the easiest header to test, which is User-Agent, used to identify the browser (and which is used by the browser capabilities feature that I described in Chapter 7).

The ASP.NET platform will execute the Index action method and cache the result for every different user-agent header value that it receives. Subsequent requests that contain the same user-agent header within the caching direction will receive the appropriate cached version.

You will need two different browsers to be able to test this example, such as Internet Explorer and Google Chrome. Start the application and navigate to /Home/Index with both browsers. You will see a different counter value for each browser, which reflects the fact that different content has been cached for each user-agent value.

Varying Caching Using Form or Query String Data

A similar approach can be taken to generate and cache content for different permutations of form or query string data. Of all the techniques I describe in the chapter, this is the one that requires the most caution and causes the most trouble. In most web applications, it is important that data sent from the user is processed and used to update the model, advancing the user through the application flows and features. Caching content based on form data can mean that user interactions don’t target the action methods in the application, which means that user input is lost. That said, this technique can be useful if you have action methods that generate content without updating the model—although if this is the case, static HTML can often be a better alternative than caching content generated by an action method. Listing 12-7 shows how I have updated the Home controller so that the content generated from the Index action method is varied based on user data values.

Listing 12-7.  Varying Cached Content Based on User Data in the HomeController.cs File

using System.Diagnostics;
using System.Threading;
using System.Web.Mvc;
using System.Web.UI;
using ContentCache.Infrastructure;
 
namespace ContentCache.Controllers {
    public class HomeController : Controller {
 
        [OutputCache(Duration = 30, VaryByHeader="user-agent",
            VaryByParam="name;city", Location = OutputCacheLocation.Any)]
        public ActionResult Index() {
            Thread.Sleep(1000);
            int counterValue = AppStateHelper.IncrementAndGet(
                AppStateKeys.INDEX_COUNTER);
            Debug.WriteLine(string.Format("INDEX_COUNTER: {0}", counterValue));
            return View(counterValue);
        }
    }
}

The VaryByParam property is used to specify one or more query string or form data parameter names. For each permutation of values of these parameters, ASP.NET will cache a version of the content generated by the action method and use it to satisfy subsequent requests made with the same values.

To see the effect, start the application and navigate to the following URL, which contains query string values for the parameters specified for the VaryByParam property:

/Home/Index?name=Adam&city=London

Open a second instance of the same browser and request the same URL, and you will receive the cached content from the previous request. If you specify a different name or city, then a new version of the content will be generated and cached.

image Tip  You can specify an asterisk (the * character) as the value for the VaryByParam property, which will cause different versions of the content to be cached for every permutation of every query string or form parameter. Use this with caution because it can effectively disable caching in most applications.

In Listing 12-7, I left the VaryByHeader property set, which means that different versions of the content will be cached for every permutation of query string values and user agents that ASP.NET receives requests for. This means that requesting the URL shown earlier from Internet Explorer and Google Chrome, for example, will generate different versions of the cached content, even though the query string values are the same.

image Tip  The best way to think about cache variations is to consider the combination of query string and headers as a key. The cache will be populated with content for each unique key for which a request is made, and requests for which the cache already contain a match for the key will receive that version of the cached content.

Creating Custom Cache Variations

You can create custom caching variations if headers and user data are not enough to differentiate between the requests that your application receives. The OutputCache.VaryByCustom property is used to specify an argument that is passed to a method in the global application class, which is called when requests are received to generate a cache key. Listing 12-8 shows the application of this property in the Home controller.

Listing 12-8.  Using a Custom Cache Variation in the HomeController.cs File

using System.Diagnostics;
using System.Threading;
using System.Web.Mvc;
using System.Web.UI;
using ContentCache.Infrastructure;
 
namespace ContentCache.Controllers {
    public class HomeController : Controller {
 
        [OutputCache(Duration = 30,
            VaryByHeader="user-agent",
            VaryByParam="name;city",
            VaryByCustom="mobile",
            Location = OutputCacheLocation.Any)]
        public ActionResult Index() {
            Thread.Sleep(1000);
            int counterValue = AppStateHelper.IncrementAndGet(
                AppStateKeys.INDEX_COUNTER);
            Debug.WriteLine(string.Format("INDEX_COUNTER: {0}", counterValue));
            return View(counterValue);
        }
    }
}

I specified the value mobile, which I’ll use to identify mobile devices using the capabilities detection feature that I described in Chapter 7. As I explained in Chapter 7, there are some serious hazards when it comes to detecting mobile devices because the term is so nebulous and the data quality is so low, but it is good enough for my purposes in this chapter. Listing 12-9 shows how I have added support for the mobile cache variation in the global application class.

Listing 12-9.  Adding a Custom Cache Variation to the Global.asax.cs File

using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
 
namespace ContentCache {
 
    public class MvcApplication : System.Web.HttpApplication {
 
        protected void Application_Start() {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);
        }
 
        public override string GetVaryByCustomString(HttpContext ctx, string custom) {
            if (custom == "mobile") {
                return Request.Browser.IsMobileDevice.ToString();
            } else {
                return base.GetVaryByCustomString(ctx, custom);
            }
        }
    }
}

HttpApplication, which is the base for the global application class, defines the GetVaryByCustomString method. To add support for a custom cache variation, I override this method and check the value of the custom argument. When this argument is mobile, I am being asked to generate my custom cache key from the request, which I do by returning the value of the HttpRequest.Browser.IsMobileDevice property. This means I will cause two variations of the data in the cache—one for devices that cause the IsMobileDevice property to return true and one for those that return false. I call the base implementation of the method for all other values of the custom argument, without which the other variation features won’t work.

image Tip  To see the effect of the custom variation in Listing 12-9, you can use the device emulation feature that Google Chrome provides to simulate requests from a smartphone or tablet. See Chapter 7 for details.

Creating Cache Profiles

As Listing 12-8 shows, the configuration of the OutputCache attribute can be verbose and is required for any action method or controller class for which caching is required. To help reduce duplication and to ensure consistent caching policies, the ASP.NET platform supports cache profiles. A cache profile is a particular caching configuration defined in the Web.config file that can be applied anywhere in the application by name. Listing 12-10 shows how I created a cache profile for the example application.

Listing 12-10.  Creating Cache Profiles in the Web.config File

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="webpages:Version" value="3.0.0.0" />
    <add key="webpages:Enabled" value="false" />
     <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
  </appSettings>
  <system.web>
    <compilation debug="true" targetFramework="4.5.1" />
    <httpRuntime targetFramework="4.5.1" />
    <caching>
      <outputCacheSettings>
        <outputCacheProfiles>
          <add name="cp1" duration="30" location="Any"
               varyByHeader="user-agent" varyByParam="name;city" varyByCustom="mobile"/>
        </outputCacheProfiles>
      </outputCacheSettings>
    </caching>
  </system.web>
</configuration>

The outputCacheProfiles collection configuration section is used to define cache profiles and is applied within the caching/outputCacheSettings section group. The add element is used to create new profiles, and the attributes on the element correspond to the OutputCache properties that I listed in Table 12-3. In Listing 12-10, I defined a cache profile called cp1, which reproduces the configuration of the OutputCache attribute from Listing 12-8. Listing 12-11 shows the application of the profile to the Home controller.

Listing 12-11.  Applying a Cache Profile in the HomeController.cs File

using System.Diagnostics;
using System.Threading;
using System.Web.Mvc;
using System.Web.UI;
using ContentCache.Infrastructure;
 
namespace ContentCache.Controllers {
    public class HomeController : Controller {
 
        [OutputCache(CacheProfile="cp1")]
        public ActionResult Index() {
            Thread.Sleep(1000);
            int counterValue = AppStateHelper.IncrementAndGet(
                AppStateKeys.INDEX_COUNTER);
            Debug.WriteLine(string.Format("INDEX_COUNTER: {0}", counterValue));
            return View(counterValue);
        }
    }
}

As the listing shows, the individual configuration properties are replaced with CacheProfile, which is set to the name of the profile in the Web.config file. Not only does this tidy up the code, but it allows a consistent caching policy to be applied throughout the application and makes it easy to change that policy without having to locate and modify every instance of the OutputCache attribute.

Caching Child Action Output

The OutputCache attribute can be applied to any action method, including those that are used only as child actions. This means you can specify different caching policies when using the MVC framework to compose content from child actions. To demonstrate how this works, I have added a child action method to the Home controller and applied the OutputCache attribute to it, as shown in Listing 12-12.

Listing 12-12.  Adding a Child Action to the HomeController.cs File

using System.Diagnostics;
using System.Threading;
using System.Web.Mvc;
using System.Web.UI;
using ContentCache.Infrastructure;
using System;
 
namespace ContentCache.Controllers {
    public class HomeController : Controller {
 
        [OutputCache(CacheProfile = "cp1")]
        public ActionResult Index() {
            Thread.Sleep(1000);
            int counterValue = AppStateHelper.IncrementAndGet(
                AppStateKeys.INDEX_COUNTER);
            Debug.WriteLine(string.Format("INDEX_COUNTER: {0}", counterValue));
            return View(counterValue);
        }
 
        [ChildActionOnly]
        [OutputCache(Duration=60)]
        public PartialViewResult GetTime() {
            return PartialView((object)DateTime.Now.ToShortTimeString());
        }
    }
}

You can use the Duration, VaryByCustom, and VaryByParam properties only when applying the OutputCache attribute to child actions. I have set the Duration property for the child action to be 60 seconds, which is longer than the duration for the Index action method. The effect will be that the output from the GetTime child action will be stored in the cache, even when cached output from the Index action is refreshed.

image Tip  Specifying a shorter caching duration for a child action than its parent has no effect, and the caching policy for the parent action will be used. This is equivalent to not applying the OutputCache attribute to the child at all.

To create the partial view for the child action, I right-clicked the GetTime method in the code editor and selected Add View from the pop-up menu. I set View Name to be GetTime, set Template to Empty (without model), and checked the Create as a Partial View option. When I clicked the Add button, Visual Studio created the Views/Home/GetTime.cshtml file, which I edited as shown in Listing 12-13.

Listing 12-13.  The Contents of the GetTime.cshtml File

@model string
<div class="alert alert-info">
    The time is: @Model
</div>

Finally, I updated the Index.cshtml file to invoke the child action, as shown in Listing 12-14.

Listing 12-14.  Invoking a Child Action in the Index.cshtml File

@model int
@{ Layout = null; }
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Output Caching</title>
    <link href="∼/Content/bootstrap.min.css" rel="stylesheet" />
    <link href="∼/Content/bootstrap-theme.min.css" rel="stylesheet" />
    <style>
        body { padding-top: 10px; }
    </style>
</head>
<body class="container">
    <div class="alert alert-info">
        The counter value: @Model
    </div>
    @Html.Action("GetTime")
    @Html.ActionLink("Update", "Index", null, new { @class = "btn btn-primary" })
</body>
</html>

To see the effect, start the application and periodically click the Update button. The counter value produced by the Index action method will update after 30 seconds, but the same time value from the GetTime child action will be used for a further 30 seconds.

DONUT AND DONUT-HOLE CACHING

There is a technique called donut caching, which is where the content from an action method is cached by the server and supplemented by calls to action methods in every request. The name arises because the part that is cached (the donut) entirely surrounds the parts that are not (the donut holes). The donut is the content from the action method, and the donut holes are the content from the child actions.

Donut caching can be useful when the skeletal structure of the content is static and needs to be blended with fragments of dynamic content. The ASP.NET platform doesn’t support donut caching for MVC framework applications, but there is an open source package that provides this functionality. See https://github.com/moonpyk/mvcdonutcaching for details.

ASP.NET does support a related technique, called donut-hole caching. If you apply the OutputCache attribute to a child action but not its parent, you create an effect where the donut hole (the child action output) is cached while the donut (the content from the action method) is not. This is useful for caching relatively static content, such as navigation controls, while leaving other content to be generated for each and every request.

Controlling Caching with Code

The strength of the OutputCache attribute is that it allows consistent caching to be applied to the content generated by action methods, but this can also be a drawback if you want to dynamically alter the caching policy based on the request. In this section, I’ll show you how to take control of the caching using C# statements inside of action methods. Table 12-6 puts code-based cache policy in context.

Table 12-6. Putting Code-Based Caching Policy in Context

Question

Answer

What is it?

Controlling caching with C# statements allows an action method to tailor the cached content on a per-request basis.

Why should I care?

Although more complicated than using the attribute, controlling the cache policy by code allows you to tightly integrate content caching into a controller.

How is it used by the MVC framework?

This feature is not used by the MVC framework but is available for use within action methods.

The caching policy for a response is set through the HttpResponse.Cache property, which returns an instance of the HttpCachePolicy class from the System.Web namespace. Table 12-7 describes the most useful properties and methods defined by the HttpCachePolicy class.

Table 12-7. The Members Defined by the HttpCachePolicy

Name

Description

AddValidationCallback(handler, data)

Specifies a callback handler that will validate that the cached content is still valid. See the “Validating Cached Content” section.

SetCacheability(policy)

Sets the Cache-Control response header using one of the values defined by the HttpCacheability enumeration. See the “Dynamically Setting Cache Policy” section.

SetExpires(time)

Sets the Expires response header using a DateTime value.

SetLastModified(time)

Sets the Last-Modified header using a DateTime value.

SetMaxAge(period)

Sets the max-age flag of the Cache-Control response header using a TimeSpan.

SetNoServerCaching()

Disables server caching for the current response.

SetNoStore()

Adds the no-store flag to the Cache-Control response header.

SetNoTransforms()

Adds the no-transform flag to the Cache-Control response header.

SetProxyMaxAge(period)

Sets the s-maxage flag of the Cache-Control response header using a TimeSpan.

SetVaryByCustom(name)

Sets the argument that will be passed to the global application class GetVaryByCustomString method. See the “Creating Custom Cache Variation” section.

VaryByHeaders

Specifies the headers that will be used to vary the cached content. See the “Varying Caching Using Headers” section.

VaryByParams

Specifies the query string or form parameters that will be used to vary the cache content. See the “Varying Caching Using Form or Query String Data” section.

image Note  Many of the methods defined by the HttpCachePolicy class relate to the Cache-Control header, which used to issue direction to clients and intermediate devices such as proxies. I am not going to detail the Cache-Control header here, in part because it is implemented as part of HTTP rather than as an ASP.NET feature and partly because there the HTTP specification already contains all the information you could possibly want about the header. See www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 for details.

Dynamically Setting Cache Policy

The benefit of working with the HttpCachePolicy class is that you can inspect the request from within the action method and adapt the caching policy dynamically. Listing 12-15 shows how I updated the Home controller to use the HttpCachePolicy class to change the caching policy based on the URL that was used to target the Index action method.

Listing 12-15.  Setting Cache Policy in the HomeController.cs File

using System.Diagnostics;
using System.Threading;
using System.Web.Mvc;
using System.Web.UI;
using ContentCache.Infrastructure;
using System;
using System.Web;
 
namespace ContentCache.Controllers {
    public class HomeController : Controller {
 
        public ActionResult Index() {
            if (Request.RawUrl == "/Home/Index") {
                Response.Cache.SetNoServerCaching();
                Response.Cache.SetCacheability(HttpCacheability.NoCache);
            } else {
                Response.Cache.SetExpires(DateTime.Now.AddSeconds(30));
                Response.Cache.SetCacheability(HttpCacheability.Public);
            }
            Thread.Sleep(1000);
            int counterValue = AppStateHelper.IncrementAndGet(
                AppStateKeys.INDEX_COUNTER);
            Debug.WriteLine(string.Format("INDEX_COUNTER: {0}", counterValue));
            return View(counterValue);
        }
 
        [ChildActionOnly]
        [OutputCache(Duration = 60)]
        public PartialViewResult GetTime() {
            return PartialView((object)DateTime.Now.ToShortTimeString());
        }
    }
}

image Tip  Be careful of using the Update button for this example because the URL it requests will always be /. When checking to see whether the /Home/Index URL is cached, use the F5 key to refresh the browser instead.

I use the HttpRequest.RawUrl property to set the caching policy for the content generated by the Index action method. If the action method has been targeted by the /Home/Index URL, then I disable server caching and use the SetCacheability method to disable browser caching. The argument to the SetCacheability method is a value from the HttpCacheability enumeration, which defines the values shown in Table 12-8. If the action method has been targeted by any other URL, then I set the cache duration to 30 seconds and use the SetCacheability method to enable browser caching.

Table 12-8. The Values Defined by the HttpCacheability Enumeration

Name

Description

NoCache

This sets the Cache-Control header to no-cache, which prevents content from being cached by clients and proxies. This is equivalent to setting the OutputCache.Location property to None.

Private

The Cache-Control header is set to private, meaning that the content is cacheable by clients but not proxy servers. This is equivalent to setting the OutputCache.Location property to Client.

Public

The Cache-Control header is set to public, meaning that the content is cacheable by clients and proxy servers. This is equivalent to setting the OutputCache.Location property to Downstream.

Server

The Cache-Control header is set to no-cache, but the content will be cached using the ASP.NET output cache. This is equivalent to setting the OutputCache.Location property to Downstream.

ServerAndNoCache

This combines the Server and NoCache settings.

ServerAndPrivate

This combines the Server and Private settings.

To test the effect of the changes I made to the Home controller, start the application and request the root URL for the application (/). When you click the Update button, you will see that the content is cached for 30 seconds. Navigate to the /Home/Index URL and use the F5 key to refresh the page, and you will see that the browser requests the content from the server every time—and if you look at the Visual Studio Output window, you will see that the request is passed to the Home controller for every request.

Validating Cached Content

The HttpCachePolicy.AddValidationCallback method takes a method that is called to validate whether a cached data item is valid before it is returned to the client. This allows for custom management of the content cache, beyond the duration and location-based configuration. Listing 12-16 shows how I have added a validation callback to the Home controller.

Listing 12-16.  Using a Cache Validation Callback to the HomeController.cs File

using System.Diagnostics;
using System.Threading;
using System.Web.Mvc;
using System.Web.UI;
using ContentCache.Infrastructure;
using System;
using System.Web;
 
namespace ContentCache.Controllers {
    public class HomeController : Controller {
 
        public ActionResult Index() {
 
            Response.Cache.SetExpires(DateTime.Now.AddSeconds(30));
            Response.Cache.SetCacheability(HttpCacheability.Server);
            Response.Cache.AddValidationCallback(CheckCachedItem, Request.UserAgent);
 
            Thread.Sleep(1000);
            int counterValue = AppStateHelper.IncrementAndGet(
                AppStateKeys.INDEX_COUNTER);
            Debug.WriteLine(string.Format("INDEX_COUNTER: {0}", counterValue));
            return View(counterValue);
        }
 
        [ChildActionOnly]
        [OutputCache(Duration = 60)]
        public PartialViewResult GetTime() {
            return PartialView((object)DateTime.Now.ToShortTimeString());
        }
 
        public void CheckCachedItem(HttpContext ctx, object data,
            ref HttpValidationStatus status) {
 
            status = data.ToString() == ctx.Request.UserAgent ?
                HttpValidationStatus.Valid : HttpValidationStatus.Invalid;
            Debug.WriteLine("Cache Status: " + status);
        }
    }
}

The AddValidationCallback method takes two arguments. The first is the method that will be called to validate the cached content, and the second is an object that can be used to identify the cached content in order to decide whether it is valid. In the example, I specified the CheckCachedItem method and selected the user-agent string for the object argument.

The callback method must take a specific set of arguments: an HttpContext object, the object argument from the call to the AddValidationCallback method, and an HttpValidationStatus value, which is marked with the ref keyword. The job of the callback method is to use the context to examine the request and set the value of the HttpValidationStatus argument in order to tell the ASP.NET platform whether the content is still valid. Table 12-9 shows the set of values defined by the HttpValidatonStatus enumeration. In the listing, I invalidate the cached content if the user-agent string doesn’t match the one used when the content was cached.

Table 12-9. The Values Defined by the HttpValidationStatus Enumeration

Name

Description

Valid

The cached content is still valid.

Invalid

The content is no longer valid and should be ejected from the cache. The request is passed to the action method.

IgnoreThisRequest

The request is passed to the action method, but the value in the cache is not ejected.

image Tip  You must set an expiry date and ensure that the content is cached at the server when using the AddValidationCallback method, just as I did in the listing. If you do not do this, then the callback method won’t be used because the content won’t be cached in the right place.

Summary

In this chapter, I described the content cache, which can be used to enable server caching of content produced by action methods and set the response headers used by HTTP clients such as browsers and proxies. I demonstrated how you can use an attribute or C# statements to control caching and showed you how to manage the cache and its contents. In the next chapter, I show you how to set up and use the ASP.NET Identity system, which is used to manage user accounts.

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

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