-
Notifications
You must be signed in to change notification settings - Fork 4
Transactions
ExecuteInTransactionAsync runs a block of repository operations inside one MongoDB transaction: they
all commit together, or all abort if the block throws.
await db.ExecuteInTransactionAsync(async () =>
{
var from = await db.Accounts.FindOneAsync(fromId);
var to = await db.Accounts.FindOneAsync(toId);
from.Withdraw(amount);
to.Deposit(amount);
await db.SaveChangesAsync(); // enlists in the ambient transaction
});
// Or return a value:
var receipt = await db.ExecuteInTransactionAsync(async () => { /* ... */ return r; });Every operation on this context's repositories inside the block — CRUD, LINQ queries,
and SaveChangesAsync — enlists automatically in the transaction (the LINQ3 provider runs through
the collection's Aggregate* methods, so queries enlist too). The transaction commits when the
block completes and aborts if it throws, discarding every enlisted operation.
A failure the server labels transient (TransientTransactionError: a write conflict with a
concurrent transaction, a primary election) retries the whole block on a new transaction of the
same session, with the policy of the driver WithTransactionAsync: a random delay between the
attempts (up to 5 ms growing by 1.5 per failed attempt, at most 500 ms), the commit retried on the
same transaction when its result is unknown (UnknownTransactionCommitResult), and every attempt
bounded by the TransactionRetryTimeout db context option (2 minutes by
default), measured from the first start. A transient failure past the budget throws to the caller like
any other failure, and a cancelled flow never retries. Each retry logs a warning with the failed
attempt and its exception.
The replay starts from the unit of work as the aborted attempt left it:
- the change tracking of the saves inside the block follows the commit:
a saved model refreshes and leaves the change candidates only when the transaction commits, so after
an abort it is still tracked and still dirty, and the next
SaveChangesAsync— the replay included — writes its changes again; - the creates inside the block are undone at the abort: the ids the create assigned return to
null, and the created instances leave the identity map and the change tracking, so a replayedCreateAsyncinserts the model anew, with its new referred models created again.
A save re-applies only the members changed against the model snapshot, so a retry landing after the conflicting transaction committed is last-writer-wins per member, like a save outside transactions: disjoint members of the two writers both survive.
Warning. The block must be replayable: any side effect it has outside the db context (a message sent, a file written) runs once per attempt.
-
Needs a deployment supporting transactions — a replica set, sharded cluster or load-balanced
topology, detected at runtime from the cluster topology (
context.Engine.SupportsTransactions); on a standalone server transactions aren't available. -
A non transient failure throws at once. Only the failures the server labels transient retry;
any other exception aborts the transaction and throws to the caller, with the saves it rolled back
still pending on the db context (the next
SaveChangesAsyncwrites them again) and the creates it rolled back undone. -
Sequential only. A session can't run concurrent operations; don't
Task.WhenAllrepository calls inside the block. - Scoped to this context's connection. Operations on other db contexts — children included — don't enlist. Change-stream watches and estimated document counts stay session-less (not allowed in transactions). Background dependency updates enqueued by saves are not transactional; after an abort they converge to the committed state.
You don't need ExecuteInTransactionAsync just to make one SaveChangesAsync atomic — that already
runs in its own implicit transaction when supported (see Change tracking and saving). Reach for
ExecuteInTransactionAsync when you must group several operations (multiple saves, deletes, cross-
repository writes) into one atomic unit. Inside it, SaveChangesAsync enlists rather than opening a
nested transaction.
For single-document atomicity you don't need a transaction (or a replica set) at all — a server-
side atomic update (AccessToCollectionAsync + FindOneAndUpdateAsync, or an
Upsert* helper) is simpler and race-free. Use transactions when the atomic unit genuinely spans
multiple documents.
Next: Change tracking and saving for save internals, or Exclusive access for locking a context during maintenance.
Scrinium — source · issues (SCR) · GNU LGPL-3.0 · info@etherna.io
Getting started
Core concepts
Working with data
Serialization & mapping
Operations & maintenance
Advanced & reference