- ASP.NET Core 7
- EF Core 7
- SQL Server
I have a routine that wants to perform multiple steps that involve calling DbContext.SaveChanges(). Thus, the routine wants to wrap all its work in a transaction. The complication is that the routine may be called in the context of a parent transaction. Since EF Core doesn't allow nested transactions, I use this pattern to create a transaction if the parent hasn't already created one:
public async Task<SomeResultType> AddPatient(<arguments>)
{
// Create a transaction?
if (_db.Database.CurrentTransaction == null)
{
// Create a transaction with retries
// https://blogs.msdn.microsoft.com/cesardelatorre/2017/03/26/using-resilient-entity-framework-core-sql-connections-and-transactions-retries-with-exponential-backoff
var strategy = _db.Database.CreateExecutionStrategy();
return await strategy.ExecuteAsync(async () => {
// Begin the transaction
using var transaction = await _db.Database.BeginTransactionAsync();
// Recursively call ourself to do the work within the transaction
var result = await AddPatient(<arguments>);
// Commit the transaction
if (result.Success)
await transaction.CommitAsync();
// Done
return result;
});
}
else
{
// Executing within a transaction
<Do the work>
// Done
Return someResult;
}
}
In this pattern, if the current transaction is null then I create a transaction (an IDbContextTransaction) via await _db.Database.BeginTransactionAsync(), then recursively call the routine. The recursion sees that the current transaction is non-null, so it proceeds to perform the work within the transaction.
Now, my challenge is that, in addition to adding data to the database using Entity Framework, my routine needs to call UserManager.Create(). But UserManager.Create() does not participate in the transaction.
The documentation mentions using "ambient transactions" to "coordinate across a larger scope", and provides an example that appears to replace the _db.Database.BeginTransaction() construct with a
using (var scope = new TransactionScope())
construct. The example code appears to replace the transaction.Commit() call with a call to scope.Complete().
In addition to the above referenced documentation, this documentation goes into detail about Implementing an Implicit Transaction using Transaction Scope.
I've turned my brain to mush trying to digest this documentation. The documentation seems to say that "transaction scope" causes operations to magically create transactions and coordinate their transactions with one another. I couldn't find any deeper explanation of when this does or does not work, although I did find examples using that technique to be able to roll back UserManager.Create() along with other EF operations if something goes wrong.
I'm hoping someone who really understands this stuff can answer some really basic questions about this technology:
Can I simply replace the
_db.Database.BeginTransaction()syntax with theusing (var scope = new TransactionScope())syntax everywhere in my code that wants to perform database operations within a transaction? Does theTransactionScopesyntax add significant overhead or come with ugly side-effects?To create a transaction in my example above (with the added complexity of executing within the "retry" strategy), can I simply replace the
_db.Database.BeginTransaction()syntax with theusing (var scope = new TransactionScope())syntax?If the code that calls this routine has created a new
TransactionScope(as opposed to explicitly creating anIDbContextTransaction), does the call to_db.Database.CurrentTransactionstill detect when this routine is being called within a parent transaction? If not, how do I detect when the routine has been called in a parent "transaction scope"? Or, is there some other, better way to enlist in a parent transaction if one exists?