A database transaction groups related updates so they either all commit or all roll back. Consider a transfer that credits one account and debits another. If the debit fails after the credit is saved, the balances are wrong unless both updates share a transaction. The database manages that boundary even though the application, its network connection, or its host can fail. section 11.2.1
A transfer that fails halfway
In an EF Core repository, CreditAsync changes a balance and calls SaveChangesAsync; DebitAsync does the same but first rejects insufficient funds. Calling them in sequence without a shared transaction can save the credit before the debit throws. A test transferring more than the source account holds should find both balances unchanged, not just catch the exception. section 11.2.1
Listing 11.11 A transactional version of the BankRepository.TransferAsync
public async Task TransferAsync(
string from, string to, decimal amt)
{
using var tx = await
_ctxt.Database.BeginTransactionAsync(); #1
try
{
await CreditAsync(to, amt); #2
await DebitAsync(from, amt);
await tx.CommitAsync(); #3
}
catch #4
{
await tx.RollbackAsync(); #5
_ctxt.ChangeTracker.Clear(); #6
throw; #7
}
}
The transactional TransferAsync begins an EF Core transaction before either account update, commits after both succeed, and rolls back on an exception. It also clears the change tracker before rethrowing: tracked account objects may otherwise still reflect changes that the database discarded. section 11.2.1
Retries add a second boundary to consider. EF Core’s SQL Server execution strategy can retry transient faults, but enabling it does not make the default strategy handle an explicitly started transaction. Put the whole transaction inside an execution strategy when it must be retried. If the connection fails during commit, the application may not know whether the transaction succeeded; when possible, verify its outcome before repeating the work. section 11.2.2
Listing 11.13 Transaction execution strategies
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
namespace MoneyTransfer;
public static class Extensions
{
public static Task ExecuteRetryableTransactionAsync( #1
this DbContext ctxt, #2
Func<Task> action) #3
{
IExecutionStrategy strategy =
ctxt.Database.CreateExecutionStrategy(); #4
return strategy.ExecuteAsync(async () =>
{
using IDbContextTransaction tx =
await ctxt.Database.BeginTransactionAsync();
try
{
await action(); #5
await tx.CommitAsync();
}
catch
{
await tx.RollbackAsync();
ctxt.ChangeTracker.Clear(); #6
throw; #7
}
});
}
public static Task ExecuteVerifyingTransactionAsync( #8
this DbContext ctxt,
Func<Task> action,
Func<Task<bool>> didItWork) #9
{
IExecutionStrategy strategy =
ctxt.Database.CreateExecutionStrategy();
return strategy.ExecuteInTransactionAsync( #10
action, didItWork);
}
}
The transaction helper obtains an execution strategy, starts the transaction inside its operation, runs the supplied action, and commits. Its exception path rolls back and clears EF Core’s change tracker. A verifying variant checks the result when commit leaves the outcome uncertain. section 11.2.2













