Why Optimistic Concurrency?
In a multi-user application, two people can load the same row, edit different fields, and both call SaveChanges — without any coordination, the second write silently overwrites the first (a 'lost update'). EF Core defaults to optimistic concurrency: rather than locking rows while they're being edited (which hurts scalability), it assumes conflicts are rare, lets both users read and edit freely, and only checks for a conflict at write time by comparing the row's current value against the value the user originally read. If they don't match, someone else has already changed the row and EF throws a DbUpdateConcurrencyException instead of blindly overwriting.
Cricket analogy: It's like two selectors independently drafting a starting XI without locking the team sheet — they only find out about a conflicting pick when the final sheet is compared at the toss, not while each was drafting.
Concurrency Tokens and RowVersion
EF Core detects conflicts using a concurrency token: a property marked with [ConcurrencyCheck] or, more commonly, [Timestamp] (which maps to SQL Server's rowversion column type, automatically incremented by the database on every update). When you configure a concurrency token, EF includes it in the WHERE clause of every generated UPDATE and DELETE statement — for example, UPDATE Products SET Price = @p WHERE ProductId = @id AND RowVersion = @originalRowVersion. If another transaction updated the row in between, the RowVersion won't match, zero rows will be affected, and EF interprets that as a concurrency conflict.
Cricket analogy: It's like requiring the exact ball number stamped on a DRS review request — if the ball count doesn't match the current over, the third umpire rejects the review as stale before even looking at the footage.
Handling DbUpdateConcurrencyException
When a concurrency conflict is detected, SaveChanges throws a DbUpdateConcurrencyException whose Entries property exposes the conflicting EntityEntry objects. From there you typically choose one of three resolutions: 'database wins' (discard the user's changes by calling entry.Reload() and reloading current database values), 'client wins' (overwrite the database anyway by setting entry.OriginalValues to the current database values via entry.GetDatabaseValues() and retrying SaveChanges), or a merge strategy where you inspect both CurrentValues and the freshly fetched database values property-by-property and let the user or business logic decide which to keep.
Cricket analogy: It's like a run-out review where the third umpire must choose one of three outcomes — uphold the on-field call, overturn it, or ask for a fresh angle — rather than just guessing which replay to trust.
public class Product
{
public int ProductId { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; } // SQL Server rowversion concurrency token
}
try
{
product.Price = 29.99m;
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.Entries)
{
var databaseValues = await entry.GetDatabaseValuesAsync();
if (databaseValues == null)
{
// Row was deleted by another user
throw new InvalidOperationException("Product was deleted by another user.");
}
// "Client wins": reapply our values on top of the current DB row
entry.OriginalValues.SetValues(databaseValues);
}
await context.SaveChangesAsync(); // retry with refreshed RowVersion
}
Never resolve a concurrency conflict by simply retrying SaveChanges without updating OriginalValues first — the retry will hit the exact same RowVersion mismatch and throw DbUpdateConcurrencyException again in an infinite loop.
Optimistic vs Pessimistic Locking
Pessimistic locking takes the opposite approach: it acquires a database-level lock (e.g. SELECT ... FOR UPDATE, or SQL Server's WITH (UPDLOCK)) the moment a row is read for editing, blocking any other transaction from modifying it until the lock is released. EF Core has no first-class API for pessimistic locking — you'd drop to raw SQL via FromSqlRaw or ExecuteSqlRaw to acquire the lock explicitly. It trades throughput for certainty: no lost updates are possible, but concurrent users editing popular rows (like a shared inventory count) will queue up waiting for locks, which doesn't scale well for high-traffic web applications — this is why EF Core's optimistic model is the default and recommended approach for most CRUD scenarios.
Cricket analogy: It's like reserving the entire nets session exclusively for one batter's throwdowns versus letting several batters share rotating turns and only stopping if two accidentally step in at once — one guarantees no collision, the other keeps more players active.
- EF Core defaults to optimistic concurrency: conflicts are detected at write time rather than prevented by locking at read time.
- A concurrency token (marked [ConcurrencyCheck] or [Timestamp]/rowversion) is added to the WHERE clause of UPDATE/DELETE statements.
- Zero rows affected by an UPDATE/DELETE due to a stale token causes EF to throw DbUpdateConcurrencyException.
- DbUpdateConcurrencyException.Entries exposes the conflicting entities so you can implement database-wins, client-wins, or merge resolution.
- Retrying SaveChanges without refreshing OriginalValues will just throw the same exception again.
- Pessimistic locking (SELECT ... FOR UPDATE / WITH (UPDLOCK)) prevents conflicts outright but requires raw SQL in EF Core and scales worse under contention.
- Optimistic concurrency is the recommended default for typical web application CRUD workloads.
Practice what you learned
1. What is EF Core's default concurrency control strategy?
2. What does marking a property with [Timestamp] do in EF Core (on SQL Server)?
3. What causes EF Core to throw a DbUpdateConcurrencyException?
4. What must you do before safely retrying SaveChanges after catching a DbUpdateConcurrencyException?
5. How does pessimistic locking differ from EF Core's default optimistic approach?
Was this page helpful?
You May Also Like
Change Tracking Explained
How EF Core's ChangeTracker snapshots entity state and computes the minimal set of INSERT, UPDATE, and DELETE statements needed at SaveChanges time.
Transactions and Savepoints
How EF Core wraps SaveChanges in implicit transactions, when to use explicit transactions across multiple calls, and how savepoints enable partial rollback.
Tracking vs No-Tracking Queries
When to let EF Core track query results for later updates versus opting out with AsNoTracking for faster, read-only workloads.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics