dotnet database_error ai_generated true

Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s).

ID: dotnet/ef-concurrency-conflict

Also available as: JSON · Markdown
83%Fix Rate
85%Confidence
80Evidence
2023-01-01First Seen

Version Compatibility

VersionStatusIntroducedDeprecatedNotes
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.

generic

Workarounds

  1. 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
  2. 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
  3. 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:

  1. 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

  2. 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

  3. 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

Error Chain

Leads to:
Preceded by:
Frequently confused with: