A single SaveChangesAsync is already a transaction.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // ====================================================================== // Q: How do transactions, isolation levels and retries work in EF Core? // // - A single SaveChangesAsync is already a transaction. You only need an // explicit transaction when several SaveChanges or ExecuteUpdate calls must // happen together. // - Isolation: the default is Read Committed (SQL Server, PostgreSQL). // Repeatable Read / Serializable prevent more anomalies but cause // serialization failures, so they need retries. On SQL Server consider // READ_COMMITTED_SNAPSHOT so reads and writes do not block each other. // - Transient errors: EnableRetryOnFailure. With your own transaction, wrap the // whole unit of work in an execution strategy so a retry re-runs all of it. // - Keep transactions short: never make an HTTP call (ERP, email) inside a // transaction. It holds locks and a connection while it waits (Q10, outbox). // ====================================================================== builder.Services.AddDbContext<AppDbContext>(o => o.UseNpgsql(cs, npg => npg.EnableRetryOnFailure(maxRetryCount: 3))); var strategy = db.Database.CreateExecutionStrategy(); await strategy.ExecuteAsync(async () => { await using var tx = await db.Database.BeginTransactionAsync( IsolationLevel.Serializable, ct); // ... all the work goes in here await db.SaveChangesAsync(ct); await tx.CommitAsync(ct); }); |