Interview question
Enforce If-Match with rowversion
Maps an HTTP ETag precondition to EF Core rowversion and returns 412 when the representation is stale.
TL;DR
Maps an HTTP ETag precondition to EF Core rowversion and returns 412 when the representation is stale.
ETags, If-Match, rowversion, preconditions, EF Core original values, and conflict recovery.
Practice the problem like a real interview: restate, reason, implement, and test.
Return an ETag from GET /projects/{id}. Require that value through If-Match on PUT; reject a missing precondition and map a stale version to 412.
Two editors load version A. The first saves version B; the second PUT with A receives 412 and can reload before deciding what to merge.
I translate the header into EF's original concurrency value. The database compares that value during update, which makes the write conditional without a check-then-update race.
app.MapGet("/projects/{id:guid}", async Task<IResult> (Guid id,
HttpResponse response, AppDbContext db, CancellationToken ct) =>
{
var project = await db.Projects.AsNoTracking().SingleOrDefaultAsync(x => x.Id == id, ct);
if (project is null) return Results.NotFound();
response.Headers.ETag = EntityTag.Format(project.Version);
return Results.Ok(new ProjectResponse(project.Id, project.Name));
});
app.MapPut("/projects/{id:guid}", async Task<IResult> (Guid id,
UpdateProjectRequest request, HttpContext http, AppDbContext db, CancellationToken ct) =>
{
if (!EntityTag.TryParseSingle(http.Request.Headers.IfMatch, out var expected))
return Results.Problem(statusCode: StatusCodes.Status428PreconditionRequired,
title: "If-Match is required");
var project = await db.Projects.SingleOrDefaultAsync(x => x.Id == id, ct);
if (project is null) return Results.NotFound();
project.Name = request.Name.Trim();
db.Entry(project).Property(x => x.Version).OriginalValue = expected;
try {
await db.SaveChangesAsync(ct);
} catch (DbUpdateConcurrencyException) {
return Results.Problem(statusCode: StatusCodes.Status412PreconditionFailed,
title: "Project changed since it was loaded");
}
http.Response.Headers.ETag = EntityTag.Format(project.Version);
return Results.Ok(new ProjectResponse(project.Id, project.Name));
});
The database, not an earlier SELECT, arbitrates the race. Returning the new ETag lets the client continue editing from the representation it just saved.
Next in API Implementation Labs: Atomic Bulk Update