100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Optimistic Concurrency Control

How EF Core detects and resolves concurrent edit conflicts using concurrency tokens and DbUpdateConcurrencyException, and how this compares to pessimistic locking.

Performance & TransactionsAdvanced10 min readJul 10, 2026
Analogies

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.

csharp
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

Was this page helpful?

Topics covered

#EntityFrameworkCoreStudyNotes#MicrosoftTechnologies#OptimisticConcurrencyControl#Optimistic#Concurrency#Control#Tokens#StudyNotes#SkillVeris#ExamPrep