Chapter 13 · Optimistic Concurrency and Conflict Resolution
Concurrency Tokens: Application-Managed Versions, rowversion, xmin, and Provider Options
Compare ServiceHub application-managed revisions with SQL Server rowversion and PostgreSQL xmin, and make provider-specific concurrency ownership explicit.
Learning outcomes
Two dispatchers can read the same ServiceHub work order, make different decisions, and save seconds apart. Without an explicit version check, the later write can silently erase the earlier one. Optimistic concurrency assumes conflicts are uncommon, allows readers to proceed without holding a long-lived database lock, and detects stale writes when they reach the database.
Define concurrency token, original value, current value, optimistic conflict, lost update, and provider-generated token.
Continue the existing SQLite Guid Revision policy and explain exactly when the application must advance it.
Configure SQL Server rowversion without implying it exists on SQLite, PostgreSQL, MySQL, or Oracle.
Explain PostgreSQL xmin as an Npgsql-specific concurrency option and its portability boundary.
Inspect IProperty metadata to prove which property is the concurrency token and whether it is store-generated.
Choose token scope by business meaning so irrelevant changes do not create needless false conflicts.
Mandatory labs use .NET SDK 10.0.400, .NET runtime 10.0.11, Microsoft.EntityFrameworkCore/SQLite 10.0.11, dotnet-ef 10.0.11, the disposable servicehub-lab.db, deterministic ServiceHub seed data, and the existing application-managed Guid Revision token. SQL Server rowversion and PostgreSQL xmin are optional provider comparisons, not mandatory infrastructure. EF Core 11 previews are excluded.
1. The token is a version claim, not a lock
A concurrency token is a mapped property whose
original value EF includes in UPDATE/DELETE predicates.
If the stored value changed after the entity was queried, the
predicate matches zero rows. EF then reports a
DbUpdateConcurrencyException. No row is “reserved”
while the user thinks or an API request travels over the
network.
| Mechanism | Who changes the token? | Protects | Main boundary |
|---|---|---|---|
ServiceHub Guid Revision |
Application/domain code | Only changes for which the app advances the token | Portable; omissions are correctness bugs. |
SQL Server rowversion |
SQL Server automatically | Any row UPDATE that changes rowversion | SQL Server-specific binary token. |
PostgreSQL xmin |
PostgreSQL MVCC engine | Row version represented by transaction ID | PostgreSQL/Npgsql-specific system column. |
| Timestamp/date column | Application or database | Depends on precision/generation rule | Clock precision/time semantics can make weak tokens. |
Token choice is a concurrency-policy decision. A token that changes for every physical write can cause conflicts for logically unrelated edits; a token that does not change for meaningful edits allows lost updates.
2. Mandatory path: ServiceHub keeps its application-managed Guid Revision
Chapter 04 already evolved WorkOrder with a
Guid Revision and configured it as a concurrency
token. Chapter 12 showed that the original token appears in
tracked write predicates. Keep that contract; do not add a
second token just for this chapter.
public Guid Revision { get; private set; } = Guid.NewGuid();public void AdvanceRevision(){ Revision = Guid.NewGuid();}
builder.Property(x => x.Revision) .HasColumnName("revision") .IsConcurrencyToken();
SQLite has no SQL Server-style automatically changing
rowversion. An application-managed token is
therefore a good free/local teaching path. The application must
advance the token for every change that should invalidate stale
writers.
3. Deliberately broken: configure a token but never change it
var order = await db.WorkOrders.SingleAsync(x => x.Id == id, ct);order.ReviseSummary("Motor vibration inspection expanded");// BUG: Revision remains unchanged.await db.SaveChangesAsync(ct);
Both writers can keep matching the same stored revision forever.
The mapping says “compare this property,” but the lifecycle
never produces a new version. The safe repair is to centralize
meaningful mutation with AdvanceRevision() or a
tested SaveChanges/interceptor policy. Avoid unconditional token
advancement for non-business cache/audit changes if those should
not force user-visible conflicts.
A changed token proves only that the concurrency scope changed since the reader observed it. It does not explain which business fields changed, whether the newer decision is correct, or whether multiple rows still satisfy a cross-row invariant.
4. SQL Server variant: rowversion is store-generated
SQL Server rowversion is an automatically changing
binary value. EF's IsRowVersion() combines
concurrency-token and store-generated-on-update semantics. This
is optional provider-specific material; the mandatory SQLite lab
does not install SQL Server.
public byte[] RowVersion { get; private set; } = Array.Empty<byte>();builder.Property(x => x.RowVersion) .IsRowVersion();
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 10.0.11
Do not copy the CLR property to the SQLite model and assume the provider will make it auto-update. Provider-generated tokens require the matching database feature and provider mapping.
5. PostgreSQL variant: xmin is a system-column option
PostgreSQL exposes the hidden system column xmin,
which Npgsql documents as a useful concurrency token because it
changes when a row is updated. A typical Npgsql mapping uses a
uint property with IsRowVersion().
This is an Npgsql/PostgreSQL convention, not a portable EF
abstraction.
public uint Version { get; private set; }builder.Property(x => x.Version) .IsRowVersion(); // Npgsql maps uint row-version semantics to xmin
At generation time the stable Npgsql EF Core 10 line is 10.0.3. Recheck provider/database support before reproducing this optional path; provider package restore alone is not proof that every production PostgreSQL topology/version is supported.
6. Inspect metadata instead of trusting comments
var entity = db.Model.FindEntityType(typeof(WorkOrder))!;var revision = entity.FindProperty(nameof(WorkOrder.Revision))!;Console.WriteLine($"Concurrency: {revision.IsConcurrencyToken}");Console.WriteLine($"Generated: {revision.ValueGenerated}");Console.WriteLine($"Column: {revision.GetColumnName()}");
For ServiceHub the expected result is
Concurrency: True and no database-generated update
lifecycle for Revision. If a SQL Server rowversion
variant is used, generated-value metadata differs because the
store owns the new token.
7. Hands-on lab: prove the token lifecycle
- Reset the disposable SQLite database using the course migration/reset procedure.
-
Query one work order and record
Revisionoriginal/current values. -
Change the summary, call
AdvanceRevision(), and inspect DebugView before save. - Save and capture the command log; verify the old revision is used for matching and the new revision is stored.
-
Repeat after deliberately omitting
AdvanceRevision()and explain why the mapping alone does not invalidate stale writers. -
Run the metadata probe and record
IsConcurrencyToken/ValueGenerated. - Document SQL Server rowversion and PostgreSQL xmin as provider alternatives only.
Check your understanding
- Does optimistic concurrency hold a database lock while the user edits?
- Why does ServiceHub regenerate Revision?
- Can SQLite provide SQL Server rowversion semantics automatically?
- What does IsRowVersion mean on SQL Server?
- Is PostgreSQL xmin portable to SQL Server or SQLite?
- Why can a very broad token create false conflicts?
Review the answers
No. It detects stale state when a write is attempted.
The new token invalidates writers that still hold the previously read token.
No. Use an application-managed token or another SQLite-appropriate policy.
It configures a concurrency token whose value is generated by the store on add/update.
No. It is a PostgreSQL system-column mechanism surfaced by Npgsql.
It may change for edits that are logically independent, causing one writer to reject another unnecessarily.
8. Production judgment and bridge
Choose a token whose lifecycle matches the business conflict boundary, keep provider-generated tokens provider-gated, and test the application-managed lifecycle. The token is only useful because EF places the original value into write predicates. Lesson 2 makes that SQL and the zero-row detection mechanism explicit.
Authoritative references
- Handling Concurrency Conflicts - EF Core — optimistic concurrency, rowversion, application-managed tokens, and SQLite limitations.
- Generated Values - EF Core — store-generated value semantics relevant to rowversion.
- SQL Server EF Core provider — provider boundary for SQL Server-specific features.
- Npgsql Concurrency Tokens — PostgreSQL xmin mapping guidance.
- Microsoft.EntityFrameworkCore.SqlServer 10.0.11 — optional SQL Server provider version checkpoint.
- Npgsql EF Core provider 10.0.3 — optional PostgreSQL provider version checkpoint.