Search code examples
c#odataetagoptimistic-concurrencyasp.net-web-api-odata

Optimistic Concurrency Handling - Asp.Net WebApi Odata V4


This is the Patch method of my OdataController

public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Product> patch)
{
   Validate(patch.GetInstance());
   Product product = await service.Products.GetAsync(key);
   if (product == null)
       return NotFound();

   patch.Put(product);

   try
   {
       await service.Products.UpdateAsync(product);
   }
   catch (DbUpdateConcurrencyException)
   {
       if (!await service.Products.ExistAsync(key))
           return NotFound();
       else
           throw;
   }

   return Updated(product);
}

My model has a property:

[Timestamp]
 public byte[] RowVersion { get; set; }

the DbUpdateConcurrencyExceptionseems not working at all. I need to implement concurrency checking mechanism using Etag. I have seen some examples here.But they are not using Delta in there method.

  1. How can I check Concurrency using etags?
  2. Is it possible to implement a custom attribute for concurrency cheacking?

Something Like:

[CustomConcurrencyCheck]
public async Task<IHttpActionResult> Put([FromODataUri] int key, Delta<Product> patch)
{
...
}

Providing a simple example will be highly appreciated.


Solution

  • First in WebApiConfig while creating your model you have to specify which property is the ETag, in your case:

    var builder = new ODataConventionModelBuilder();
    builder.EntityType<Product>()
        .Property(p => p.RowVersion)
        .IsConcurrencyToken();
    

    Later you can retrieve the ETag from the ODataQueryOptions<T> parameter of your Patch method in the controller:

    [AcceptVerbs("PATCH", "MERGE")]
    public IHttpActionResult Patch([FromODataUri] int key, Delta<Product> delta, ODataQueryOptions<Product> options) {
        var existingEntity = //Code to get existing entity by ID, 404 if not found
    
        byte[] requestETag = options.IfMatch["RowVersion"] as byte[];
        if(!requestETag.SequanceEqual(existingEntity.RowVersion)) { //Simplified if-statement, also do null-checks and such
            // ETags don't match, return HTTP 412
            return StatusCode(HttpStatusCode.PreconditionFailed);
        } else {
            // The ETags match, implement code here to update your entity
            // You can use the 'Delta<Product> delta' parameter to get the changes and use the 'Validate' function here
            // ...
    

    This is the solution I use, it is a simple check to see if the client which requests the update has the same version of the object the service has. A notable drawback of my solution is that I have to retrieve the existing object from the DB to get it to work, that costs some performance.

    This is code for the If-Match header, ODataQueryOptions<T> also has .IfNoneMatch[VersionColumnName] available. Which you can use in your Get method. If the If-None-Match header equals your RowVersion you can return a HTTP 304 (Not modified) and save some bandwidth.

    This is a very simple example, if you want to implement your own custom attribute that's up to you. At the very least I would move some of this logic to a helper class so it can be reused.