Intro

Optimistic HTTP concurrency lets clients update a resource only if the version they read is still current. The HTTP precondition is If-Match; the storage boundary must enforce the same comparison atomically. An application-side read, comparison, then unconditional save still has a race.

Contract

GET /orders/42 returns ETag: "17". The client sends that validator with its update:

PUT /orders/42 HTTP/1.1
If-Match: "17"
Content-Type: application/json
 
{"shippingAddress":"Kyiv"}

The server returns 428 Precondition Required when If-Match is missing and 412 Precondition Failed when another writer has already advanced the version.

Atomic compare-and-swap

app.MapPut("/orders/{id:long}", async (
    long id,
    UpdateOrder request,
    HttpRequest http,
    OrdersDb db,
    CancellationToken ct) =>
{
    if (!http.Headers.TryGetValue("If-Match", out var raw) ||
        !long.TryParse(raw.ToString().Trim('"'), out var expectedVersion))
    {
        return Results.StatusCode(StatusCodes.Status428PreconditionRequired);
    }
 
    var affected = await db.Database.ExecuteSqlInterpolatedAsync($"""
        UPDATE orders
        SET shipping_address = {request.ShippingAddress}, version = version + 1
        WHERE id = {id} AND version = {expectedVersion}
        """, ct);
 
    return affected == 1
        ? Results.NoContent()
        : Results.StatusCode(StatusCodes.Status412PreconditionFailed);
});

The single UPDATE ... WHERE version = expected is the concurrency boundary. If zero rows change, the client must re-read, merge deliberately, and retry with the new validator.

References