Skip to content
Version v3.0.0

Optimistic Concurrency

Mutating endpoints in the Management API v2 and User Store API v2 that expose a versioned resource use HTTP optimistic concurrency control as defined by RFC 9110 §13.

The contract follows the standard ETag / If-Match exchange — no proprietary extensions — so any RFC 9110-compliant HTTP client can participate without a ProAuth-specific SDK.

Where this applies

Concurrency control is enforced for every resource whose DTO derives from ConcurrencyAwareDto. In v2 this includes (non-exhaustive): Tenant, ClientApp, IdpInstance, User, Group, ProAuthUser, ProAuthRole, ProAuthRoleAssignment, ClaimRule, Subscription, CustomView, and most other long-lived configuration entities. v1 endpoints do not enforce concurrency.

At a glance

HTTP methodServer emits ETag?Client must send If-Match?Conflict response
GETYes (on aware resources)Non/a
POST (create)Yes — ETag of the newly created resourceNon/a
PUT (replace)Yes — new ETag of the updated resourceYes412 Precondition Failed
PATCHYes — new ETag of the updated resourceYes412 Precondition Failed
DELETEn/aYes412 Precondition Failed

Missing the If-Match header on a write to an aware resource yields 428 Precondition Required. A malformed or syntactically inconsistent If-Match value yields 400 Bad Request.

ETag format

ProAuth emits strong, double-quoted ETags whose tag value is the lowercase 32-character hexadecimal form (the .NET Guid "N" format) of the entity's internal concurrency token:

http
ETag: "8b1f3c2d4e5f6789a0b1c2d3e4f50617"

Weak ETags (W/"…") are never emitted by ProAuth and must not be sent on If-Match.

Read–then–write workflow

  1. GET the resource and capture the ETag response header.
  2. Mutate the local copy.
  3. Send the mutation back with If-Match set to the captured ETag.
  4. On 200/201/204, capture the new ETag from the response and use it for the next mutation.
  5. On 412 Precondition Failed, refetch the resource and reconcile.
http
GET /api/management/v2/Tenant/3a7e1f5a-1b2c-4d5e-9f80-aa11bb22cc33 HTTP/1.1
Authorization: Bearer …

HTTP/1.1 200 OK
ETag: "8b1f3c2d4e5f6789a0b1c2d3e4f50617"
Content-Type: application/json

{ "id": "3a7e1f5a-…", "name": "Acme",  }
http
PUT /api/management/v2/Tenant/3a7e1f5a-1b2c-4d5e-9f80-aa11bb22cc33 HTTP/1.1
Authorization: Bearer …
If-Match: "8b1f3c2d4e5f6789a0b1c2d3e4f50617"
Content-Type: application/json

{ "id": "3a7e1f5a-…", "name": "Acme Holdings", "subscriptionId": "…" }

A successful response carries the new token to use for the next write:

http
HTTP/1.1 200 OK
ETag: "f60a3b8c5d2e1f9087564738291a0b1c"

Error responses

412 Precondition Failed — Conflict

The request reached the server but its If-Match does not match the current server-side token. Another client has updated the resource since the token was issued. Refetch, reconcile, and retry.

http
HTTP/1.1 412 Precondition Failed
Content-Type: application/problem+json

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.13",
  "title": "Precondition Failed",
  "status": 412,
  "detail": "The supplied If-Match precondition does not match the current resource version."
}

428 Precondition Required — Missing If-Match

The endpoint requires If-Match and none was supplied.

http
HTTP/1.1 428 Precondition Required
Content-Type: application/problem+json

{
  "type": "https://tools.ietf.org/html/rfc6585#section-3",
  "title": "Precondition Required",
  "status": 428,
  "detail": "If-Match is required for this operation."
}

400 Bad Request — Malformed precondition

Sent when:

  • If-Match is present but not a syntactically valid entity-tag,
  • the request body's embedded concurrency token contradicts the If-Match header (for DTOs that carry their own ConcurrencyToken field).

Body-level concurrency token

Update*Command and Patch*Command DTOs derive from ConcurrencyAwareDto, which exposes an optional ConcurrencyToken property. When present the value must equal the If-Match header tag; the server uses the header as the authoritative precondition and treats body/header divergence as a malformed request (400 Bad Request).

Most clients should rely solely on the If-Match header and leave the body's ConcurrencyToken unset.

OpenAPI surface

Concurrency-aware operations are advertised in the OpenAPI document:

  • Every concurrency-protected PUT, PATCH, and DELETE declares an If-Match header parameter as required.
  • Every GET, POST, PUT, and PATCH of an aware resource declares an ETag response header.

Generated SDKs are expected to surface the header on the operation signature or to forward it transparently.

Client SDK behaviour

The first-party .NET clients (ProAuth.Clients.Management.V2 and ProAuth.Clients.UserStore.V2) currently expose the If-Match parameter on each mutating operation. Test fixtures used by ProAuth's own system tests ship a DelegatingHandler (EtagIfMatchForwardingHandler) that captures the ETag from each successful response and replays it as If-Match on the next mutation of the same resource — a useful reference implementation for production SDKs.

v1 compatibility

The v1 endpoints (/api/management/v1/…, /api/userstore/v1/…) do not participate in optimistic concurrency. Last-writer-wins semantics still apply there; migrate to v2 to gain conflict detection.