diff --git a/src/libraries/System.Transactions.Local/ref/System.Transactions.Local.cs b/src/libraries/System.Transactions.Local/ref/System.Transactions.Local.cs
index 2f08b176bc27b..99e82f525ee68 100644
--- a/src/libraries/System.Transactions.Local/ref/System.Transactions.Local.cs
+++ b/src/libraries/System.Transactions.Local/ref/System.Transactions.Local.cs
@@ -4,6 +4,8 @@
// Changes to this file must follow the https://aka.ms/api-review process.
// ------------------------------------------------------------------------------
+using System.Runtime.Versioning;
+
namespace System.Transactions
{
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
@@ -191,6 +193,7 @@ public static partial class TransactionManager
[System.Diagnostics.CodeAnalysis.DisallowNullAttribute]
public static System.Transactions.HostCurrentTransactionCallback? HostCurrentCallback { get { throw null; } set { } }
public static System.TimeSpan MaximumTimeout { get { throw null; } set { } }
+ public static bool ImplicitDistributedTransactions { get; [System.Runtime.Versioning.SupportedOSPlatform("windows")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Distributed transactions support may not be compatible with trimming. If your program creates a distributed transaction via System.Transactions, the correctness of the application cannot be guaranteed after trimming.")] set; }
public static event System.Transactions.TransactionStartedEventHandler? DistributedTransactionStarted { add { } remove { } }
public static void RecoveryComplete(System.Guid resourceManagerIdentifier) { }
public static System.Transactions.Enlistment Reenlist(System.Guid resourceManagerIdentifier, byte[] recoveryInformation, System.Transactions.IEnlistmentNotification enlistmentNotification) { throw null; }
diff --git a/src/libraries/System.Transactions.Local/src/ILLink/ILLink.Suppressions.LibraryBuild.xml b/src/libraries/System.Transactions.Local/src/ILLink/ILLink.Suppressions.LibraryBuild.xml
deleted file mode 100644
index d002d008a2ac6..0000000000000
--- a/src/libraries/System.Transactions.Local/src/ILLink/ILLink.Suppressions.LibraryBuild.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- ILLink
- IL2026
- member
- M:System.Transactions.DtcProxyShim.DtcProxyShimFactory.ConnectToProxyCore(System.String,System.Guid,System.Object,System.Boolean@,System.Byte[]@,System.Transactions.DtcProxyShim.ResourceManagerShim@)
- This warning is left in the product so developers get an ILLink warning when trimming an app using this transaction support
-
-
-
diff --git a/src/libraries/System.Transactions.Local/src/Resources/Strings.resx b/src/libraries/System.Transactions.Local/src/Resources/Strings.resx
index 7e75fee595f9a..647187affad81 100644
--- a/src/libraries/System.Transactions.Local/src/Resources/Strings.resx
+++ b/src/libraries/System.Transactions.Local/src/Resources/Strings.resx
@@ -423,4 +423,10 @@
Distributed transactions are currently unsupported in 32-bit processes.
-
+
+ Implicit distributed transactions have not been enabled. If you're intentionally starting a distributed transaction, set TransactionManager.ImplicitDistributedTransactions to true.
+
+
+ TransactionManager.ImplicitDistributedTransaction cannot be changed once set, or once System.Transactions distributed transactions have been initialized. Set this flag once at the start of your program.
+
+
\ No newline at end of file
diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcProxyShimFactory.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcProxyShimFactory.cs
index 35454d05adae4..2ec9b30f49812 100644
--- a/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcProxyShimFactory.cs
+++ b/src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcProxyShimFactory.cs
@@ -21,6 +21,11 @@ internal sealed class DtcProxyShimFactory
// at the same time.
private static readonly object _proxyInitLock = new();
+ // This object will perform the actual distributed transaction connection.
+ // It will be set only if TransactionManager.ImplicitDefaultTransactions
+ // is set to true, allowing the relevant code to be trimmed otherwise.
+ internal static ITransactionConnector? s_transactionConnector;
+
// Lock to protect access to listOfNotifications.
private readonly object _notificationLock = new();
@@ -41,6 +46,7 @@ internal DtcProxyShimFactory(EventWaitHandle notificationEventHandle)
// https://docs.microsoft.com/previous-versions/windows/desktop/ms678898(v=vs.85)
[DllImport(Interop.Libraries.Xolehlp, CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)]
+ [RequiresUnreferencedCode(TransactionManager.DistributedTransactionTrimmingWarning)]
private static extern void DtcGetTransactionManagerExW(
[MarshalAs(UnmanagedType.LPWStr)] string? pszHost,
[MarshalAs(UnmanagedType.LPWStr)] string? pszTmName,
@@ -49,7 +55,7 @@ private static extern void DtcGetTransactionManagerExW(
object? pvConfigPararms,
[MarshalAs(UnmanagedType.Interface)] out ITransactionDispenser ppvObject);
- [RequiresUnreferencedCode("Distributed transactions support may not be compatible with trimming. If your program creates a distributed transaction via System.Transactions, the correctness of the application cannot be guaranteed after trimming.")]
+ [RequiresUnreferencedCode(TransactionManager.DistributedTransactionTrimmingWarning)]
private static void DtcGetTransactionManager(string? nodeName, out ITransactionDispenser localDispenser) =>
DtcGetTransactionManagerExW(nodeName, null, Guids.IID_ITransactionDispenser_Guid, 0, null, out localDispenser);
@@ -61,15 +67,27 @@ public void ConnectToProxy(
out byte[] whereabouts,
out ResourceManagerShim resourceManagerShim)
{
- switch (RuntimeInformation.ProcessArchitecture)
+ if (RuntimeInformation.ProcessArchitecture == Architecture.X86)
+ {
+ throw new PlatformNotSupportedException(SR.DistributedNotSupportedOn32Bits);
+ }
+
+ lock (TransactionManager.s_implicitDistributedTransactionsLock)
{
- case Architecture.X86:
- throw new PlatformNotSupportedException(SR.DistributedNotSupportedOn32Bits);
+ if (s_transactionConnector is null)
+ {
+ // We set TransactionManager.ImplicitDistributedTransactionsInternal, so that any attempt to change it
+ // later will cause an exception.
+ TransactionManager.s_implicitDistributedTransactions = false;
+
+ throw new NotSupportedException(SR.ImplicitDistributedTransactionsDisabled);
+ }
}
- ConnectToProxyCore(nodeName, resourceManagerIdentifier, managedIdentifier, out nodeNameMatches, out whereabouts, out resourceManagerShim);
+ s_transactionConnector.ConnectToProxyCore(this, nodeName, resourceManagerIdentifier, managedIdentifier, out nodeNameMatches, out whereabouts, out resourceManagerShim);
}
+ [RequiresUnreferencedCode(TransactionManager.DistributedTransactionTrimmingWarning)]
private void ConnectToProxyCore(
string? nodeName,
Guid resourceManagerIdentifier,
@@ -80,9 +98,7 @@ private void ConnectToProxyCore(
{
lock (_proxyInitLock)
{
-#pragma warning disable IL2026 // This warning is left in the product so developers get an ILLink warning when trimming an app using this transaction support
DtcGetTransactionManager(nodeName, out ITransactionDispenser? localDispenser);
-#pragma warning restore IL2026
// Check to make sure the node name matches.
if (nodeName is not null)
@@ -353,6 +369,8 @@ internal ITransactionTransmitter GetCachedTransmitter(ITransaction transaction)
internal void ReturnCachedTransmitter(ITransactionTransmitter transmitter)
{
+ // Note that due to race conditions, we may end up enqueuing above s_maxCachedInterfaces.
+ // This is benign, as this is only a best-effort cache, and there are no negative consequences.
if (_cachedTransmitters.Count < s_maxCachedInterfaces)
{
transmitter.Reset();
@@ -375,10 +393,46 @@ internal ITransactionReceiver GetCachedReceiver()
internal void ReturnCachedReceiver(ITransactionReceiver receiver)
{
+ // Note that due to race conditions, we may end up enqueuing above s_maxCachedInterfaces.
+ // This is benign, as this is only a best-effort cache, and there are no negative consequences.
if (_cachedReceivers.Count < s_maxCachedInterfaces)
{
receiver.Reset();
_cachedReceivers.Enqueue(receiver);
}
}
+
+ internal interface ITransactionConnector
+ {
+ void ConnectToProxyCore(
+ DtcProxyShimFactory proxyShimFactory,
+ string? nodeName,
+ Guid resourceManagerIdentifier,
+ object managedIdentifier,
+ out bool nodeNameMatches,
+ out byte[] whereabouts,
+ out ResourceManagerShim resourceManagerShim);
+ }
+
+ [RequiresUnreferencedCode(TransactionManager.DistributedTransactionTrimmingWarning)]
+ internal sealed class DtcTransactionConnector : ITransactionConnector
+ {
+ public void ConnectToProxyCore(
+ DtcProxyShimFactory proxyShimFactory,
+ string? nodeName,
+ Guid resourceManagerIdentifier,
+ object managedIdentifier,
+ out bool nodeNameMatches,
+ out byte[] whereabouts,
+ out ResourceManagerShim resourceManagerShim)
+ {
+ proxyShimFactory.ConnectToProxyCore(
+ nodeName,
+ resourceManagerIdentifier,
+ managedIdentifier,
+ out nodeNameMatches,
+ out whereabouts,
+ out resourceManagerShim);
+ }
+ }
}
diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs
index 7ce21c51bad23..93a6fefadb88b 100644
--- a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs
+++ b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs
@@ -4,8 +4,12 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.IO;
+using System.Runtime.Versioning;
using System.Threading;
using System.Transactions.Configuration;
+#if WINDOWS
+using System.Transactions.DtcProxyShim;
+#endif
using System.Transactions.Oletx;
namespace System.Transactions
@@ -29,6 +33,10 @@ public static class TransactionManager
private static TransactionTable? s_transactionTable;
private static TransactionStartedEventHandler? s_distributedTransactionStartedDelegate;
+
+ internal const string DistributedTransactionTrimmingWarning =
+ "Distributed transactions support may not be compatible with trimming. If your program creates a distributed transaction via System.Transactions, the correctness of the application cannot be guaranteed after trimming.";
+
public static event TransactionStartedEventHandler? DistributedTransactionStarted
{
add
@@ -391,6 +399,60 @@ public static TimeSpan MaximumTimeout
}
}
+ ///
+ /// Controls whether usage of System.Transactions APIs that require escalation to a distributed transaction will do so;
+ /// if your application requires distributed transaction, opt into using them by setting this to .
+ /// If set to (the default), escalation to a distributed transaction will throw a .
+ ///
+#if WINDOWS
+ public static bool ImplicitDistributedTransactions
+ {
+ get => DtcProxyShimFactory.s_transactionConnector is not null;
+
+ [SupportedOSPlatform("windows")]
+ [RequiresUnreferencedCode(DistributedTransactionTrimmingWarning)]
+ set
+ {
+ lock (s_implicitDistributedTransactionsLock)
+ {
+ // Make sure this flag can only be set once, and that once distributed transactions have been initialized,
+ // it's frozen.
+ if (s_implicitDistributedTransactions is null)
+ {
+ s_implicitDistributedTransactions = value;
+
+ if (value)
+ {
+ DtcProxyShimFactory.s_transactionConnector ??= new DtcProxyShimFactory.DtcTransactionConnector();
+ }
+ }
+ else if (value != s_implicitDistributedTransactions)
+ {
+ throw new InvalidOperationException(SR.ImplicitDistributedTransactionsCannotBeChanged);
+ }
+ }
+ }
+ }
+
+ internal static bool? s_implicitDistributedTransactions;
+ internal static object s_implicitDistributedTransactionsLock = new();
+#else
+ public static bool ImplicitDistributedTransactions
+ {
+ get => false;
+
+ [SupportedOSPlatform("windows")]
+ [RequiresUnreferencedCode(DistributedTransactionTrimmingWarning)]
+ set
+ {
+ if (value)
+ {
+ throw new PlatformNotSupportedException(SR.DistributedNotSupported);
+ }
+ }
+ }
+#endif
+
// This routine writes the "header" for the recovery information, based on the
// type of the calling object and its provided parameter collection. This information
// we be read back by the static Reenlist method to create the necessary transaction
diff --git a/src/libraries/System.Transactions.Local/tests/OleTxTests.cs b/src/libraries/System.Transactions.Local/tests/OleTxTests.cs
index 739ca93afeb2d..0d52469c696d8 100644
--- a/src/libraries/System.Transactions.Local/tests/OleTxTests.cs
+++ b/src/libraries/System.Transactions.Local/tests/OleTxTests.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.DotNet.RemoteExecutor;
@@ -492,6 +493,80 @@ public void GetDtcTransaction()
Retry(() => Assert.Equal(TransactionStatus.Committed, tx.TransactionInformation.Status));
});
+ [ConditionalFact(nameof(IsRemoteExecutorSupportedAndNotNano))]
+ public void Distributed_transactions_require_ImplicitDistributedTransactions_true()
+ {
+ // Temporarily skip on 32-bit where we have an issue.
+ if (!Environment.Is64BitProcess)
+ {
+ return;
+ }
+
+ using var _ = RemoteExecutor.Invoke(() =>
+ {
+ Assert.False(TransactionManager.ImplicitDistributedTransactions);
+
+ using var tx = new CommittableTransaction();
+
+ Assert.Throws(MinimalOleTxScenario);
+ });
+ }
+
+ [ConditionalFact(nameof(IsRemoteExecutorSupportedAndNotNano))]
+ public void ImplicitDistributedTransactions_cannot_be_changed_after_being_set()
+ {
+ // Temporarily skip on 32-bit where we have an issue.
+ if (!Environment.Is64BitProcess)
+ {
+ return;
+ }
+
+ using var _ = RemoteExecutor.Invoke(() =>
+ {
+ TransactionManager.ImplicitDistributedTransactions = true;
+
+ Assert.Throws(() => TransactionManager.ImplicitDistributedTransactions = false);
+ });
+ }
+
+ [ConditionalFact(nameof(IsRemoteExecutorSupportedAndNotNano))]
+ public void ImplicitDistributedTransactions_cannot_be_changed_after_being_read_as_true()
+ {
+ // Temporarily skip on 32-bit where we have an issue.
+ if (!Environment.Is64BitProcess)
+ {
+ return;
+ }
+
+ using var _ = RemoteExecutor.Invoke(() =>
+ {
+ TransactionManager.ImplicitDistributedTransactions = true;
+
+ MinimalOleTxScenario();
+
+ Assert.Throws(() => TransactionManager.ImplicitDistributedTransactions = false);
+ TransactionManager.ImplicitDistributedTransactions = true;
+ });
+ }
+
+ [ConditionalFact(nameof(IsRemoteExecutorSupportedAndNotNano))]
+ public void ImplicitDistributedTransactions_cannot_be_changed_after_being_read_as_false()
+ {
+ // Temporarily skip on 32-bit where we have an issue.
+ if (!Environment.Is64BitProcess)
+ {
+ return;
+ }
+
+ using var _ = RemoteExecutor.Invoke(() =>
+ {
+ Assert.Throws(MinimalOleTxScenario);
+
+ Assert.Throws(() => TransactionManager.ImplicitDistributedTransactions = true);
+ TransactionManager.ImplicitDistributedTransactions = false;
+ });
+ }
+
private static void Test(Action action)
{
// Temporarily skip on 32-bit where we have an issue.
@@ -500,6 +575,8 @@ private static void Test(Action action)
return;
}
+ TransactionManager.ImplicitDistributedTransactions = true;
+
// In CI, we sometimes get XACT_E_TMNOTAVAILABLE; when it happens, it's typically on the very first
// attempt to connect to MSDTC (flaky/slow on-demand startup of MSDTC), though not only.
// This catches that error and retries.
@@ -549,23 +626,25 @@ private static void Retry(Action action)
}
}
+ static void MinimalOleTxScenario()
+ {
+ using var tx = new CommittableTransaction();
+
+ var enlistment1 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed);
+ var enlistment2 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed);
+
+ tx.EnlistDurable(Guid.NewGuid(), enlistment1, EnlistmentOptions.None);
+ tx.EnlistDurable(Guid.NewGuid(), enlistment2, EnlistmentOptions.None);
+
+ tx.Commit();
+ }
+
public class OleTxFixture
{
// In CI, we sometimes get XACT_E_TMNOTAVAILABLE on the very first attempt to connect to MSDTC;
// this is likely due to on-demand slow startup of MSDTC. Perform pre-test connecting with retry
// to ensure that MSDTC is properly up when the first test runs.
public OleTxFixture()
- => Test(() =>
- {
- using var tx = new CommittableTransaction();
-
- var enlistment1 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed);
- var enlistment2 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed);
-
- tx.EnlistDurable(Guid.NewGuid(), enlistment1, EnlistmentOptions.None);
- tx.EnlistDurable(Guid.NewGuid(), enlistment2, EnlistmentOptions.None);
-
- tx.Commit();
- });
+ => Test(MinimalOleTxScenario);
}
}