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.

Advanced135–175 minutestoken/provider comparison + SQLite labEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 mandatory baselineSDK checkpoint: 10.0.400Last reviewed: August 2026

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.

01

Define concurrency token, original value, current value, optimistic conflict, lost update, and provider-generated token.

02

Continue the existing SQLite Guid Revision policy and explain exactly when the application must advance it.

03

Configure SQL Server rowversion without implying it exists on SQLite, PostgreSQL, MySQL, or Oracle.

04

Explain PostgreSQL xmin as an Npgsql-specific concurrency option and its portability boundary.

05

Inspect IProperty metadata to prove which property is the concurrency token and whether it is store-generated.

06

Choose token scope by business meaning so irrelevant changes do not create needless false conflicts.

Reproducible baseline

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.

csharp · existing domain policy
public Guid Revision { get; private set; } = Guid.NewGuid();public void AdvanceRevision(){    Revision = Guid.NewGuid();}
csharp · existing mapping
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

csharp · looks protected, is not
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.

What the token proves

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.

csharp · SQL Server-only model variant
public byte[] RowVersion { get; private set; } = Array.Empty<byte>();builder.Property(x => x.RowVersion)    .IsRowVersion();
shell · optional package checkpoint
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.

csharp · Npgsql/PostgreSQL-only illustration
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

csharp · runtime model proof
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

  1. Reset the disposable SQLite database using the course migration/reset procedure.
  2. Query one work order and record Revision original/current values.
  3. Change the summary, call AdvanceRevision(), and inspect DebugView before save.
  4. Save and capture the command log; verify the old revision is used for matching and the new revision is stored.
  5. Repeat after deliberately omitting AdvanceRevision() and explain why the mapping alone does not invalidate stale writers.
  6. Run the metadata probe and record IsConcurrencyToken/ValueGenerated.
  7. Document SQL Server rowversion and PostgreSQL xmin as provider alternatives only.

Check your understanding

  1. Does optimistic concurrency hold a database lock while the user edits?
  2. Why does ServiceHub regenerate Revision?
  3. Can SQLite provide SQL Server rowversion semantics automatically?
  4. What does IsRowVersion mean on SQL Server?
  5. Is PostgreSQL xmin portable to SQL Server or SQLite?
  6. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n