100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Transactions and Savepoints

How EF Core wraps SaveChanges in implicit transactions, when to use explicit transactions across multiple calls, and how savepoints enable partial rollback.

Performance & TransactionsIntermediate9 min readJul 10, 2026
Analogies

SaveChanges' Implicit Transaction

Every call to SaveChanges() that generates more than one SQL statement is already wrapped in an implicit database transaction by EF Core. If you add three new entities and modify two existing ones, EF batches all five statements inside a single BEGIN TRANSACTION / COMMIT block, so a failure partway through rolls everything back rather than leaving the database half-updated. This implicit behavior covers the common case, but it only spans a single SaveChanges call — if you need atomicity across multiple SaveChanges calls, or want to mix EF operations with raw ADO.NET commands, you need an explicit transaction.

🏏

Cricket analogy: It's like a team's entire fielding change during a bowling over being applied together — the umpire only signals play resumed once every fielder is in position, not partway through the reshuffle.

Explicit Transactions with BeginTransaction

When you need multiple SaveChanges calls, or a mix of EF LINQ operations and raw SQL via ExecuteSqlRaw, to succeed or fail together, you open an explicit transaction with context.Database.BeginTransaction() (or the async BeginTransactionAsync). Everything executed against that context — including subsequent SaveChanges calls — participates in the same transaction until you call transaction.Commit() or transaction.Rollback(). This is the pattern for workflows like 'debit one account, credit another, then log an audit record,' where any failure in step two must undo step one even though they were separate SaveChanges calls.

🏏

Cricket analogy: It's like a Duckworth-Lewis-Stern recalculation that spans an entire rain-affected innings — the revised target isn't finalized until every input (overs lost, wickets, par score) is confirmed together, or the whole recalculation is discarded.

Savepoints for Partial Rollback

Within a single explicit transaction, EF Core automatically creates savepoints around each SaveChanges call when the underlying provider supports them (SQL Server and PostgreSQL both do). A savepoint is a named marker inside the transaction that you can roll back to without discarding everything since the transaction began. If SaveChanges fails, EF automatically rolls back to the savepoint created just before that call, undoing only that batch's statements while preserving earlier successful work in the same transaction — you can then retry or handle the error and continue the transaction instead of being forced to abort it entirely.

🏏

Cricket analogy: It's like a batting partnership where only the most recent partnership resets after a wicket falls — the runs and overs from earlier partnerships in the innings stay on the board rather than the whole innings being scrubbed.

csharp
using var transaction = await context.Database.BeginTransactionAsync();
try
{
    context.Accounts.Find(fromId).Balance -= amount;
    await context.SaveChangesAsync();      // EF creates a savepoint here

    context.Accounts.Find(toId).Balance += amount;
    await context.SaveChangesAsync();      // and another savepoint here

    context.AuditLogs.Add(new AuditLog { Message = "Transfer completed" });
    await context.SaveChangesAsync();

    await transaction.CommitAsync();
}
catch (DbUpdateException)
{
    // EF automatically rolls back to the last savepoint on SaveChanges failure;
    // Rollback here discards the whole transaction if you choose not to retry.
    await transaction.RollbackAsync();
    throw;
}

// Manual savepoints are also available directly:
await transaction.CreateSavepointAsync("BeforeRiskyStep");
// ... risky work ...
await transaction.RollbackToSavepointAsync("BeforeRiskyStep");

Not every provider supports savepoints — check context.Database.AutoSavepointsEnabled and the provider docs; SQLite, for example, has more limited savepoint support than SQL Server or PostgreSQL.

  • SaveChanges() always wraps its own generated SQL statements in an implicit transaction, even without explicit code.
  • Use context.Database.BeginTransactionAsync() when atomicity must span multiple SaveChanges calls or mix EF with raw SQL.
  • An explicit transaction stays open until transaction.CommitAsync() or transaction.RollbackAsync() is called.
  • EF Core automatically creates a savepoint before each SaveChanges call within an explicit transaction on providers that support them.
  • A failed SaveChanges rolls back only to the most recent savepoint, preserving earlier successful work in the same transaction.
  • You can also create and roll back to named savepoints manually via CreateSavepointAsync and RollbackToSavepointAsync.
  • Savepoint support varies by provider — confirm compatibility before relying on partial rollback behavior.

Practice what you learned

Was this page helpful?

Topics covered

#EntityFrameworkCoreStudyNotes#MicrosoftTechnologies#TransactionsAndSavepoints#Transactions#Savepoints#SaveChanges#Implicit#StudyNotes#SkillVeris#ExamPrep