Integrating ASP.NET Output Caching with WCF WebHttp Services
This is part seven of a twelve part series that introduces the features of WCF WebHttp Services in .NET 4. In this post we will cover:
- Creating ASP.NET cache profiles in configuration for use with WCF WebHttp Services
- Using the [AspNetCacheProfile] attribute to associate service operations with cache profiles
Over the course of this blog post series, we are building a web service called TeamTask. TeamTask allows a team to track tasks assigned to members of the team. Because the code in a given blog post builds upon the code from the previous posts, the posts are intended to be read in-order.
Downloading the TeamTask Code
At the end of this blog post, you’ll find a link that will allow you to download the code for the current TeamTask Service as a compressed file. After extracting, you’ll find that it contains “Before” and “After” versions of the TeamTask solution. If you would like to follow along with the steps outlined in this post, download the code and open the "Before" solution in Visual Studio 2010. If you aren’t sure about a step, refer to the “After” version of the TeamTask solution.
Note: If you try running the sample code and see a Visual Studio Project Sample Loading Error that begins with “Assembly could not be loaded and will be ignored…”, see here for troubleshooting.
Getting Visual Studio 2010
To follow along with this blog post series, you will need to have Microsoft Visual Studio 2010 and the full .NET 4 Framework installed on your machine. (The client profile of the .NET 4 Framework is not sufficient.) At the time of this posting, the Microsoft Visual Studio 2010 Ultimate Beta 2 is available for free download and there are numerous resources available regarding how to download and install, including this Channel 9 video.
Step 1: Specifying that Responses should be Cached
Caching is fundamental to HTTP, so its not surprising that since .NET 1.1 there has been a rich mechanism for declaratively setting the caching policy of a webpage in ASP.NET. Known as Output Caching, it allows developers to specify many aspects of how a page should be cached. Should the page be cached on the server to be used again for future requests, or should an HTTP Cache-Control header be included with the response for client (or proxy) caching? When should the cached content expire and become considered stale? Are there certain query string parameters in the URI or HTTP headers in the request that will require different responses and therefore will need to be cached separately? ASP.NET output caching can accommodate all of these complexities.
With .NET 4, the rich caching capabilities of the ASP.NET output cache can now be leveraged from within a WCF WebHttp Service when ASP.NET compatibility mode is enabled. Specifying that a response from a service operation should be cached in some form is done by adding an [AspNetCacheProfile] attribute to the operation. The [AspNetCacheProfile] attribute will then point to an ASP.NET cache profile in the Web.config where the details of how the response should be cached are specified.
With the TeamTask service we’ll begin caching the user data returned by the GetUsers() and GetUser() operations. We’ll assume that this user data changes very rarely, making it appropriate for caching. In this step we’ll add the [AspNetCacheProfile] attribute to the service operations and in the next step we will configure their respective cache profiles in the Web.config file.
If you haven't already done so, download and open the “Before” solution of the code attached to this blog post.
Open the UserService.cs file from the TeamTask.Service project in the code editor.
On the GetUsers() operation, add an [AspNetCacheProfile] attribute with the cache profile name “UsersCollection” like so:
[AspNetCacheProfile("UsersCollection")]
[Description("Returns the users on the team.")]
[WebGet(UriTemplate = "?skip={skip}&top={top}&manager={userName}")]
public List<User> GetUsers(int skip, int top, string userName)On the GetUser() operation, add an [AspNetCacheProfile] attribute with the cache profile name “SingleUser” like so:
[AspNetCacheProfile("SingleUser")]
[Description("Returns the details of a user on the team.")]
[WebGet(UriTemplate = "{userName}")]
public User GetUser(string userName)
Step 2: Configuring Server-side Caching
Let’s first configure the “UsersCollection” cache profile for the GetUsers() operation. We’ll configure the profile to cache the responses on the server.
In addition to the caching location, we’ll need to specify some other details in the cache profile. For the sake of demonstrating the feature we’ll specify that the cached responses should expire after one minute, but we could set them to expire after an hour, a day or even longer. Since the GetUsers() operation accepts the “skip”, “top” and “manager” query string parameters and these change the content of the response, we’ll need to specify this in the cache profile. Lastly, with automatic format selection enabled, the format (XML or JSON) of the response will vary depending on the value of the request’s HTTP Accept Header, so we’ll also need to account for this in the cache profile configuration.
Open the Web.Config file of the TeamTask.Service project in the code editor. Since cache profiles are an ASP.NET feature, they are specified under the <system.Web> element, which should currently have the following configuration elements specified:
<system.web>
<compilation debug="true" targetFramework="4.0" />
</system.web>All of the caching related configuration is specified in a <caching> element under the <system.web> element. Add the caching related configuration elements as follows:
<system.web>
<compilation debug="true" targetFramework="4.0" /><caching>
<outputCache enableOutputCache="true"/>
<outputCacheSettings>
<outputCacheProfiles>
</outputCacheProfiles>
</outputCacheSettings>
</caching>
</system.web>ASP.NET output caching has to be enabled for any of the cache profiles to be applied and this is done with the “enableOutputCache” attribute on the <outputCache> element. The cache profiles themselves are specified under the <outputCacheProfiles> element.
Add the following cache profile under the <outputCacheProfiles> element:
<outputCacheProfiles>
<add name="UsersCollection" location="Server" duration="60"
varyByParam="skip; top; manager" varyByHeader="Accept"/>
</outputCacheProfiles>The settings on the cache profile should be fairly self-explanatory. The duration is specified in seconds, so a value of “60” indicates that the response should be cached for a single minute. The varyByParam attribute specifies that requests with different values for the “skip”, “top” and “manager” URI query string parameters should be considered different requests such that the respective responses are cached separately. Likewise, the varyByHeader attribute specifies that requests with different Accept header values should be considered different requests. Note: For the varyByParam and varyByHeader attributes, use a semi-colon separated list if there is more than a single value.
Be Careful: Requests that require authorization should not have their responses cached, because the authorization is not performed when the response is served from the cache. Caching such responses would introduce a serious security vulnerability. Usually, requests that require authorization provide user-specific data and therefore server-side caching is not even beneficial. In such situations, client-side caching or simply not caching at all will be more appropriate. |
Step 3: Configuring Client-side Caching
One of the benefits of using ASP.NET output caching is that it provides a single programming model for both server-side and client-side caching even though the two mechanisms are very different. Server-side caching is invisible to the client. After a response has been cached on the server, future responses are served from the cache such that the request never even reaches the WCF WebHttp Service code. Client-side caching on the other hand is achieved by adding an HTTP Cache-Control header to the response. Future request from (supposedly different) clients would still be handled by the WCF WebHttp Service code.
Configuring server-side verse client-side caching is a simple matter of setting the location attribute on the cache profile. For the “UsersCollection” cache profile in step two, we set the location to “Server”. For the “SingleUser” cache profile, we’ll set the location to “Client” to enable client-side caching. Of course, it is also possible to use server-side and client-side caching in tandem—see here for the possible values of the location attribute.
Open the Web.Config file from the TeamTask.Service project in the code editor.
Add the “SingleUser” cache profile under the <outputCacheProfiles> element as shown below:
<outputCacheProfiles>
<add name="UsersCollection" location="Server" duration="60"
varyByParam="skip, top, manager" varyByHeader="Accept"/>
<add name="SingleUser" location="Client" duration="60"
varyByParam="none"/>
</outputCacheProfiles>Notice that the location of the "SingleUser" cache profile is "Client". Note: Because we are configuring client-side caching, the query string parameters and HTTP headers of the request aren't relevant. However, a varyByParam value is required, so we’ve set the value to “none”.
Helpful Tip: It is possible to configure an SQL dependency with cached responses in WCF WebHttp Services. Once configured, the ASP.NET cache will poll a given table of your database such that any changes to the table will result in the cached response being evicted. For more information on configuring SQL dependencies with the output cache, see here. |
Step 4: Verifying that Responses are Cached
Now that we have our cache profiles configured we’re ready to demonstrate that the responses are indeed cached. To verify the client-side caching we’ll write some client code to print the HTTP Cache-Control header from the response to the console. To verify the server-side caching we’ll set a break-point in the service code and send two HTTP GET requests, noting that the debugger breaks with the first request but not the second (since it is served from the cache).
Open the Program.cs file from the TeamTask.Client project in the code editor.
In the static GetUser() method, add a line of code to write the Cache-Control header value from the response to the console like so:
using (HttpResponseMessage response = client.Send(request))
{
Console.WriteLine(" Cache-Control: {0}",
response.Headers.CacheControl);
Console.WriteLine(" Status Code: {0}", response.StatusCode);
Console.WriteLine(" Content: {0}", response.Content.ReadAsString());
}Now we’ll implement the Main() method to simply call GetUser() once and GetUsers() three times. Replace any code within the Main() method with the following code:
using (HttpClient client = new HttpClient("https://localhost:8080/TeamTask/"))
{
GetUser(client, "user1", "application/json");
Console.WriteLine(
"Press Enter to call GetUsers() with top=3. Should hit the breakpoint...");
Console.ReadKey();
client.Get("Users/?top=3");Console.WriteLine(
"Press Enter to call GetUsers() with top=3 again. Should hit in the cache...");
Console.ReadKey();
client.Get("Users/?top=3");Console.WriteLine(
"Press Enter to call GetUsers() with top=2. Should hit the breakpoint...");
Console.ReadKey();
client.Get("Users/?top=2");Console.ReadKey();
}Before we start the server and client, we need to set a breakpoint in the GetUsers() operation. Open the UserService.cs file from the TeamTask.Service project in the code editor and click on the breakpoint bar along the left-hand side to add a breakpoint like so:
Start with debugging (F5) to get the TeamTask service running and then start the client by right clicking on the TeamTask.Client project in the “Solution Explorer” window (Ctrl+W, S) and selecting “Debug”—>”Start New Instance”. The console should contain the following output:
Notice that the Cache-Control header is present with a max-age of 60, which agrees with the duration value we used in the “SingleUser” cache profile. This tells the client that the response can be cached on the client for up to 60 seconds.
With the client console application in focus, press "Enter" as directed so that the client application continues to execute.
You should find that the service breaks after the first request to the GetUsers() operation. You can press (F5) to continue executing the service. The service should not break after the second request because the response is being served from the cache and the GetUsers() operation never executes. However, the service should break on the third request because it has a different value for the “top” query string parameter and the response can’t be served from the cache.
Next Steps: Returning Custom Formats from WCF WebHttp Services
Back in part four of this blog post series we demonstrated the first-class support for XML and JSON in WCF WebHttp Services for .NET 4. While XML and JSON are certainly popular web content-types, there are others. In part eight of this blog post series we’ll reinvestigate the format support included with WCF WebHttp Services in .NET 4 and demonstrate how to send responses with other content-types like Atom feeds, plain text, or even binary data.
Randall Tombaugh
Developer, WCF WebHttp Services