Skip to main content

NP0003: SaveChanges runs once per element (N+1 write)

Default severity: Warning · Code fix: moves the call after the loop

What it detects​

SaveChanges or SaveChangesAsync called once per element, directly or through a helper:

foreach (var order in orders)
{
order.Total = 0;
await db.SaveChangesAsync(); // NP0003
}

A transaction started once per element (Database.BeginTransaction, BeginTransactionAsync) is reported the same way: its begin and commit are round trips, and each element commits on its own. Start one transaction around the loop.

ABP's repository writes with autoSave: true (InsertAsync, UpdateAsync, DeleteAsync, and their Many forms) save at once, so they are reported per element too; without it the unit of work saves everything once at its end.

Why it matters​

Every call is a round trip and a transaction. Saving once after the loop sends the same changes in one batch (EF Core batches the statements) inside one transaction, which is also what most code means: all or nothing.

How to fix it​

Call it once after the loop (the code fix does this when the call is a statement directly in the loop body), or replace load-modify-save with one set-based statement:

await db.Orders.Where(o => ids.Contains(o.Id)).ExecuteUpdateAsync(s => s.SetProperty(o => o.Total, 0));

If each element really must be committed on its own (the side effect after it must not be lost when a later element fails), suppress the warning there and say why.

What it leaves alone​

foreach over Chunk(...): saving once per batch is the fix for very large loops.