Tuesday, June 28, 2011

ETags and optimistic concurrency control with WCF Web Apis

In my previous post I wrote about how to take advantage of ETags to implement HTTP cache validation with WcfWebApis, but this is not the only use of ETags, you can also use them to implement optimistic concurrency control over HTTP. The following workflow illustrates this feature:

  1. The client wants to get a resource and for that it sends a GET request and the server answers with the resource representation and its ETag.
  2. The client decides to update the resource and for that it sends a PUT request with the modified resource representation along with the ETag from the previous step.
  3. The server checks the incoming ETag against the current resource representation ETag from its repository (i.e: Cache, DB).
    • If they match, the server carries on with the resource update and returns a 200 HTTP status if it succeeds.
    • If they don’t match, it’s because someone else has updated the resource in the meantime. Therefore the server invalidates the request and returns a 402 Precondition Failed status. (Concurrency error)

Now replace “GET” by “SELECT” and “PUT” by “UPDATE… WHERE” and this is exactly the same optimistic concurrency mechanism that you can usually find implemented in relational databases.

Enough of theory, we can implement step 3 from the workflow with a DelegatingChannel. The channel takes advantage of the ETag cache that I used in my previous post.

public class EntityTagConcurrencyChannel : DelegatingChannel
{
    private readonly ETagCache _eTagCache = ETagCacheProvider.Instance;
 
    public EntityTagConcurrencyChannel(HttpMessageChannel innerChannel)
        : base(innerChannel)
    {
 
    }
 
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, 
                                                  CancellationToken cancellationToken)
    {
        if (request.Method == HttpMethod.Put && 
            request.Headers.IfNoneMatch.Any())
        {
            var etag = request.Headers.IfNoneMatch.First().ToString();
            if (etag != "*")
            {
                var cached = _eTagCache.Get(request.RequestUri.ToString());
                return etag != cached
                  ? Task.Factory.StartNew(() => CreatePreconditionFailedResponse(),
                                                    cancellationToken)
                  : base.SendAsync(request, cancellationToken);
            }
        }
        return base.SendAsync(request, cancellationToken);
    }
 
    private static HttpResponseMessage CreatePreconditionFailedResponse()
    {
        return new HttpResponseMessage(HttpStatusCode.PreconditionFailed, 
                                        "If-None-Match");
    }
}
As per the HTTP specification, the special wildcard “*” value makes the server not to run the validation.

No comments:

Post a Comment