Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s).
ID: dotnet/ef-concurrency-conflict
Version Compatibility
| Version | Status | Introduced | Deprecated | Notes |
|---|---|---|---|---|
| 8 | active | — | — | — |
Root Cause
EF Core throws DbUpdateConcurrencyException when an UPDATE or DELETE statement affects zero rows, indicating the row was modified or deleted by another process since it was loaded. This is the optimistic concurrency mechanism triggered by [ConcurrencyCheck] or [Timestamp]/rowversion columns. The entity's concurrency token no longer matches the database value. Fix by implementing proper concurrency conflict resolution: reload and retry, merge changes, or present the conflict to the user.
genericWorkarounds
-
85% success Implement a retry loop that reloads the entity and reapplies changes
Catch DbUpdateConcurrencyException and reload the entity from the database, then reapply the intended changes: catch (DbUpdateConcurrencyException) { await entry.ReloadAsync(); entry.Entity.PropertyX = newValue; await dbContext.SaveChangesAsync(); } Wrap in a retry loop with a maximum attempt count (typically 3). This pattern works well when changes are simple overwrites that do not require user input to resolve -
88% success Resolve conflicts by comparing original, current, and database values
In the catch block, access all three value sets to make informed merge decisions: catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.Single(); var proposedValues = entry.CurrentValues; var databaseValues = await entry.GetDatabaseValuesAsync(); foreach (var property in proposedValues.Properties) { var proposed = proposedValues[property]; var database = databaseValues[property]; proposedValues[property] = /* merge logic */; } entry.OriginalValues.SetValues(databaseValues); await dbContext.SaveChangesAsync(); } This enables field-level conflict resolution -
82% success Present the concurrency conflict to the user for manual resolution
Catch the exception and return a 409 Conflict response with both the user's attempted changes and the current database values. The client can then display a conflict resolution UI showing both versions. After the user resolves the conflict, resubmit with the updated concurrency token: return Conflict(new { userValues = proposed, databaseValues = current, message = "This record was modified by another user" }); This is the most correct approach for complex business data where automatic merging is risky
Dead Ends
Common approaches that don't work:
-
Removing the [ConcurrencyCheck] or [Timestamp] attribute to suppress the exception
80% fail
Removing concurrency tokens eliminates the exception but introduces silent data loss through last-write-wins behavior. Concurrent updates will silently overwrite each other without any notification, which is far worse than a visible exception in most business scenarios
-
Wrapping SaveChangesAsync in a try-catch that silently swallows DbUpdateConcurrencyException
90% fail
Silently swallowing the exception means the user's changes are discarded without notification. The entity in memory still holds stale data and the user believes their save succeeded. This leads to data inconsistency and confused users who cannot understand why their changes disappeared
-
Using Serializable transaction isolation level to prevent concurrent access
75% fail
Serializable isolation causes severe performance degradation and deadlocks under concurrent load. It converts optimistic concurrency to pessimistic locking, which does not scale and introduces lock timeout errors that are harder to handle than concurrency exceptions. The cure is worse than the disease for web applications