I have code that deletes records older than 30 days using an Entity Framework Core 6 DbContext
. The deletion logic is executed concurrently from multiple places, and sometimes it throws a DbUpdateConcurrencyException
with the message:
The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded
I suspect that a race condition occurs where two processes identify the same old entities, but only one manages to delete them, causing the other to fail its concurrency check.
Here is the initial deletion code:
var expireBefore = DateTime.Now.AddDays(-30);
db.MyTable.RemoveRange(db.MyTable.Where(t => t.CreatedDate <= expireBefore));
await db.SaveChangesAsync();
To mitigate the issue, I wrapped the deletion code in a transaction with a serializable isolation level, assuming this would prevent the race condition. However, the exception still occurs occasionally. Below is the transaction version:
var expireBefore = DateTime.Now.AddDays(-30);
var executionStrategy = db.Database.CreateExecutionStrategy();
await executionStrategy.ExecuteAsync(async () =>
{
await using var transaction = await db.Database.BeginTransactionAsync(IsolationLevel.Serializable);
db.MyTable.RemoveRange(db.MyTable.Where(t => t.CreatedDate <= expireBefore));
await db.SaveChangesAsync();
await transaction.CommitAsync();
});
Why does placing the delete operations inside a transaction not prevent the concurrency exception?