6

I am developing small POC application to test .NET7 support for distributed transactions since this is pretty important aspect in our workflow.

So far I've been unable to make it work and I'm not sure why. It seems to me either some kind of bug in .NET7 or im missing something.

In short POC is pretty simple, it runs WorkerService which does two things:

  1. Saves into "bussiness database"
  2. Publishes a message on NServiceBus queue which uses MSSQL Transport.

Without Transaction Scope this works fine however, when adding transaction scope I'm asked to turn on support for distributed transactions using:

TransactionManager.ImplicitDistributedTransactions = true;

Executable code in Worker service is as follows:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        int number = 0;
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                number = number + 1;
                using var transactionScope = TransactionUtils.CreateTransactionScope();
              
               
                await SaveDummyDataIntoTable2Dapper($"saved {number}").ConfigureAwait(false);
             
                await messageSession.Publish(new MyMessage { Number = number }, stoppingToken)
                    .ConfigureAwait(false);

                _logger.LogInformation("Publishing message {number}", number);
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                 transactionScope.Complete();
                _logger.LogInformation("Transaction complete");
                await Task.Delay(1000, stoppingToken);
            }
        }
        catch (Exception e)
        {
            _logger.LogError("Exception: {ex}", e);
            throw;
        }
    }

Transaction scope is created with the following parameters:

public class TransactionUtils 
{
    public static TransactionScope CreateTransactionScope()
    {
        var transactionOptions = new TransactionOptions();
        transactionOptions.IsolationLevel = IsolationLevel.ReadCommitted;
        transactionOptions.Timeout = TransactionManager.MaximumTimeout;
        return new TransactionScope(TransactionScopeOption.Required, transactionOptions,TransactionScopeAsyncFlowOption.Enabled);
    }
}

Code for saving into database uses simple dapper GenericRepository library:

private async Task SaveDummyDataIntoTable2Dapper(string data)
    {
        using var scope = ServiceProvider.CreateScope();
        var mainTableRepository = 
            scope.ServiceProvider
                .GetRequiredService<MainTableRepository>();
        await mainTableRepository.InsertAsync(new MainTable()
        {
            Data = data,
            UpdatedDate = DateTime.Now
        });
    }

I had to use scope here since repository is scoped and worker is singleton so It cannot be injected directly.

I've tried persistence with EF Core as well same results:

Transaction.Complete() line passes and then when trying to dispose of transaction scope it hangs(sometimes it manages to insert couple of rows then hangs).

Without transaction scope everything works fine

I'm not sure what(if anything) I'm missing here or simply this still does not work in .NET7?

Note that I have MSDTC enable on my machine and im executing this on Windows 10

1

5 Answers 5

3

Ensure you're using Microsoft.Data.SqlClient +v5.1 Replace all "usings" System.Data.SqlClient > Microsoft.Data.SqlClient

Ensure ImplicitDistributedTransactions is set True:

TransactionManager.ImplicitDistributedTransactions = true;

using (var ts = new TransactionScope(your options))
{
    TransactionInterop.GetTransmitterPropagationToken(Transaction.Current);
    
    ... your code ..
    
    
    ts.Complete();
}
Sign up to request clarification or add additional context in comments.

4 Comments

This enabled my TransactionScope instances to complete without hanging, but the secondary database transaction didn't actually save. Could this be related to EF Core 7 w/SQL Server referencing Microsoft.Data.SqlClient v. 5.0.1 instead of 5.1?
Yes, you need update 5.1 first. See: github.com/dotnet/SqlClient/pull/1801
I guess my question here is if EF Core 7 is referencing 5.0.1, I'm not sure how I can update that since it's an external package and not something I control? Do I just wait for the EF team to reference the new version and update my package when they do?
You are probably referencing System.Data.SqlClient and not Microsoft.Data.SqlClient Try to add the reference Microsoft.Data.SqlClient and .SNI 5.1 from NUGET, and replace from visual studio all "usings". I have migrated several .NET 4.8 / EF 6 projects to .NET 7 / EF6 and it works correctly.
2

For anyone who uses multiple nested TransactionScope instances that can be scattered across multiple business classes, I came up with a simple class that lets you keep a similar approach, but enables the TransactionManager.ImplicitDistributedTransactions flag and links a scope to it with lambda functions for ease of use.

*Note this has only been mildly tested, but it seems to work! I'm currently running on EF Core 7.0.4.

public class InternalTransactionScope
    {
        /// <summary>
        /// Executes an <see cref="Action"/> within the context
        /// of a <see cref="TransactionScope"/> that has enabled
        /// support for distributed transactions.
        /// </summary>
        /// <param name="action"></param>
        public static void ExecuteTransaction(Action action)
        {
            //enable distributed transactions
            TransactionManager.ImplicitDistributedTransactions = true;

            using (var scope = new TransactionScope())
            {
                //link this scope to our overall transaction
                TransactionInterop.GetTransmitterPropagationToken(Transaction.Current);

                //execute and complete the scope
                action();
                scope.Complete();
            }
        }
        
        /// <summary>
        /// Executes an <see cref="Action"/> within the context
        /// of a <see cref="TransactionScope"/> that has enabled
        /// support for distributed transactions and returns the
        /// result of the execution.
        /// </summary>
        /// <typeparam name="T">The type of return value expected.</typeparam>
        /// <param name="action">The action to execute and retrieve a value from.</param>
        /// <returns>An instance of <typeparamref name="T"/> representing the result of the request.</returns>
        public static T ExecuteTransaction<T>(Func<T> action)
        {
            //enable distributed transactions
            TransactionManager.ImplicitDistributedTransactions = true;

            using (var scope = new TransactionScope())
            {
                //link this scope to our overall transaction
                TransactionInterop.GetTransmitterPropagationToken(Transaction.Current);

                //execute and complete the scope
                var result = action();
                scope.Complete();

                return result;
            }
        }
    }

Comments

0

We've been able to solve this by using the following code.

With this modification DTC is actually invoked correctly and works from within .NET7.

using var transactionScope = TransactionUtils.CreateTransactionScope().EnsureDistributed();

Extension method EnsureDistributed implementation is as follows:

   public static TransactionScope EnsureDistributed(this TransactionScope ts)
    {
        Transaction.Current?.EnlistDurable(DummyEnlistmentNotification.Id, new DummyEnlistmentNotification(),
            EnlistmentOptions.None);

        return ts;
    }

    internal class DummyEnlistmentNotification : IEnlistmentNotification
    {
        internal static readonly Guid Id = new("8d952615-7f67-4579-94fa-5c36f0c61478");
        public void Prepare(PreparingEnlistment preparingEnlistment)
        {
            preparingEnlistment.Prepared();
        }
        public void Commit(Enlistment enlistment)
        {
            enlistment.Done();
        }
        public void Rollback(Enlistment enlistment)
        {
            enlistment.Done();
        }
        public void InDoubt(Enlistment enlistment)
        {
            enlistment.Done();
        }

This is 10year old code snippet yet it works(im guessing because .NET Core merely copied and refactored the code from .NET for DistributedTransactions, which also copied bugs).

What it does it creates Distributed transaction right away rather than creating LTM transaction then promoting it to DTC if required.

More details explanation can be found here:

https://www.davidboike.dev/2010/04/forcibly-creating-a-distributed-net-transaction/

https://github.com/davybrion/companysite-dotnet/blob/master/content/blog/2010-03-msdtc-woes-with-nservicebus-and-nhibernate.md

Comments

0

At the time of writing this post the transaction scope used to have a deadlock issue running on .NET Framework. The deadlock has been fixed as part of https://github.com/dotnet/SqlClient/pull/1242 but was never ported to .NET. With the introduction of distributed transaction support for .NET 7 and higher on Windows it is possible to run into the same deadlock. I have submitted a pull request that hopefully will fix this.

1 Comment

This indeed sounds like the issue we encountered. In meantime we dropped DTC and moved to outbox.
0

This happened to me when I had unawaited async call inside transaction-wrapped code. After adding await, the problem was gone.

Comments

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.