Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Entity

ConcurrencyCheck

[ConcurrencyCheck] // on an entity (every property) or on one property

Refuses an update whose value was changed by somebody else since this writer read it. Field-scoped — two people editing different fields of one record are not in conflict — and raised as a ConflictException the app can catch.

stable2 examples compiled by CIentitymodelconcurrencyvalidation

Summary#

[ConcurrencyCheck] refuses an update whose value was changed by somebody else since this writer read it. It is what stops the last save winning silently — the failure mode where two people open the same record, both save, and the first one's work disappears with nothing said to either of them.

It is field-scoped: only the fields a write actually sets are checked, so two people editing different fields of the same record both succeed.

Signature#

[ConcurrencyCheck]              // on the ENTITY — guards every property
entity Case { … }

entity Case {
  [ConcurrencyCheck] decimal? Amount;    // …or on ONE property
}

Description#

What it does

A guarded update only lands if the row still holds the value this writer read. If it does not, the write is refused and nothing is written — the other person's value stands, and yours is still in front of you to re-apply.

The refusal is a ConflictException, so you catch it exactly as you catch any other conflict:

try { c.Amount = amount; UnitOfWork.Commit(); }
catch (ConflictException e) { message = e.Message; }

Field-scoped, and why that matters

Only the columns a write SETS are checked. If Anna changes Amount and Bo adds a note to the same record, neither is refused — they were never in conflict.

The alternative, refusing on any change to the row, is simpler and worse: it refuses writes that are not conflicts, and a conflict message that fires when nothing is wrong is one people learn to click past. That is the failure this scoping exists to avoid.

It is one statement, so there is no window

The check rides the UPDATE's own WHERE:

SET amount = @amount WHERE id = @id AND amount IS NOT DISTINCT FROM @was

Deciding and writing are the same statement, so nothing can change between them. The two designs that look equivalent both have a window: re-reading the row and comparing leaves a gap before the write, and consulting the audit trail is worse still — the trail is written after the commit, so a competing writer's entry may not be there yet.

What "since it was read" means across a page load

The value compared against is what this writer read, not what the row holds now. That distinction is the whole feature: a person holding a page across a round trip is the only writer who can be stale, and their belief is pinned when they read it, so it survives being resumed later.

The message

The refusal names the fields it refused, and names who changed the row and when when there is somebody to name — that comes from the entity-change audit trail. An anonymous write records no user, so the sentence falls back to "somebody else". If your app turns the trail off, the refusal still works and the sentence loses the name; a lint rule (security-concurrency-check-without-its-trail) says so at compile time.

What it does not do

It does not tell you somebody else has the record open — that is presence, a different mechanism. It does not lock anything: nobody is blocked from editing, and a conflict is only possible when two writes genuinely overlap on one field.

Examples#

[ConcurrencyCheck]
entity Case {
  [Required, MaxLength(40)] string Reference;
  decimal? Amount;
  [MaxLength(200)] string? Owner;

  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}
void Save(Guid id, decimal amount) {
  var c = Case.Single(x => x.Id == id);
  c.Amount = amount;
}

See also#

Related

constraints

The per-member rules the database enforces — Required, Unique, MaxLength/MinLength, Min/Max, Pattern, and the…

throw

Raises a fault. It ends the function immediately, and the rows the function wrote are discarded rather than…

try / catch / finally

Handles a fault. C#'s syntax, including typed catches, catch filters and finally. The rows written inside a try block…

entity

Declares a persisted type — a table of rows the app stores, queries and secures. Every entity gets an Id and audit…