2
  • 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:

  1. Can I simply replace the _db.Database.BeginTransaction() syntax with the using (var scope = new TransactionScope()) syntax everywhere in my code that wants to perform database operations within a transaction? Does the TransactionScope syntax add significant overhead or come with ugly side-effects?

  2. 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 the using (var scope = new TransactionScope()) syntax?

  3. If the code that calls this routine has created a new TransactionScope (as opposed to explicitly creating an IDbContextTransaction), does the call to _db.Database.CurrentTransaction still 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?

1
  • Yes, you can replace _db.Database.BeginTransaction() with TransactionScope for ambient transactions. Note that TransactionScope may escalate to distributed transactions, requiring MSDTC and adding overhead. Within async, use TransactionScopeAsyncFlowOption.Enabled. To detect an ambient transaction in EF Core, check Transaction.Current instead of _db.Database.CurrentTransaction. Ensure thorough testing for correct behavior across all services. Commented Nov 23, 2023 at 15:23

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.