Transactions in the MongoDB EF Core Provider
- 📅
- 📝 1,350 words
- 🕙 6 minutes
- 📦 Development
- 🏷️ Entity Framework, MongoDB
Database transactions ensure that multi-record (or in our case multi-document) operations either all succeed or all fail together.
This is absolutely critical for data consistency in your applications. EF Core has support for three different types of transactions: implicit, explicit, and ambient.
Our upcoming 9.0.3 release adds explicit transaction support, joining the implicit transaction support we added in August 2024.
Let's cover how the different types of transactions behave and can be used.
Setup MongoDB for transactions
We need MongoDB running in a transaction-capable mode such as replica set or sharded cluster configuration. Note: The default standalone mode does not support transactions.
The good news is that it's easy to run MongoDB in replica set mode, and you have several options:
Docker
Docker is an easy way to get started. The mongodb-atlas-local image provides replica set transaction support by default, plus vector and text search capabilities.
docker run --name mongodb-atlas-local -p 27017:27017 -d mongodb/mongodb-atlas-local
This fetches the latest Atlas-capable MongoDB image for Docker and runs it locally, binding it to port 27017 (MongoDB's default port).
Manual setup
If you prefer to run MongoDB directly, download MongoDB Community Server and start it with the replica set option:
mongod --replSet rs0 --port 27017
Then connect with mongosh and initialize the replica set:
rs.initiate({
_id: "rs0",
members: [{ _id: 0, host: "localhost:27017" }],
})
Atlas Cloud
MongoDB Atlas Cloud provides replica sets by default, so if you're already using it, you're all set.
Alternatively, you can install the MongoDB Atlas CLI, which will guide you through setting up an Atlas account and creating your cluster.
Understanding transaction types
The MongoDB EF Core Provider provides support for both implicit and explicit transactions. Implicit is simple and automatic while explicit gives you more control for performing multi-step operations.
Implicit transactions
Implicit transactions are automatically created when SaveChanges (or SaveChangesAsync) detects multiple documents being modified, inserted, or deleted. Note: You can disable this behavior through the AutoTransactions option, though this is not recommended.
Let's look at a simple bank account transfer example. Here's our minimal model and DbContext subclass with setup code:
public record Account(ObjectId Id, string Number, decimal Balance);
public record AuditLog(ObjectId Id, DateTime Timestamp, string Action, string Details);
public class BankContext(DbContextOptions<BankContext> options) : DbContext(options)
{
public DbSet<Account> Accounts { get; set; }
public DbSet<AuditLog> AuditLogs { get; set; }
}
var optionsBuilder = new DbContextOptionsBuilder<BankContext>()
.UseMongoDB("mongodb://localhost:27017/?directConnect=True", "banking");
using var db = new BankContext(optionsBuilder.Options);
Now when we modify multiple documents in a single SaveChanges call, the provider automatically wraps them in a transaction:
var source = db.Accounts.First(a => a.Number == "12345");
var target = db.Accounts.First(a => a.Number == "67890");
// Transfer funds
source.Balance -= 100m;
target.Balance += 100m;
// Automatically uses an implicit transaction because we're affecting multiple documents
db.SaveChanges();
The provider detects that two documents are being affected and automatically creates a transaction to ensure both balance updates succeed or fail together.
Explicit transactions
Explicit transactions give you complete control over transaction boundaries and options. This is useful when you need to:
- Include multiple
SaveChangescalls in one transaction - Mix database operations with non-database operations
- Handle complex business logic that spans multiple steps
- Explicitly control on whether to commit or rollback
Here's the same bank transfer operation using an explicit transaction (and this time using async):
// Start an explicit transaction
await using var transaction = await db.Database.BeginTransactionAsync();
try
{
var source = await db.Accounts.FirstAsync(a => a.Number == "12345");
var target = await db.Accounts.FirstAsync(a => a.Number == "67890");
// Check if sufficient funds
if (source.Balance < 100m)
throw new InvalidOperationException("Insufficient funds");
Console.WriteLine($"Withdrawing from {source.Number}");
source.Balance -= 100m;
await db.SaveChangesAsync();
Console.WriteLine($"Depositing to {target.Number}");
target.Balance += 100m;
await db.SaveChangesAsync();
await transaction.CommitAsync();
Console.WriteLine("Transfer completed successfully");
}
catch (Exception ex)
{
// Something went wrong, rollback
await transaction.RollbackAsync();
Console.WriteLine($"Transfer failed: {ex.Message}");
throw;
}
Explicit transactions let you span multiple SaveChanges (or SaveChangesAsync) calls and include your own validation logic, all within a single atomic operation.
Ambient transactions
Ambient transactions are a feature in the .NET CLR whereby System.Transactions.Transaction.Current provides a way to coordinate a transaction across technologies.
Due to the complicated nature of System.Transactions and the difficulties in using it correctly with async/await and multi-threading, we have decided instead to encourage use of our explicit transaction support, which also allows you to coordinate work across providers but in a more explicit manner.
When to use implicit vs explicit transactions
Choosing between implicit and explicit transactions depends on your specific use case. Here's a comparison to help you decide:
| Scenario | Implicit Transactions | Explicit Transactions |
|---|---|---|
Single SaveChanges call | ✅ Automatic and simple | ❌ Unnecessarily complex |
Multiple SaveChanges calls | ❌ Each call is a separate transaction | ✅ Spans multiple operations |
| Error handling control | ❌ Automatic rollback only | ✅ Custom error handling |
| Cross-provider steps | ❌ Not supported | ✅ Coordinate with other providers |
Quick decision guide
- Use implicit transactions when you have simple multi-document operations that can be completed in a single
SaveChangescall - Use explicit transactions when you need multiple
SaveChangescalls, custom validation, or complex error handling
Implementation considerations
Scope
Starting an explicit transaction on a DbContext means that context instance can read all its own uncommitted writes, including across collections. For example:
using var transaction = db.Database.BeginTransaction();
try
{
var account = db.Accounts.First(a => a.Number == "12345");
account.Balance += 500m;
db.SaveChanges();
db.AuditLogs.Add(new AuditLog
{
Timestamp = DateTime.UtcNow,
Action = "Deposit",
Details = $"Added 500 to account {account.Number}"
});
db.SaveChanges();
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
Any read operations on different instances of your DbContext will be isolated from the uncommitted writes. They cannot opt-in to the transaction via any kind of UseTransaction operation.
You may BeginTransaction with specific TransactionOptions that tailor the read preference, read concern, write concern, and max commit time.
Performance & limitations
Transactions in MongoDB have some technical implications to be aware of:
- 🕑 Keep transactions short: Long-running transactions can impact cluster performance
- 🤏 Limit transaction size: MongoDB has a 16MB limit on transaction sizes
- 🔢 Consider operation order: Place read operations before write operations when possible
- ✒️ Use write concerns wisely: Higher write concerns increase latency but improve durability
- 📁 Use
db.Database.EnsureCreated(): Ensure you call this at app startup as collections cannot be created in cross-shard write transactions
Check out the MongoDB Limits and Thresholds.
As always, start with the defaults and aim for code clarity, then measure and optimize as appropriate. Performance optimizations are usually at the expense of clarity, and you don't want to start with wrong-optimized, hard-to-understand code.
Also check out the MongoDB Production Considerations for Transactions.
That's a wrap
The MongoDB EF Core Provider's transaction support gives you the flexibility to choose between convenient automatic implicit transactions and powerful explicit transactions for multi-step scenarios.
In summary:
- Implicit transactions happen automatically today inside
SaveChanges - Explicit transactions let you perform multiple
SaveChangesbefore rolling back or committing - Always use replica sets in production to provide support for transactions
- Use optimistic concurrency to ensure you are not writing back stale data
You can find the full source code to the samples over at GitHub.
Enjoy!
Damien
0 responses to Transactions in the MongoDB EF Core Provider