In this article, we’ll discuss a few approaches to implementing optimistic concurrency in ASP.NET Core Web API.
Let’s begin.
Understanding Optimistic Concurrency
There are two different approaches for managing concurrent access to shared resources – Optimistic and Pessimistic concurrency.
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:
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:
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.