4

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

Bola
  • 718
  • 1
  • 6
  • 20
  • 1
    We are also having this exact same issue, plus another person as well is having this issue as seen here: https://learn.microsoft.com/en-us/answers/questions/1118409/net-7-distributed-transaction-scope-dispose()-bloc?source=docs – party_Rob Jan 19 '23 at 19:55

3 Answers3

2

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();
}
  • 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? – Scott Salyer Feb 11 '23 at 10:23
  • 1
    Yes, you need update 5.1 first. See: https://github.com/dotnet/SqlClient/pull/1801 – Alejandro Llermanos Feb 14 '23 at 10:36
  • 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? – Scott Salyer Feb 15 '23 at 11:33
  • 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. – Alejandro Llermanos Feb 16 '23 at 14:25
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

Bola
  • 718
  • 1
  • 6
  • 20
0

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;
            }
        }
    }
Scott Salyer
  • 2,165
  • 7
  • 45
  • 82