Udostępnij za pośrednictwem


Conditional GET and ETag Support in WCF WebHttp Services

This is part ten of a twelve part series that introduces the features of WCF WebHttp Services in .NET 4.  In this post we will cover:

  • Generating and including ETags in responses from a WCF WebHttp Service 
  • Adding Conditional GET support to a WCF WebHttp Service using ETags

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: Adding ETags to Responses

Entity Tags (or ETags) are a crucial aspect of the caching mechanisms in HTTP.  An ETag is an opaque quoted string that may be returned along with a response in an ETag HTTP header.  ETags are used to indicate whether or not a resource has changed state.  If two requests for the same resource result in responses with the same ETag then it can be assumed that the state of the resource has not changed between the two requests.

As we’ll see, ETags can be used to implement conditional GET.  With conditional GET the client sends the ETag from a previous response and effectively asks the server to only send the content if the resource state has changed.  Using conditional GET reduces the bandwidth demands on the service and client since the content need not be sent if the client already has the most recent version.

To demonstrate the ETag support in WCF WebHttp Services for .NET 4 we’ll update the TeamTask service to send responses with ETags for all requests of a single task.  The GetTask() service operation on the TaskService class is responsible for returning individual tasks, so it's the operation we’ll be modifying. 

We’ll need to do two things in order to provide ETags with responses from the GetTask() operation.  First, we’ll need to specify that a given ETag should be included with a given response.  As you’ll see below, this is easily done by calling the new SetETag() method from the WebOperationContext within the service operation.   But we also need to generate the ETag in the first place.  Reliably and efficiently generating Etags for our tasks could be a messy job, but since we’re using an SQL database to store our tasks, we can delegate the ETag generation to the database and make it easy for ourselves.

We’ll be using the Version property of the Task type for our ETags.  The Version property has existed on the Task type from the very first post of this series but you might not have noticed it since we haven’t been serializing it when generating responses.  The Version property on the Task type is mapped to the Version column of the Tasks table in the TeamTask.mdf database file.  If you open the Tasks table definition in the Server Explorer window (Ctrl+W, L), you’ll see that the Version column is a timestamp data type that is updated by SQL anytime the task is updated in the database.

The Version property of the Task type is an eight byte array, so the first thing we’ll do is create a an extension method that will encode the eight byte array as a string representing a hexadecimal number.

  1. If you haven't already done so, download and open the “Before” solution of the code attached to this blog post.

  2. In the Solution Explorer (Ctrl+W, S) right click on the TeamTask.Service project and select “Add”—>”Class" from the context menu that appears.  In the Add New Item window enter the name “Extensions.cs” and hit enter.

  3. Make the Extensions class static and add the following ToHexString() extension method as shown below.  You'll also need to add "using System.Linq;" to the code file:

        public static class Extensions
        {
            public static string ToHexString(this byte[] byteArray)
            {
                return string.Join(string.Empty, byteArray.Select(b => b.ToString("X")));
            }
        }

  4. Now open the TaskService.cs file in the code editor and add the following two lines of code to the GetTask() operation directly before returning the task within the using code block:

        string etag = task.Version.ToHexString();
        WebOperationContext.Current.OutgoingResponse.SetETag(etag);

    By calling the SetETag() method for the outgoing response we ensure that an HTTP ETag header with the task version value is included with the response.

  5. The complete  GetTask() operation should look like the following:

        [Description("Returns the details of a single task.")]
        [WebGet(UriTemplate = "{id}")]
        public Task GetTask(string id)
        {
            int parsedId = ParseTaskId(id);
            using (TeamTaskObjectContext objectContext =
                          new TeamTaskObjectContext())
            {
                var task = objectContext.Tasks 
                                          .FirstOrDefault(t => t.Id == parsedId);
                if (task == null)
                {
                    ThrowNoSuchTaskId(parsedId);
                }

                string etag = task.Version.ToHexString();
                WebOperationContext.Current.OutgoingResponse.SetETag(etag);
                    
                return task;
            }
        }

Helpful Tip:   The SetETag() method is found on the OutgoingResponse of the WebOperationContext, which also provides access to the response headers and even an ETag property.  However, the SetETag() method will ensure that the ETag is correctly quoted as per the HTTP specification so you should use it instead of the headers collection or the ETag property.

 

Step 2: Adding Conditional GET Support

In step one of this blog post we briefly discusses the concept of a conditional GET.  If a client has already sent a previous request for a resource and received a response along with an ETag, the client can send that ETag in an HTTP If-None-Match header on future requests.  If the state of the resource hasn’t changed since the previous request, the server can send back a response without a body and an HTTP status code of 304 (Not Modified).  Of course, if the state of the resource has changed since the previous request, the server will respond with the new state of the resource and a new ETag. 

In the next step, we’ll update our client to use ETags and send conditional GET requests so we’ll see conditional GET in action.  But first we need to edit the GetTasks() operation of the TaskService service class so that it checks for the HTTP If-None-Match header.  We’ll add this check for the If-None-Match header with a single call to the new CheckConditionalRetrieve() method from the WebOperationContext. 

The CheckConditionalRetrieve() method takes the current ETag as an argument and tries to match it against the ETags in the If-None-Match header of the request.  If there is a match then the client already has the current version of the resource so the CheckConditionalRetrieve() method throws a WebFaultException with an HTTP status code of 304 (Not Modified) just as the HTTP specification prescribes.  If there is no If-None-Match header in the request or if none of the ETags in the header match the current ETag, then the CheckConditionalRetrieve() method returns so that service operation can complete as normal. 

  1. Now open the TaskService.cs file in the code editor and add a call to the CheckConditionalRetrieve() method in the GetTask() operation like so:

        string etag = task.Version.ToHexString();
        WebOperationContext.Current.IncomingRequest
            .CheckConditionalRetrieve(etag);
        WebOperationContext.Current.OutgoingResponse.SetETag(etag);

Helpful Tip:   In part seven of this blog post series we discussed how ASP.NET server and client-side caching functionality can be leveraged by a WCF WebHttp service in .NET 4.  It's important to consider how ETags and conditional GET interacts with server and client-side caching.  

For example, conditional GET support requires checking the ETag from the request  within the service operation.  However, if server-side caching has naively been enabled, two requests with different ETags (one current, the other stale) may both receive the same response from the ASP.NET output cache.  The correct way to compose server-side caching and conditional GET is to cache different responses based on the value of the HTTP If-Modified-Since header from the requests.  Therefore when a request with a new ETag value is received there will be a cache-miss and the service operation will execute.   In step two of part seven of this blog post series we discussed how to cache based on request headers. 

The right caching mechanisms for your service will depend on the type of resources your service exposes.  If a resource changes rarely (or at known intervals) and you expect lots of different clients to only request the resource once, then server-side caching makes sense because you can serve those requests from the cache.  If you expect clients to poll an often-changing resource for updates, then you'd probably want to include conditional GET support.    

 

Step 3: Updating the Client to use ETags

Now that we’ve implemented the GetTask() service operation to both return ETags and support conditional GET requests, let’s update the client to take advantage of this new functionality.

We’ll first have the client send a GET request for a task.  The client won’t yet have an ETag for the task, so it won’t include one.  However, we’ll capture the ETag from the response and send a second request with the same ETag in an If-Modified-Since header.  With this second request we’ll see that the TeamTask service will return a 304 (Not Modified) response and no message body.  We’ll then update the task and send a third request with the original ETag.  Since we’ve updated the task, the original ETag is stale and the response to the third request will include the updated task and a new ETag.

  1. Open the Program.cs file of the TeamTask.Client project in the code editor and replace the current implementation of the static GetTask() method with the implementation below.  You’ll also need to add “using Microsoft.Http.Headers;” and  “using System.Net;” to the code file:

        static Task GetTask(HttpClient client, int id, ref EntityTag etag)
        {
            Console.WriteLine("Getting task '{0}':", id);
            using (HttpRequestMessage request =
                          new HttpRequestMessage("GET", "Tasks/" + id))
            {
                request.Headers.Accept.AddString("application/json");
                if (etag != null)
                {
                    request.Headers.IfNoneMatch.Add(etag);
                }
                using (HttpResponseMessage response = client.Send(request))
                {                
                    if (response.StatusCode == HttpStatusCode.NotModified)
                    {
                        Console.WriteLine(
                            "The task with id '{0}' has not been modified.", id);
                        Console.WriteLine();
                        return null;
                    }
                    else
                    {
                        etag = response.Headers.ETag;
                        response.EnsureStatusIsSuccessful();
                        Console.WriteLine("Etag: {0}", etag.Tag);
                        WriteOutContent(response.Content);
                        return response.Content.ReadAsJsonDataContract<Task>();
                    }
                }
            }
        }

    This new implementation of the GetTask() method is similar to the client code that we created in part two of this blog post series except that we’ve added the conditional GET functionality.  Before sending the request, we include the If-None-Match header by calling the IfNoneMatch.Add() method on the headers collection.  After receiving the response we check the status code for a value of 304 (Not Modified).

  2. Replace the Main() method implementation with the following code:

        using (HttpClient client = new HttpClient("https://localhost:8080/TeamTask/"))
        {
            EntityTag etag = null;
                    
            // Get the task and the etag
           Task task = GetTask(client, 2, ref etag);

            // Get the task a second time and include the etag
            GetTask(client, 2, ref etag);

            // Update the task;
            task.Status = (task.Status == TaskStatus.Completed) ?
                TaskStatus.InProgress :
                TaskStatus.Completed;
            UpdateTask(client, task);

            // Get the task again... the etag should be stale
            GetTask(client, 2, ref etag);

            Console.ReadKey();
        }

  3. Start without debugging (Ctrl+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 and selecting “Debug”—>”Start New Instance”.  The console should contain the following output:

    ClientUsingEtagsInConsole

    Notice that the second response for task '2' has a status code of 304 (Not Modified) and that the ETag returned for the third request is different than the ETag returned from the first request because of the update that occurred between the two requests.

Next Steps: Optimistic Concurrency Support in WCF WebHttp Services

In this part of the series we’ve shown how easy it is to add ETag and conditional GET functionality to a WCF WebHttp service in .NET 4.  In part eleven of this blog post series we’ll explore how ETags can be used to implement optimistic concurrency in a WCF WebHttp service in order to prevent clients from unknowingly updating data that has changed since they last retrieved it.  

Randall Tombaugh
Developer, WCF WebHttp Services

Post10-ConditionalGet.zip