c# - Entity Framework Core : delete old items results in DbUpdateConcurrencyException - Stack Overflow

admin2025-04-17  4

I had the following code that is deleting items in a table that were created more than 30 days ago (using an Entity Framework Core 6 DbContext):

var expireBefore = DateTime.Now.AddDays(-30);           
db.MyTable.RemoveRange(db.MyTable.Where(t => t.CreatedDate <= expireBefore));
await db.SaveChangesAsync();

This code was executing in multiple places at the same time, and it would occasionally throw a DbUpdateConcurrencyException:

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 believe this happened due to a race condition where two places both find the old entities, but only one is able to delete them.

To try and fix the issue I put the code into a transaction, I thought this would prevent the race condition, however the same exception is still occasionally thrown. Why is a transaction here not preventing this issue?

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();
});

I had the following code that is deleting items in a table that were created more than 30 days ago (using an Entity Framework Core 6 DbContext):

var expireBefore = DateTime.Now.AddDays(-30);           
db.MyTable.RemoveRange(db.MyTable.Where(t => t.CreatedDate <= expireBefore));
await db.SaveChangesAsync();

This code was executing in multiple places at the same time, and it would occasionally throw a DbUpdateConcurrencyException:

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 believe this happened due to a race condition where two places both find the old entities, but only one is able to delete them.

To try and fix the issue I put the code into a transaction, I thought this would prevent the race condition, however the same exception is still occasionally thrown. Why is a transaction here not preventing this issue?

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();
});
Share Improve this question edited Jan 30 at 18:19 marc_s 756k184 gold badges1.4k silver badges1.5k bronze badges asked Jan 30 at 18:06 innominate227innominate227 11.7k1 gold badge20 silver badges22 bronze badges 0
Add a comment  | 

1 Answer 1

Reset to default 2

The problem seems to be that you are using .Where to actually read the entities in, then mark them as removed and save the context. This is both inefficient and does not provide proper locking.

In the new versions of EF Core, you can just use an ExecuteDeleteAsync. This executes directly on the database without loading the data into the context, and should do the locking correctly.

var expireBefore = DateTime.Now.AddDays(-30);
await db.MyTable
    .Where(t => t.CreatedDate <= expireBefore));
    .ExecuteDeleteAsync();

If you really, really want to load the entities into the context, then firstly you need a WITH (UPDLOCK) or FOR UPDATE clause to get the correct locking (otherwise the shared lock won't stop someone else reading the data). And secondly you want to do this async.

You also need a transaction, but you don't need an execution strategy.

var expireBefore = DateTime.Now.AddDays(-30);
var query = db.MyTable
        .FromSql("SELECT * FROM dbo.MyTable WITH (UPDLOCK)")
        .Where(t => t.CreatedDate <= expireBefore);
await using var transaction = await db.Database.BeginTransactionAsync(IsolationLevel.Serializable);
db.MyTable.RemoveRange(await query);
await db.SaveChangesAsync();
await transaction.CommitAsync();
转载请注明原文地址:http://anycun.com/QandA/1744899556a89201.html