DbUpdateConcurrencyException – Complete Fix Guide

DbUpdateConcurrencyException – Complete Fix Guide

🔄 DbUpdateConcurrencyException Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException

Complete troubleshooting guide — understand optimistic concurrency, how to detect and handle conflicts, and build resilient data access layers in EF Core.

What is DbUpdateConcurrencyException?

DbUpdateConcurrencyException is thrown by Entity Framework Core when an optimistic concurrency conflict occurs during a SaveChanges or SaveChangesAsync call. This happens when you attempt to update or delete an entity that has been modified or deleted in the database since it was loaded into the context.

⚠️ Typical Stack Trace

Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.

This is a runtime exception that signals a data consistency issue. It is not a bug in EF Core but an indication that your application needs to handle concurrent edits gracefully.

🔍 Root Causes

The exception occurs when one of the following conditions is true:

1️⃣ Concurrent update by another user Most common

Two users loaded the same entity, one saved changes, then the other tried to save an older version.

2️⃣ The entity was deleted in the database Deletion

Another process deleted the record after it was loaded into the context.

3️⃣ Missing or misconfigured concurrency token Configuration

No RowVersion or ConcurrencyCheck attribute is applied, so EF can't detect changes.

4️⃣ Incorrect timestamp value sent from client API

When using disconnected scenarios, the client may send an outdated row version.

5️⃣ Database trigger or computed column Advanced

A trigger modifies the row after EF’s update, causing the rowversion to change unexpectedly.

6️⃣ Using UseSqlServer without proper isolation level Transaction

Snapshot isolation or other settings can affect concurrency detection.

🛠️ Step‑by‑Step Fixes

Apply these strategies to resolve concurrency issues:

1 Identify the entity and the conflict

Catch the exception and inspect the Entries property to see which entities caused the conflict.

2 Retrieve the current database values

Use ReloadAsync() or GetDatabaseValues() to get the latest values from the database.

3 Decide on a resolution strategy

Common strategies: client wins (overwrite), database wins (discard changes), or merge (combine).

4 Implement retry logic with exponential backoff

Use Polly to retry the operation a few times with delays to give transient conflicts time to resolve.

5 Notify the user

In UI applications, inform the user that the data changed and ask them to review their changes.

✅ Quick Tip

Always include a row version (timestamp) column in your entities to enable EF to detect conflicts automatically.

⚖️ Optimistic Concurrency Explained

EF Core uses optimistic concurrency by default. It assumes that conflicts are rare, so it doesn't lock rows during reads. When saving, it checks if the row has changed since it was loaded.

To detect changes, EF uses a concurrency token – a property marked with [Timestamp] or [ConcurrencyCheck]. During update, EF includes the original value of this token in the SQL WHERE clause. If the token value doesn't match the database, the update affects zero rows and EF throws the exception.

// Example entity with RowVersion public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } [Timestamp] public byte[] RowVersion { get; set; } } // EF generates SQL like: // UPDATE Products SET Name = @p1, Price = @p2 // WHERE Id = @p0 AND RowVersion = @p3 // If @p3 doesn't match, 0 rows affected → exception.

💡 Note

Without a concurrency token, EF will still try to update by primary key only – but then you won't get a concurrency exception; instead, the last write wins (lost update problem).

⏱️ Configuring RowVersion (Timestamp)

In SQL Server, use a rowversion (formerly timestamp) column. In other databases, use a byte[] with IsRowVersion or IsConcurrencyToken.

Fluent API configuration

modelBuilder.Entity<Product>() .Property(p => p.RowVersion) .IsRowVersion(); // or .IsConcurrencyToken() for other types

Data Annotations

[Timestamp] public byte[] RowVersion { get; set; }

When you retrieve an entity, the RowVersion is populated. When you update, you must send the same RowVersion back so EF can include it in the WHERE clause.

⚠️ Important for API scenarios

If you expose entities via REST APIs, you must send the RowVersion back to the server. Otherwise, EF will use the default value (all zeros) and the update will fail (0 rows affected).

🛡️ Handling DbUpdateConcurrencyException

When the exception occurs, you can use the Entries collection to get the affected entities and their current database values.

try { await context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException ex) { foreach (var entry in ex.Entries) { var databaseValues = await entry.GetDatabaseValuesAsync(); var clientValues = entry.Entity; // Decide which values to keep // Option 1: Client wins – overwrite database entry.OriginalValues.SetValues(databaseValues); // Then retry // Option 2: Database wins – discard client changes entry.Reload(); // Option 3: Merge – combine client and database values // ... } // Retry SaveChanges }

In a typical web API, you might return a 409 Conflict response with details, and let the client resolve the conflict.

🔁 Retry Logic with Polly

Transient concurrency conflicts can be resolved by retrying the entire transaction. Use Polly to implement a retry policy specifically for DbUpdateConcurrencyException.

// Define a retry policy var retryPolicy = Policy .Handle<DbUpdateConcurrencyException>() .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (exception, timeSpan, context) => { // Log or handle }); // Use it await retryPolicy.ExecuteAsync(async () => { using var context = new MyDbContext(); // ... load, modify, save await context.SaveChangesAsync(); });

In ASP.NET Core, you can register a retry policy with AddDbContext using EnableRetryOnFailure:

services.AddDbContext<MyDbContext>(options => options.UseSqlServer(connectionString, sqlOptions => sqlOptions.EnableRetryOnFailure(3, TimeSpan.FromSeconds(2), null)));

💡 Note

This built‑in retry handles transient errors (including concurrency) but may not be sufficient for all conflict scenarios. Combining with manual handling is recommended.

🌐 ASP.NET Core & API Design

In web APIs, you typically receive a DTO with the entity's data, including the row version. When a concurrency conflict occurs, you can return a 409 Conflict with the current database values so the client can decide.

[HttpPut("{id}")] public async Task<IActionResult> UpdateProduct(int id, ProductDto dto) { var product = await _context.Products.FindAsync(id); if (product == null) return NotFound(); // Map DTO to entity, include RowVersion _context.Entry(product).CurrentValues.SetValues(dto); _context.Entry(product).Property(p => p.RowVersion).OriginalValue = dto.RowVersion; try { await _context.SaveChangesAsync(); return Ok(product); } catch (DbUpdateConcurrencyException) { // Get current database values var dbProduct = await _context.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id); return Conflict(new { message = "The record was modified by another user.", current = dbProduct }); } }

🧪 Testing Concurrency Scenarios

To test concurrency, you can simulate two contexts modifying the same entity.

// Simulate two users using (var context1 = new MyDbContext()) using (var context2 = new MyDbContext()) { var product1 = await context1.Products.FindAsync(1); var product2 = await context2.Products.FindAsync(1); product1.Price = 10; product2.Price = 20; await context1.SaveChangesAsync(); // succeeds await Assert.ThrowsAsync<DbUpdateConcurrencyException>(() => context2.SaveChangesAsync()); }

In unit tests, you can mock the DbContext or use an in‑memory database (though in‑memory doesn't enforce concurrency, so you'd need a real database or a custom test double).

🛡️ Prevention and Best Practices

✅ Always include a concurrency token

Use RowVersion (byte[]) with [Timestamp] or IsRowVersion() for SQL Server.

✅ Send the token back from client

In disconnected scenarios (REST, Blazor, etc.), ensure the client sends the original token.

✅ Use retry policies

Handle transient conflicts with Polly or built‑in EF retry logic.

✅ Provide user feedback

In UI apps, show a conflict resolution dialog so users can decide how to merge changes.

✅ Use optimistic concurrency for high‑conflict scenarios

If conflicts are frequent, consider using pessimistic locking (e.g., UPDLOCK), but this can reduce scalability.

✅ Log conflicts

Log details (entity, old/new values) to help diagnose frequent conflicts.

🏆 Pro Tip

For large scale applications, consider using ETag (HTTP) with row version to let clients know if their version is stale before they even try to update – this reduces unnecessary roundtrips.

Post a Comment

0 Comments