5

Consider the following small program that simply creates a TransactionScope, prints Transaction.Current, calls a method in another AppDomain (that takes a while to execute) and then prints Transaction.Current upon return.

using System;

using System.Linq;
using System.Runtime.Remoting.Lifetime;
using System.Threading;
using System.Transactions;

namespace TransactionScopeFlowTest
{
   class Program
   {
      static void Main(string[] args)
      {
         // These times are just to generate the error faster. Normally the initial lease is 5 minutes, meaning the method call
         // would have to take 5 minutes to occur, so we speed it up here for demonstration purposes.
         LifetimeServices.LeaseManagerPollTime = TimeSpan.FromSeconds(1);
         LifetimeServices.LeaseTime = TimeSpan.FromSeconds(1);
         LifetimeServices.RenewOnCallTime = TimeSpan.FromSeconds(1);

         AppDomain domain = AppDomain.CreateDomain("Temp", null, AppDomain.CurrentDomain.SetupInformation);

         using (TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
         {
            Console.WriteLine($"Transaction Before Call = {Transaction.Current?.TransactionInformation?.LocalIdentifier?.ToString() ?? "<null>"}");
            domain.DoCallBack(AppDomainCallback);
            Console.WriteLine($"Transaction After Call = {Transaction.Current?.TransactionInformation?.LocalIdentifier?.ToString() ?? "<null>"}");
            scope.Complete();
         }

         AppDomain.Unload(domain);
      }

      public static void AppDomainCallback()
      {
         Thread.Sleep(3000);
      }
   }
}

Quite unexpectedly, the program generates the following output:

Transaction Before Call = 1f980219-2583-4796-8d6d-256a6f100698:1
Transaction After Call = <null>

If I change the TransactionScopeAsyncFlowOption in the ctor of TransactionScope to TransactionScopeAsyncFlowOption.Suppress, then the Transaction remains after the call.

My suspicion is that the transaction scope flow across the logical context is handled by CallContext which is propagated across the remoting call, and the keys used inherits MarshalByRefObject and since they are not registered with any ISponsor, the proxy will disconnect after the initial lease time. And then the logical call context is merged back with the original once we return from the call, meaning that the transaction no longer exists.

I'm looking for a way to circumvent this problem, and also if this is to be considered a bug in .NET?

DeCaf
  • 6,026
  • 1
  • 29
  • 51
  • You said it all. Reference Source shows that when you want AsyncFlow, transaction state is using a MarshalByRefObject ContextKey class: https://referencesource.microsoft.com/#System.Transactions/System/Transactions/Transaction.cs,20878e4ffc8a417f Thing is, transactions do *not* flow accross AppDomains by design : https://referencesource.microsoft.com/#System.Transactions/System/Transactions/Transaction.cs,a538de61b60d1252,references "For async flow scenarios, we should not allow flowing of transaction across app domains". – Simon Mourier Oct 08 '17 at 09:49
  • There is no "merge back". The key that was stored as a weak reference is just gone, so it's like the transaction never existed. Not sure it's a bug. I don't see an easy solution but report to Microsoft (or stop doing fancy things with that poor TransactionScope that was designed long time ago, before even .NET existed :-) – Simon Mourier Oct 08 '17 at 09:52
  • I know the transaction does not flow across the call, and I'm not using the transaction in the other AppDomain. The strange thing is that the transaction disappears in the original AppDomain after making the call. – DeCaf Oct 08 '17 at 10:04
  • Since the ContextKey instance is supposed to be the same across domains, once it's killed in the Temp domain because of the lease time, it's also dead for other domains. And because it was initialy stored in a weak reference dictionary: https://referencesource.microsoft.com/#System.Transactions/System/Transactions/Transaction.cs,8f4357758cfad57b for the initial domain, it's like it had never existed – Simon Mourier Oct 08 '17 at 11:34
  • The issue has been reported at https://github.com/Microsoft/dotnet-framework-early-access/issues/7 – DeCaf Oct 13 '17 at 07:13

0 Answers0