Optimistic concurrency support in HTTP and WebAPI – part 2

A short follow-up to the latest post on Optimistic concurrency control in ASP.NET WebAPI.

One question that might come up is: what should we do if, after changing a resource (entity), the client send a PUT request with the changes, but forgets to specify the original ETag value (in the ‘If-Match’ HTTP header)?
In my sample code, I just ignore this case and let the code continue, the update is performed, and the concurrency conflict is not detected – the changes done by the second user are lost (lost update case).

How can we avoid this? In the HTTO and REST ‘spirit’, we should return some error code, but which one?
Fortunately, a very recent proposal for the HTTP standard comes to rescue: RFC 6585 ‘Additional HTTP Status Codes’: http://www.rfc-editor.org/rfc/rfc6585.txt.

A new HTTP status code (428 Precondition Required) is proposed, that the request from the client must be conditional, in our case ‘If-Match’:

The 428 status code indicates that the origin server requires the request to be conditional.
Its typical use is to avoid the “lost update” problem, where a client GETs a resource’s state, modifies it, and PUTs it back to the server, when meanwhile a third party has modified the state on the server, leading to a conflict. By requiring requests to be conditional, the server can assure that clients are working with the correct copies.

How do we add support in our web service, using ASP.NET Web API?
Quite easy: in the code from my last post, just add this:

    public class ProductsController : ApiController
    {

        // PUT /api/values/5
        public void Put(int id, Product product)
        {
            // retrive the existing product from persitence
            // ...

            if (existingProduct != null)
            {
                // perform concurrency conflict check
                CheckIfProductWasModified(this.Request.Headers, existingProduct);

                //update the product
                // ...
            }
            else
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
        }

        private void CheckIfProductWasModified(HttpRequestHeaders requestHeaders, Product existingProduct)
        {
            bool isConditional = false;

            if (requestHeaders.IfMatch != null)
            {
                // if the request contains an If-Match haeder with a non-empty ETag
                EntityTagHeaderValue firstHeaderVal = requestHeaders.IfMatch.FirstOrDefault();
                if ((firstHeaderVal != null) && (!string.IsNullOrEmpty(firstHeaderVal.Tag))
                    && (firstHeaderVal.Tag != "*")
                    )
                {
                    isConditional = true;
                    // compare the old and new ETag value (this can be done at DB-level using a WHERE clause)
                    // ...
                }
            }

            if (!isConditional)
            {
                throw new HttpResponseException(CreatePreconditionRequiredResponse());
            }
        }

        private static HttpResponseMessage CreatePreconditionRequiredResponse()
        {
            var resp = new HttpResponseMessage((HttpStatusCode)428)
            {

                Content = new StringContent(
@"<html>
      <head>
         <title>Precondition Required</title>
      </head>
      <body>
         <h1>Precondition Required</h1>
         <p>This request is required to be conditional;
         try using ""If-Match"".</p>
      </body>
   </html>"),
                ReasonPhrase = "Precondition Required",    
            };

            return resp;
        }


      // ...

    }

And a quick test to check if the service returns the right response in such cases:

        [TestMethod]
        public async void Put_IfEtag_Is_Missing_Returns_PreconditionRequired_Status()
        {
            // first user reads the product 11
            HttpResponseMessage getProdUser1 = await _client.GetAsync(_productUri);
            getProdUser1.EnsureSuccessStatusCode();
            Product prodUser1 = await getProdUser1.Content.ReadAsAsync<Product>();

            ///////////////
            // first user modifies (PUT) and tries to persist the same product
            prodUser1.Description = prodUser1.Description + " modif by user 1";

            var putRequest1 = GetPutRequestWithoutETagMessage(prodUser1);
            HttpResponseMessage putUser1Response = await _client.SendAsync(putRequest1);

            /////////////////////
            Assert.AreEqual(428, (int)(putUser1Response.StatusCode),
                            "The response was not precondition required!");
        }

        private HttpRequestMessage GetPutRequestWithoutETagMessage(Product product)
        {
            var requestMessage = new HttpRequestMessage(HttpMethod.Put, _productUri);
            
            // add the modified object serialized as JSON
            ObjectContent prodObjContent =
                requestMessage.CreateContent<Product>(
                product,
                MediaTypeHeaderValue.Parse(jsonMediaType),
                new MediaTypeFormatter[] { new JsonMediaTypeFormatter() },
                new FormatterSelector());

            return requestMessage;
        }

Above, we have to “force” a status code, because ASP.NET WebAPI does not know yet of 428 status code.

A natural question comes up: is there any advantage of using a specific status code in this case?
If we use it just to signal a bug in our client code, there is little value. But, if we expose a public API, a third-party client could more easily “learn” our web service and adapt to it, without having to read many pages of documentation.

Advertisements
This entry was posted in .NET, C#, Web and tagged , , , , , . Bookmark the permalink.

2 Responses to Optimistic concurrency support in HTTP and WebAPI – part 2

  1. Pingback: Optimistic concurrency control in ASP.NET WebAPI | Tudor Turcu – blog

  2. Pingback: friday links 30 « A Programmer with Microsoft tools

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s