Sunday, May 18, 2014

OPTIMISTIC CONCURRENCY CONTROL USING ETAG

This is the scenario: you have a CRM system where the editors can change customer details. The CRM user interface is a web application which will be used by several editors. There is a chance that multiple editors will edit the same customer simultaneously.
Since the HTTP protocol is stateless there is a chance that an editor can overwrite changes made after the editor loaded the “edit customer” web page.
To solve this you can make use of an ETag containing a value representation of the customer data, preferably a changed date. By submitting that value when initially sending the page to the web client and then posting the value back along with the new customer details the values can be compared. The comparison will result in either accepting or rejecting the changed customer information.
The HTTP specification (http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24) states that if the If-Match HTTP header value is not a representation of the current entity the server should return status code 412 (Precondition Failed) and not persist the data. Otherwise, return 200 (OK).
When loading the page you submit the ETag either in the header or in the body. When the customer details are sent back to the server using a PUT request you pass the ETag value in the If-Match HTTP header.
If you are utilizing an ASP.NET MVC solution with AngularJS (without using SPA) and ASP.NET Web API you can solve this by doing the following.
GET request – when loading the page with the customer information
Pass a representation of the ETag through the MVC model from the MVC controller and make it accessible from your Angular controller. I use a sort of initial data collection which will populate an AngularJS scope variable when the page is loaded.
PUT request – when passing the changed data back to the server
The data is passed from the UI through an AngularJS $http.put request
var config = {
method: 'PUT',
url: '/customer',
data: {  },
headers: { 'If-Match': $scope.etag }
// $scope.etag is initiated during loading of page
};
$http(config)
.success(function (response) {
// notify user that update was ok
})
.error(function (data, status) {
if(status == '412'){
// notify the editor that customer has already been updated by someone else and should reload the page to get the new customer data.
}
});
The receiving end which is the Web API controller
public HttpResponseMessage Put(CustomerData customer)
{
var customer = GetCustomerFromDatabase(customer.Id);
var isAlreadyModified = IsAlreadyModified(customer);
if (isAlreadyModified)
{
// return status code 412 if the customer has already been changed during the editing
return Request.CreateErrorResponse(HttpStatusCode.PreconditionFailed, "Customer has already been modified. Please reload the page and redo your changes.");
}
return Request.CreateResponse(HttpStatusCode.OK);
}
private bool IsAlreadyModified (Customer customer)
{
// using the ticks as etag
var ourEtag = customer.ChangedDate.Ticks.ToString(CultureInfo.InvariantCulture)
: string.Empty;
var theirEtag = Request.Headers.IfMatch.ToString();
return ourEtag.Equals(theirEtag, StringComparison.InvariantCultureIgnoreCase) == false;


}