How does an OData 4.01 client obtain an entity's ETag and use it with If-Match to update that entity safely?
answer
- two places the token travels
- one per entity in a collection
- Core.OptimisticConcurrency announces it
- missing versus stale token
- what the star means
basics
~20 sAn entity's ETag comes from the ETag header of a single-entity response or, per entity, the @etag control information (@odata.etag in 4.0). It goes back unchanged in If-Match on PATCH, PUT, DELETE or a bound action; stale gets 412, missing-but-required 428.
solid answer
~40 sOData puts each entity's ETag in two places: the `ETag` response header when a response is about one resource, and the `etag` control information on each entity inside a payload (`@etag` in 4.01 JSON, `@odata.etag` in 4.0), so a client listing fifty orders already holds fifty tokens. To update, it copies the token unchanged into `If-Match` on the `PATCH`, `PUT`, `DELETE` or bound-action request. A service that requires this announces it with the `Core.OptimisticConcurrency` annotation on the entity set; it must then send the `ETag` header on GET, answer a modification without `If-Match` with **428**, and a stale token with **412**, with no observable change either way. `If-Match: *` matches any current version. Services should use weak ETags, and OData then mandates weak comparison - a deliberate deviation from HTTP.
code
http · 13 linesGET https://sales.example.com/service/Orders(1042)
Accept: application/json
HTTP/1.1 200 OK
ETag: W/"a1f4"
PATCH https://sales.example.com/service/Orders(1042)
Content-Type: application/json
If-Match: W/"a1f4"
{ "Status": "Shipped" }
HTTP/1.1 412 Precondition Failedgo deeper
Remember the round trip: read the ETag, send it back in If-Match on the update, and expect a refusal when someone else changed the entity first.
Explain both sources of the token, the per-entity etag in collections, Core.OptimisticConcurrency, 428 versus 412, and what If-Match: * does to an upsert.
Catch the integration traps: metadata=none removes per-entity tokens, If-Match never goes on a batch, the 4.0 versus 4.01 etag spelling, and weak comparison.
Decide which entity sets need mandatory concurrency control, weighing lost updates against clients that must handle 412 and reread before retrying.
## Why OData needs a convention **Optimistic concurrency** lets clients edit without locks: each version of an entity has an **ETag** (an opaque version token), and a write succeeds only if the client proves it saw the current version. HTTP supplies the machinery - the `ETag` response header and the `If-Match` precondition. OData adds where the token appears in its payloads, how a service says it is required, and a few rules of its own. ## Where the client gets the ETag 1. **The `ETag` response header** - on a response about a single resource, such as `GET Orders(1042)`. If a service requires ETags for modifying a resource, it **must** include this header on GET. 2. **The `etag` control information** - inside JSON payloads, attached to each entity: `@etag` in 4.01, `@odata.etag` in 4.0, and 4.01 consumers must read both spellings. ```json { "@context": "https://sales.example.com/service/$metadata#Orders", "value": [ { "@etag": "W/\"a1f4\"", "ID": 1042, "Status": "Open" }, { "@etag": "W/\"77c0\"", "ID": 1043, "Status": "Open" } ] } ``` The second source is what matters for list-and-edit screens: one response has no single `ETag` header for fifty entities, but each entity carries its own. The etag control information is kept under `metadata=minimal` and `full` and **not written under `metadata=none`**, so a client asking for none gives up per-entity tokens. ## Sending it back The client copies the token, unchanged, into `If-Match`: ```http PATCH https://sales.example.com/service/Orders(1042) Content-Type: application/json If-Match: W/"a1f4" { "Status": "Shipped" } ``` - The value must be an ETag previously retrieved for that resource, or `*`. - It applies to `PATCH`, `PUT` and `DELETE`, and to **actions bound to the resource**; `GET` and `POST` may carry it too. - Inside a `$batch`, `If-Match` goes on the individual requests, never on the batch request itself. ## When the service requires it | Situation | Service behaviour | |---|---| | Entity set annotated with `Core.OptimisticConcurrency` (or the navigation restriction `OptimisticConcurrencyControl`), no `If-Match` sent | **428 Precondition Required**, no observable change | | `If-Match` does not match the current ETag | **412 Precondition Failed**, no observable change | | `If-Match` matches | request processed | | `If-Match: *` | matches any current version; a service may reject it when it is used to sidestep required concurrency control | The `Core.OptimisticConcurrency` term lists the properties the ETag is computed from (an empty list means the service will not say). An `ETag` header alone does **not** prove that concurrency control is required: the token may exist only for caching and conditional GETs. ## Weak ETags and comparison One entity can be served in several formats and with different control information, so OData says services **should** use **weak ETags** that depend only on the entity's state, not its representation; a strong ETag would have to change with `Content-Type` and `Content-Encoding`. When a service uses such weak ETags, it **must** compare `If-Match` values with the weak comparison function. HTTP itself requires strong comparison for `If-Match`, and OData records this as a deliberate deviation. ## What changes an ETag, and the star - An entity's ETag must change whenever its **structural properties or links** change. Changing a contained entity may also change the parent's ETag; a collection's own ETag, if any, has service-specific meaning. - `If-Match: *` on a `PUT` or `PATCH` turns a possible **upsert** into an update only: if the entity does not exist, the condition counts as not matching, so the service answers 412 rather than creating it. Clients sometimes send `*` to opt out of concurrency control, and a service may refuse. ## ETags on reads The same token serves conditional reads. Services **must** accept the value of an `ETag` header in `If-None-Match` on a later data request for that resource, so a client can revalidate a cached entity cheaply. The metadata document can carry its own `ETag` too, and a response's `@metadataEtag` lets a client check that the payload was produced against the model version it has cached. Generic HTTP semantics of 412 and 428, and how to design ETags for a plain REST API, sit outside OData's own conventions.
- Why does OData tell services to prefer weak ETags?One entity can be returned as different representations - formats and metadata levels - and a strong ETag must change whenever the representation does. A weak ETag that depends only on the entity's state survives those differences. Services sending such weak ETags must use weak comparison for `If-Match`, which OData records as a deliberate deviation from HTTP's strong-comparison rule.
- What happens when a client sends If-Match: * on a PATCH to an entity that does not exist?A `PUT` or `PATCH` to a missing entity would normally be an upsert that creates it. `If-Match: *` turns it into an update only: OData treats the condition as not matching when the addressed entity does not exist, so the service returns 412 Precondition Failed instead of creating anything.
- If a GET returns an ETag header, must the client send If-Match on every update?Not necessarily. An ETag may exist only for caching and conditional GETs. The service announces that modifications require it with `Core.OptimisticConcurrency` on the entity set or the navigation restriction `OptimisticConcurrencyControl`; only then does a modification without `If-Match` get 428 Precondition Required.
saying these in an interview costs you the question
- OData payloads carry no ETags; each entity must be fetched singly to get one.
- If-Match belongs on the $batch request so it protects every change inside.
- An ETag header on a GET proves the service requires If-Match on updates.
- metadata=none still writes the etag on every entity in a collection.
- OData services must use strong ETags because If-Match uses strong comparison.
- If-Match: * on a PATCH creates the entity when it does not exist yet.