Optimistic concurrency control in ASP.NET WebAPI

[warning: long post 🙂 ]

Setting the stage

The first question, and an expected one would be: what has to do ASP.NET Web API with concurrency control? Until now, in most applications, this problem was approached at persistence layer level or even on database server level: when implementing optimistic concurrency control, if during an update we detect that the data was modified in the meanwhile (since it was read), probably by another user or system, usually a specific exception was thrown (or a certain error code was returned) and the client application was supposed to treat this case one way or another – usually by displaying a dialog to the user that will allow to choose a way to solve the conflict, as appropriate: by cancelling his own changes, overwriting the changes done by the other user or by merging the changes, if it’s possible and depending on the application requirements.

I won’t go into details about the “theory” of optimistic concurrency control, when it’s used and when not, how it’s implemented etc. – it’s known stuff. What is less known is that HTTP (as an application-level protocol), among many other features, comes with a built-in feature that can be used (among other things) for making the optimistic concurrency control mechanism explicit: ETags. Usually used for caching in HTTP, ETags have another possible use, suggested even by the definition from the standard: “Entity tags are used for comparing two or more entities from the same requested resource. … An entity tag MUST be unique across all versions of all entities associated with a particular resource.”

Even if the terminology is a bit different, we realize that one resource (or document) could be associated to one (or more) records from a database, the ‘entities’ can be regarded as in-memory object instances (loaded from a database entity) – all this as an approximate equivalence.

Also as an analogy, an eTag could be considered (or implemented) as a ‘version’, ‘timestamp’ associated to a database record, in general an opaque value, without a meaning for the user. We realize that the ETag, apart from it’s usage for caching, it’s an ideal candidate as a standard way to transport the values used for concurrency control. The method is not something unheard of, being suggested even by those who worked at the standard long time ago (http://www.w3.org/1999/04/Editing/) and it’s used by various document/content management systems (where it appears under the name of “unreserved checkout”).

What have all of these to do with ASP.NET WebAPI? They have, because Web API tries to provide support for HTTP-based services, where HTTP is made explicit as an application-level protocol, instead of being hidden/abstracted away.

Back on earth..
Enough theory for now. How can be an ETag used in a practical way to offer support for optimistic concurrency control, at HTTP level?
Some pseudocode:

  • a client application makes a request for a certain resource: GET /products/23
  • if the resource does exists, the server will send back a response that will contain, apart from the resource itself, the ETag value for that resource, as a field in the HTTP header (ex.: ETag: “s2hk707Mvk+GqzxNe+lbOQ==”)
  • the client application displays the resource, the user (1) modifies it (on the client)
  • during this time, another user (2) (or another system) can edit and modify the resource without problems. On the server, the ETag is updated when the resource is persisted
  • the first user (1) finishes it’s changes and press a ‘Save’ button
  • the client application sends a PUT request to the server, with the modified resource and the original ETag, as a field in the HTTP header, like:
    If-Match: “<original ETag value>”
  • on the server, if we can verify (one way or another) that the resource was modified in the meanwhile (usually by comparing the original and current etags), the update won’t be performed and a response will be returned with 412 (Precondition Failed) code, where ‘precondition’ is the “If-Match” condition from the change request (some services return 409 Conflict, so be prepared fro that)
  • it’s up to the client application to decide what to do with this response, like: display to the user a message that let him to choose between different ways to solve the conflict (“owerwrite the changes done by other users”, “cancel my changes” etc.), optionally by showing a list of changes done by the other user

How does ASP.NET WebAPI helps us to implement something like this?
Easy: the actions from an ApiController offer an easy access to the HTTP headers – the rest is up to us. I let the code speak by itself 🙂

(if you read this a few years from now: the code is tested using .NET Framework 4.5 beta – it might be changed until the final release, as it happened in the past)

    public class ProductsController : ApiController
    {
        private static IList _products = new List(); // dummy product repository

        static ProductsController()
        {
            // dummy data
            _products.Add(new Product() { Id = 10, Code = "P1", Description = "Product 1", Price = 123.45m,
                                          Version = Guid.NewGuid().ToByteArray() });
            _products.Add(new Product() { Id = 11, Code = "P2", Description = "Product 2", Price = 567.47m,
                                          Version = Guid.NewGuid().ToByteArray() });
            _products.Add(new Product() { Id = 12, Code = "P3", Description = "Product 3", Price = 100.22m,
                                          Version = Guid.NewGuid().ToByteArray() });
        }

        // GET /api/products
        public IEnumerable Get()
        {
            return _products;
        }

        // GET /api/products/11
        public HttpResponseMessage Get(int id)
        {
            Product prod = (from p in _products
                         where p.Id == id
                         select p).FirstOrDefault();
            if (prod == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            else
            {
                var response = new HttpResponseMessage(prod, HttpStatusCode.OK);
                response.Headers.ETag = new System.Net.Http.Headers.EntityTagHeaderValue("\""
                    + Convert.ToBase64String(prod.Version, Base64FormattingOptions.None) + "\"");
                return response;
            }
        }

        // PUT /api/values/5
        public void Put(int id, Product product)
        {
            // retrive the existing product from persitence
            Product existingProduct = (from p in _products
                            where p.Id == id
                            select p).FirstOrDefault();

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

                //update the product
                existingProduct.Code = product.Code;
                existingProduct.Description = product.Description;
                existingProduct.Price = product.Price;
                // this should be done by the persistence layer (DB, etc..)
                existingProduct.Version = Guid.NewGuid().ToByteArray();

            }
            else
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
        }

        private void CheckIfProductWasModified(HttpRequestHeaders requestHeaders, Product existingProduct)
        {
            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 != "*")
                    )
                {
                    // compare the old and new ETag value (this can be done at DB-level using a WHERE clause)
                    string encodedNewTagValue = firstHeaderVal.Tag.Trim("\"".ToCharArray());
                    string encodeExistingTagValue = Convert.ToBase64String(existingProduct.Version, Base64FormattingOptions.None);
                    if (!encodedNewTagValue.Equals(encodeExistingTagValue, StringComparison.Ordinal))
                    {
                        // concurrency conflict if the resource was modified
                        throw new HttpResponseException(HttpStatusCode.PreconditionFailed);
                    }
                }
            }
        }

        // POST /api/products
        public HttpResponseMessage Post(Product product)
        {
            _products.Add(product);
            var response = new HttpResponseMessage(product, HttpStatusCode.Created);
            response.Headers.Location = new Uri(Request.RequestUri,
                "/api/products/" + product.Id.ToString(CultureInfo.InvariantCulture));
            return response;
        }

    }

The highlighted lines show the relevant parts:
– at GET, we set the ETag for the returned resource, by using HttpRespnseMessage.Headers.ETag (the value must be enclosed in double quotes (“)
GET with ETag
– at PUT (modify), if this.Request.Headers.IfMatch has a value (that it’s not “*”), we compare it with the ETag of the persisted resource (in DB or cache)
– if they don’t match, we return a response with HttpStatusCode.PreconditionFailed
PUT with ETag

Finally, some tests that show what we expect to happen:

    [TestClass]
    public class ProductsControllerIntegrationTests
    {
        private HttpClient _client;
        private string _productUri = "http://ipv4.fiddler/TestETag/api/products/11";
        private readonly string jsonMediaType = "application/json";

        [TestInitialize]
        public void Init()
        {
            _client = new HttpClient();
        }

        [TestMethod]
        public async Task Get_ReturnsTheETagInTheHeaders()
        {
            HttpResponseMessage httpResp = await _client.GetAsync(_productUri);

            Assert.IsNotNull(httpResp.Headers.ETag, "No ETag was included in the response headers!");
            Assert.IsTrue(!string.IsNullOrEmpty(httpResp.Headers.ETag.Tag), "An empty ETag was received!");

        }

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

            ///////////////
            // second user reads the product 11
            HttpResponseMessage getProdUser2 = await _client.GetAsync(_productUri);
            getProdUser2.EnsureSuccessStatusCode();
            Product prodUser2 = await getProdUser2.Content.ReadAsAsync();

            //second users modifies (PUT) and persist the product
            prodUser2.Description = prodUser2.Description + " modif by user 2";

            var putRequest2 = GetPutRequestMessage(prodUser2);
            // we have to use SendAsync in order to be able to set any header, including If-Match
            HttpResponseMessage putUser2Response = await _client.SendAsync(putRequest2);
            putUser2Response.EnsureSuccessStatusCode();

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

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

            /////////////////////
            Assert.AreEqual(HttpStatusCode.PreconditionFailed, putUser1Response.StatusCode,
                            "The response was not precondition failed!");

        }

        private HttpRequestMessage GetPutRequestMessage(Product product)
        {
            var requestMessage = new HttpRequestMessage(HttpMethod.Put, _productUri);
            // add an 'If-Match' header
            requestMessage.Headers.IfMatch.ParseAdd("\"" + Convert.ToBase64String(product.Version) + "\"");
            // add the modified object serialized as JSON
            ObjectContent prodObjContent =
                requestMessage.CreateContent(
                product,
                MediaTypeHeaderValue.Parse(jsonMediaType),
                new MediaTypeFormatter[] { new JsonMediaTypeFormatter() },
                new FormatterSelector());

            return requestMessage;
        }

        [TestCleanup]
        public void Cleanup()
        {
            if (_client != null)
            {
                _client.Dispose();
            }
        }

    }

Obviously, it doesn’t matter the language or framework used on the client – I used HttpClient from ASP.NET Web API because it was at hand, but I could use JavaScript, C++ or something else.

For a real application, of course, there are a few more steps to be done: a lot of refactoring for having some reusable code, extracting the code dealing with concurrency checks in a separate class – separation of concerns (probably using a DelagatingChanell, like it’s described at http://javiercrespoalvez.com/2011/06/etags-and-optimistic-concurrency.html or http://codebetter.com/howarddierking/2011/07/01/automatic-etag-management-with-web-api-message-handlers/), using a real persistence solution (like a database). Obviously, the above tests are only some ‘integration’ tests – some real unit tests would be useful.

What is not relevant in the above code:
– the format used for ETag – I used byte[] only because it’s easier to map to rowversion columns from MS SQL Server, when using EF, but equally good would be an int or GUID as long as make sure it’s unique
– the way in which the ETag is encodded in the HTTP header: I used Convert.ToBase64String only because it’s a convenient and safe way to encode an array of bytes (in both directions)
– the format used to serialize the resource (entity) – it can be JSON as above, but also XML or something else

What is not discussed in this post: the role played by ETag in HTTP caching, that is important, if used.

A bit of context:
In those cases when our REST-like service is simple enough to be exposed using the OData protocol proposed by Microsoft, the protocol will use ETags for concurrency support: http://www.odata.org/documentation/operations#ConcurrencycontrolandETags
Also the Microsoft framework that uses the OData protocol, like WCF Data Services (former ADO.NET Data Services, codename Astoria) is using HTTP Etags for concurrency, so when we can use that, it’s already baked for us (http://msdn.microsoft.com/en-us/data/hh127792 ; http://blogs.msdn.com/b/astoriateam/archive/2008/04/22/optimistic-concurrency-data-services.aspx).
Even if it might not be obvious, the OData protocol (based on HTTP) can be found in many places: Sharepoint 2010 services, Excel services, Azure Storage.

Outside Microsoft world, Raven DB API also is using ETags for concurrency: http://ravendb.net/docs/http-api/http-api-comcurrency, and some GData (Google Data) services do the same: https://developers.google.com/gdata/docs/2.0/reference#ResourceVersioning

Later edit: a short follow up: part 2

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

16 Responses to Optimistic concurrency control in ASP.NET WebAPI

  1. Excellent post! I appreciated the extra information at the end – I didn’t know OData, RavenDB, and GData were using ETags.

  2. Micle says:

    Good article

    We can also submit our .net related article links on http://www.dotnettechy.com to get more traffic.

    This is a community of .Net developers joined together to find solutions, to learn, to teach, to find interview questions and answers, to find .net website / blog collection

  3. Bruno says:

    Awesome!

  4. Henry says:

    Thanks Tudor for great article.

    I’m just curious why you storing Guid in Byte array. Why don’t just keep it as it is?
    Is the only reason because you need that for Base64 encoding..

    Cheers,

    • Henry says:

      Ahh. Sorry Ignore my question..

      Didn’t read “What is not relevant in the above code:” section clearly.. mybad 😦

      • Tudor Turcu says:

        Indeed 🙂 , that byte[] was just an opaque container that could store a Guid or the content of a rowversion column read from a MS SQL database.. (previouly known as timestamp in older TSQL versions)

  5. Pingback: Optimistic concurrency support in HTTP and WebAPI – part 2 | Tudor Turcu – blog

  6. Dan Miser says:

    Absolutely brilliant post. Thank you.

  7. Gorka Lerchundi says:

    What about race conditions between (inside Put method) Product retrieving and a upcoming update?

    • Tudor Turcu says:

      indeed, the Put method is not thread-safe – in a real implementation the update and the etag comparation would be done at DB level, in an atomic operation (probably as a single update statement with an extra where clause), and the Put method should catch the exception and return the proper http error code.

  8. How can a client get the version of an entity which has been retrieved as part of a list using the first Get() action?

    • Tudor Turcu says:

      Indeed, when retrieving a list of products using a simple GET, the version for each product can’t be included in the (single) etag http header – in my example the version is included as a property of each product in the response..

      • J. Johnson says:

        Can you please elaborate on how you would supply the ETAG when you want to return a LIST of objects from the Api controller ? Otherwise, great article! Thanks.

      • Tudor says:

        The ‘etag’ in that case (list of objects) is just the Version property of each object in the list, it’s not being send at HTTP level:
        _products.Add(new Product() { Id = 12, Code = “P3”, Description = “Product 3”, Price = 100.22m,
        Version = …
        });
        }

  9. Pingback: Using ETags in SharePoint REST Calls to Manage Concurrency Control | Marc D Anderson's Blog

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