Problem
This post discusses how to handle concurrency in ASP.NET Core Web API.
Solution
Create an empty project and update Startup
class to add services and middleware for MVC:
public void ConfigureServices(
IServiceCollection services)
{
services.AddMvc();
}
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env)
{
app.UseMvcWithDefaultRoute();
}
Add a controller with GET
and PUT
, to demonstrate concurrency:
[Route("movies")]
public class MoviesController : Controller
{
const string ETAG_HEADER = "ETag";
const string MATCH_HEADER = "If-Match";
[HttpGet("{id}")]
public IActionResult Get(int id)
{
var model_from_db = new Movie
{
Id = 1,
Title= "Thunderball",
ReleaseYear = 1965,
};
var eTag = HashFactory.GetHash(model_from_db);
HttpContext.Response.Headers.Add(ETAG_HEADER, eTag);
if (HttpContext.Request.Headers.ContainsKey(MATCH_HEADER) &&
HttpContext.Request.Headers[MATCH_HEADER].RemoveQuotes() == eTag)
return new StatusCodeResult(StatusCodes.Status304NotModified);
return Ok(model_from_db);
}
[HttpPut("{id}")]
public IActionResult Put(int id, [FromBody]Movie model)
{
var model_from_db = new Movie
{
Id = 1,
Title = "Thunderball-changed",
ReleaseYear = 1965,
};
var eTag = HashFactory.GetHash(model_from_db);
HttpContext.Response.Headers.Add(ETAG_HEADER, eTag);
if (!HttpContext.Request.Headers.ContainsKey(MATCH_HEADER) ||
HttpContext.Request.Headers[MATCH_HEADER].RemoveQuotes() != eTag)
{
return new StatusCodeResult(StatusCodes.Status412PreconditionFailed);
}
else
{
}
return NoContent();
}
}
Send a GET
request and observe ETag
header (using Postman):
Now using this ETag
value, send a PUT
request:
Discussion
We want to ensure that users are not overriding changes made by other users between the time they retrieved the data and submit their changes.
At its core, implementing such concurrency is a simple two-step process:
GET
We add a magic value to the response based on data we hold at the time. Usually ETag
header is added for this purpose containing hashed value based on data/body of response.
Clients send this ETag
value in subsequent requests as If-Match
header, which we compare against ETag
produced for current data and if unchanged, send a 304 (Not Modified)
response.
PUT
We compare against ETag
produced for current data and if changed, send a 412 (Precondition Failed)
response.
Other Methods
Using hashed values of model representation is not the only way to implement concurrency. Entity Framework also provides a mechanism to implement concurrency. I’ll discuss in a later post on EF Core.