In this article, we’ll discuss a few approaches to implementing optimistic concurrency in ASP.NET Core Web API.

To download the source code for this article, you can visit our GitHub repository.

Let’s begin.

Understanding Optimistic Concurrency

There are two different approaches for managing concurrent access to shared resources – Optimistic and Pessimistic concurrency.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Optimistic Concurrency is a concurrency control method that assumes update conflicts will occur rarely. That means it will allow updating records without locking the rows or imposing any other restrictions. It checks for conflicts just before updating a record. In case a conflict is detected, it will not perform the update and instead ask the user to refresh the data and retry the operation.

This approach typically ensures higher concurrency and scalability as there is no need to lock resources exclusively.  

This is in contrast to Pessimistic Concurrency, which assumes that update conflicts are likely to happen. So this approach allows updating records only after gaining exclusive access to them by locking them. Once a user or process locks a resource, others cannot access it and will have to wait till the operation is complete. This can lead to reduced concurrency and scalability as it will lock the resources.

Pessimistic concurrency can also lead to deadlock when multiple users or processes wait for each other to release locked resources.    

Optimistic Concurrency With Database-Generated Tokens

The most common way of handling optimistic concurrency is by using a concurrency token. Let’s see how to generate a concurrency token from the database and use it in an ASP.NET Core Web API application.

Creating the Application

Let’s start by creating an ASP.NET Core Web API application using the dotnet new webapi command or by using Visual Studio templates.

After that, let’s create a Product model class:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    
    [Timestamp]
    public byte[]? RowVersion { get; set; }
}

Notice that we decorate the RowVersion property with the Timestamp attribute. By doing so, EF Core will treat it as a concurrency token. That means EF Core will automatically generate and manage the value for that field. Not just that, before performing an update or delete operation, it makes sure that the RowVersion value is the same by adding the check in the WHERE clause.

In case it is different, that means someone else modified the record after we fetched it and EF Core throws a DbUpdateConcurrencyException.

Next, let’s add a new API Controller to perform the CRUD operations.

It is easier to use the Visual Studio Scaffolding option to create the DbContext and Controller. For that we just need to right-click on the Controllers folder and then select Add – New Scaffolded Item from the menu. From there, we can choose the API Controller with actions, using the Entity Framework option. Once we choose this option, we just need to choose the Product model class and decide to create a new data context by giving a name:

optimistic concurrency scaffolding

Visual Studio will create the data context and add the API controller action methods for us. Of course, we can create everything manually if we prefer to do it that way. Make sure to update the database connection string in the appsettings file.

Now let’s add the database migrations and update the database. For more information about database migrations in EF Core please refer to our excellent article on Migrations and Seed Data With Entity Framework Core.

Let’s start by adding an initial migration:

add-migration initial-create

After that, let’s apply the changes to the database:

update-database

This will create a Product table in the database. Notice that the RowVersion column is of type timestamp.

The Action Methods

Let’s take a look at the POST action method:

[HttpPost]
public async Task<ActionResult<Product>> PostProduct(Product product)
{
    if (_context.Product is null)
    {
        return Problem("Entity set 'ProductsDbContext.Product'  is null.");
    }
    _context.Product.Add(product);
    await _context.SaveChangesAsync();

    return CreatedAtAction("GetProduct", new { id = product.Id }, product);
}

There isn’t much here; it just creates a new product record with the supplied values.

Now let’s take a look at the PUT action method:

[HttpPut("{id}")]
public async Task<IActionResult> PutProduct(int id, Product product)
{
    if (id != product.Id)
    {
        return BadRequest();
    }

    _context.Update(product);

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!ProductExists(id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return NoContent();
}

It retrieves the entity entry from the context and sets its state to Modified. After that, it tries to update the database. Notice that we catch the DbUpdateConcurrencyException exception and throw it in case we detect a conflicting update.

Testing Optimistic Concurrency

Now let’s run the application and create a new product record using the POST endpoint:

curl -X 'POST' \
  'https://localhost:7048/api/Products' \
  -H 'accept: text/plain' \
  -H 'Content-Type: application/json' \
  -d '{ 
  "name": "Bread",
  "price": 5
}'

This will produce a 200 Success response and create a new record in the database:

{
  "id": 1,
  "name": "Bread",
  "price": 5,
  "rowVersion": "AAAAAAAAJx4="
}

Notice that the database auto-populates the value for the RowVersion column along with the Id.

Next, let’s update the Product record by using the PUT endpoint. Notice that if we try to update the record without passing the RowVersion, it will throw the DbUpdateConcurrencyException. So let’s pass the current RowVersion as well as the request:

curl -X 'PUT' \
  'https://localhost:7048/api/Products/1' \
  -H 'accept: */*' \
  -H 'Content-Type: application/json' \
  -d '{
  "id": 1,
  "name": "Bread",
  "price": 6,
  "rowVersion": "AAAAAAAAJx4="
}'

When we execute this request, we can see that EF Core automatically adds a check to see if the RowVersion matches the Id in the WHERE clause:

UPDATE [Product] SET [Name] = @p0, [Price] = @p1
OUTPUT INSERTED.[RowVersion]
WHERE [Id] = @p2 AND [RowVersion] = @p3;

Of course, this will update the record and change the RowVersion as well:

{
  "id": 1,
  "name": "Bread",
  "price": 6,
  "rowVersion": "AAAAAAAAJx8="
}

If another user is working on the same record and tries to perform the update operation using the old RowVersion, it will throw an error:

Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: The database operation was expected 
to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted 
since entities were loaded.

As the error clearly explains, someone else modified the row after we loaded it and hence the operation fails with DbUpdateConcurrencyException.

This way, we can detect concurrency errors and prevent accidental overwrites. Ideally, in these kinds of scenarios, we should ask the user to fetch the latest record once again and work on it. 

Optimistic Concurrency With Application-Managed Tokens

It is possible to manage the concurrency token in the application without having the database manage it. By using this approach, we can implement optimistic concurrency even when using databases that don’t natively support auto-generated values – for instance, SQLite. Not only that, we can have better control over how to manage the token, when to regenerate it, etc.

Code Changes

To implement an application-managed concurrency token, let’s modify the Product class and replace the RowVersion property with a GUID type Version property:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    [ConcurrencyCheck]
    public Guid Version { get; set; }        
}

Note that we have decorated the Version property with the ConcurrencyCheck attribute. Once we apply this attribute to a property of an entity class, EF Core will include the corresponding column in the concurrency check. It does so by including these columns in the WHERE clause for UPDATE and DELETE operations. The only difference is that it does not auto-generate or manage this column.

Since we modified the entity class, we have to add a new migration and update the database once again for these changes to be reflected in the database.

However, remember that EF Core will no longer auto-generate or manage the Version column. We should do that on the application side now. So let’s modify the POST operation to generate this value:

[HttpPost]
public async Task<ActionResult<Product>> PostProduct(Product product)
{
    // code removed for brevity

    product.Version = Guid.NewGuid();
    _context.Product.Add(product);
    await _context.SaveChangesAsync();

    return CreatedAtAction("GetProduct", new { id = product.Id }, product);
}

Here we generate a new GUID and assign it to the Version property.

Now let’s modify the PUT method to generate and assign a new version while updating the record:

[HttpPut("{id}")]
public async Task<IActionResult> PutProduct(int id, Product product)
{
    // code removed for brevity

    _context.Entry(product).State = EntityState.Modified;

    try
    {
        product.Version = Guid.NewGuid();
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        // code removed for brevity
    }

    return NoContent();
}

EF Core will now check for concurrency during every UPDATE and DELETE operation.

Testing Optimistic Concurrency

Now let’s create a new record using the POST endpoint:

curl -X 'POST' \
  'https://localhost:7048/api/Products' \
  -H 'accept: text/plain' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "Milk",
  "price": 7
}'

It creates a new record in the database:

{
  "id": 2,
  "name": "Milk",
  "price": 7,
  "version": "2ba7e672-b733-46a2-9bf5-294e84e3c883"
}

Let’s try to update the record using the PUT endpoint:

curl -X 'PUT' \
  'https://localhost:7048/api/Products/2' \
  -H 'accept: */*' \
  -H 'Content-Type: application/json' \
  -d ' {
    "id": 2,
    "name": "Milk",
    "price": 8,
    "version": "2ba7e672-b733-46a2-9bf5-294e84e3c883"
  }'

Notice that we are passing the current version in the request body.

When we execute this request, we can notice that EF Core automatically adds a check in the WHERE clause to see if the Version matches along with the Id:

UPDATE [Product] SET [Name] = @p0, [Price] = @p1, [Version] = @p2
OUTPUT 1
WHERE [Id] = @p3 AND [Version] = @p4;

Since the Version matches, this will update the record and assign a new version to it:

{
  "id": 2,
  "name": "Milk",
  "price": 8,
  "version": "74af534d-1354-4a41-be4c-54c5b905792f"
}

However, if other users working on this record try to update the record using the old version afterward, it will throw a DbUpdateConcurrencyException. We should handle that and inform the user that someone else modified the product and ask them to refresh and work on the latest record.

Use of ETags & If-Match Headers

ETag & If-Match are HTTP headers that we can use for optimistic concurrency control. ETag stands for Entity Tag and represents the unique identifier for a specific version of the record.

When a client requests a resource, the server includes the ETag value in the response. A client can then use the If-Match header while updating a record to verify that no other clients updated this record in the meantime. To do so, a client can send the ETag value in the If-Match request header. The server can then compare this ETag with the current ETag value of the record to check for conflicts.

Code Changes

For implementing the ETag & If-Match headers, first, let’s add the header constants in the controller class and modify the GET method:

public class ProductsController : ControllerBase
{
    private const string ETagHeader = "ETag";
    private const string IfMatchHeader = "If-Match";

    // code removed for brevity

    [HttpGet("{id}")]
    public async Task<ActionResult<Product>> GetProduct(int id)
    {
        // code removed for brevity

        var product = await _context.Product.FindAsync(id);

        if (product == null)
        {
            return NotFound();
        }

        Response.Headers.Add(ETagHeader, product.Version.ToString());

        return product;
    }

    // code removed for brevity
}

In the GET method, we add the value of Version as the ETag header response.  

Next, let’s modify the PUT method to check if the If-Match request header value matches with the current ETag value of the record:

[HttpPut("{id}")]
public async Task<IActionResult> PutProduct(int id, Product product)
{
    // code removed for brevity

    var currentProduct = await _context.Product.FindAsync(id);
    if (currentProduct == null)
    {
        return NotFound();
    }

    var currentETag = currentProduct.Version.ToString();
    var requestETag = Request.Headers[IfMatchHeader].ToString();
    if (requestETag != currentETag)
    {
        return StatusCode(StatusCodes.Status412PreconditionFailed);
    }

    _context.Entry(currentProduct).CurrentValues.SetValues(product);
    currentProduct.Version = Guid.NewGuid();
    _context.Entry(currentProduct).State = EntityState.Modified;

    try
    {                
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        // code removed for brevity
    }

    return NoContent();
}

Here, first, we fetch the latest record and get the current ETag value. If the If-Match header value does not match with the current ETag value, it means someone has updated the record since we last fetched it. In that case, we return a 412 Precondition Failed HTTP response. If the values match, we can safely update the record.

Also, remember to remove the [ConcurrencyCheck] attribute from the Version property of the Product entity as we are already handling concurrency using ETag and If-Match headers and don’t want EF Core to check it anymore.

Testing Optimistic Concurrency

Now we can see the ETag response header with the GET results:

optimistic concurrency using etag

So let’s update the record using the PUT method. Remember that we should pass the current ETag value in the If-Match request header:

curl -X 'PUT' \
  'https://localhost:7048/api/Products/2' \
  -H 'accept: */*' \
  -H 'Content-Type: application/json' \
  -H 'If-Match: c1409437-21fd-4991-ab28-03ffe444fae0' \
  -d ' {
    "id": 2,
    "name": "Milk",
    "price": 9
  }'

Of course, this will update the record and assign a new version to it as well:

{
    "id": 2,
    "name": "Milk",
    "price": 9,
    "version": "5e2569e6-714e-4caa-b8e4-75fc02e61258"
}

Also, note that now the application returns the current version as a new ETag value. If other users try to update the same record using the old ETag value, they will get a 412 Precondition Failed HTTP response which indicates that a precondition has failed:

{
    "status": 412,
    "traceId": "00-2412ee1113be82c953029473a6a5807d-f456cf37c313ef3a-00"
}

In such cases, we need to ask the user to fetch the latest record and work on that.

Optimistic Concurrency With Hypermedia

Hypermedia in REST API refers to the concept of including links and related information in the API response so that clients can use that information for further interactions with the API.

To learn how to Implement HATEOAS (Hypermedia as the Engine of Application State) in ASP.NET Core Web API, refer to our excellent article on Implementing HATEOAS in ASP.NET Core Web API.

With this approach, when a client fetches a resource, the server can pass the update links with version information.

Later, when the client tries to update the resource using that link, the server will validate the version information. If the version has changed, it detects a conflict and will abort the update operation. In this case, the server should inform the client that someone has updated the record and ask them to fetch the latest record and work on that.

Code Changes

First, let’s modify the GET method to return the links as well:

[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetProduct(int id)
{
    if (_context.Product == null)
    {
        return NotFound();
    }
    var product = await _context.Product.FindAsync(id);

    if (product == null)
    {
        return NotFound();
    }

    return Ok(new
    {
        id = product.Id,
        name = product.Name,
        price = product.Price,
        links = new[]
        {
            new { rel = "edit", href = $"/products/{id}/{product.Version}", method ="PUT" }
        }
    });
}

Here we return an object with all the product properties and a link for performing the current version-specific PUT operation. It is possible to create version-specific links for other operations like GET, DELETE, etc. as well while implementing HATEOAS.

Now let’s modify the PUT operation as well:

[HttpPut("{id}/{version}")]
public async Task<IActionResult> PutProduct(int id, string version, Product product)
{
    // code removed for brevity

    var currentProduct = await _context.Product.FindAsync(id);
    if (currentProduct == null)
    {
        return NotFound();
    }
                        
    var currentVersion = currentProduct.Version.ToString();                        
    if (version != currentVersion)
    {
        return StatusCode(StatusCodes.Status412PreconditionFailed);
    }

    _context.Entry(currentProduct).CurrentValues.SetValues(product);
    currentProduct.Version = Guid.NewGuid();
    _context.Entry(currentProduct).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
       // code removed for brevity
    }

    return NoContent();
}

Here we compare the version information in the URL with the current version in the database. If they don’t match, it indicates someone has updated the record since we last fetched it and we return a 412 Precondition Failed HTTP response. On the other hand, if the version is the same, the record gets updated.

Testing Optimistic Concurrency

The GET endpoint will now generate a response that includes the update link:

{
  "id": 2,
  "name": "Milk",
  "price": 9,
  "links": [
    {
      "rel": "edit",
      "href": "/products/2/5e2569e6-714e-4caa-b8e4-75fc02e61258",
      "method": "PUT"
    }
  ]
}

Let’s try updating the record using the PUT endpoint:

curl -X 'PUT' \
  'https://localhost:7048/api/Products/2/5e2569e6-714e-4caa-b8e4-75fc02e61258' \
  -H 'accept: */*' \
  -H 'Content-Type: application/json' \
  -d '{
  "id": 2,
  "name": "Milk",
  "price": 10
}'

If the version in the database matches the version that we pass in the URL, it updates the record:

{
  "id": 2,
  "name": "Milk",
  "price": 10,
  "links": [
    {
      "rel": "edit",
      "href": "/products/2/e29903b1-bba8-462e-9673-0c39ed52459a",
      "method": "PUT"
    }
  ]
}

Along with that, it updates the version value and returns the new updated URL as the link.

If other users attempt to update the record afterward using the old version, they will get a 412 Precondition Failed HTTP response:

{
  "status": 412,
  "traceId": "00-26174ae89509711d6c4b472f0a813cb7-9b197751b8479b80-00"
}

In such cases, they are required to refresh the record and work on the latest data.

Conclusion

In this article, we delved into the usage of optimistic concurrency control in an ASP.NET Core Web API application. We grasped the methods to implement it using both a database-generated concurrency token and an application-managed variant. Subsequently, we explored the application of ETag and If-Match HTTP headers for similar purposes. Finally, we learned how to make use of the Hypermedia REST API concepts for concurrency control.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!