1+ using HarmonyLib ;
2+ using System ;
3+ using System . Reflection ;
4+ using System . Runtime . InteropServices ;
5+ using System . Transactions ;
6+
7+ namespace Softwarehelden . Transactions . Oletx
8+ {
9+ /// <summary>
10+ /// The MSDTC patcher applies patches to the distributed transaction implementation in
11+ /// System.Transactions to make promotable transactions work with SQL servers in .NET 6.0. The
12+ /// patch would in theory also work for other data providers that support promotable single
13+ /// phase enlistment (PSPE) using the method Transaction.EnlistPromotableSinglePhase() where the
14+ /// database itself or an external service acts as a MSDTC superior transaction manager and can
15+ /// promote the internal transaction to a MSDTC transaction. The patch won't work when the data
16+ /// provider enlist a durable resource manager to participate in the transaction using
17+ /// Transaction.EnlistDurable() or Transaction.PromoteAndEnlistDurable(). However the data
18+ /// provider or the application can still enlist volatile resource managers using
19+ /// Transaction.EnlistVolatile() when data recovery is not required (or implemented) in case of
20+ /// a crash between the prepare and commit phase.
21+ /// </summary>
22+ /// <remarks>.NET issue: https://github.com/dotnet/runtime/issues/715</remarks>
23+ public static class OletxPatcher
24+ {
25+ /// <summary>
26+ /// The IL method patcher used to patch methods in System.Transactions.
27+ /// </summary>
28+ private static readonly Harmony MethodPatcher = new Harmony ( nameof ( OletxPatcher ) ) ;
29+
30+ /// <summary>
31+ /// A custom non-MSDTC promoter type for transaction enlistments.
32+ /// </summary>
33+ private static readonly Guid NonMsdtcPromoterType = new Guid ( "12ddadb4-dea6-4dea-a32a-51e8d9570b4e" ) ;
34+
35+ /// <summary>
36+ /// The transaction manager that communicates with the MSDTC services.
37+ /// </summary>
38+ private static readonly OletxTransactionManager TransactionManager = new OletxTransactionManager ( ) ;
39+
40+ /// <summary>
41+ /// Applies the patches to System.Transactions.
42+ /// </summary>
43+ public static void Patch ( )
44+ {
45+ // MSDTC is still required on the machine. Therefore the patch can only be applied on Windows.
46+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
47+ {
48+ var getExportCookieMethod = typeof ( TransactionInterop ) . GetMethod (
49+ nameof ( TransactionInterop . GetExportCookie ) ,
50+ BindingFlags . Static | BindingFlags . Public
51+ ) ;
52+
53+ var getNewExportCookieMethod = typeof ( Patches ) . GetMethod (
54+ nameof ( Patches . GetExportCookie ) ,
55+ BindingFlags . Static | BindingFlags . Public
56+ ) ;
57+
58+ var enlistPromotableSinglePhaseMethod = typeof ( Transaction ) . GetMethod (
59+ nameof ( Transaction . EnlistPromotableSinglePhase ) ,
60+ new Type [ ] { typeof ( IPromotableSinglePhaseNotification ) , typeof ( Guid ) }
61+ ) ;
62+
63+ var setNonMsdtcPromoterTypeMethod = typeof ( Patches ) . GetMethod (
64+ nameof ( Patches . SetNonMsdtcPromoterType ) ,
65+ BindingFlags . Static | BindingFlags . Public
66+ ) ;
67+
68+ MethodPatcher . Patch ( getExportCookieMethod , new HarmonyMethod ( getNewExportCookieMethod ) ) ;
69+ MethodPatcher . Patch ( enlistPromotableSinglePhaseMethod , new HarmonyMethod ( setNonMsdtcPromoterTypeMethod ) ) ;
70+ }
71+ }
72+
73+ /// <summary>
74+ /// Patches for System.Transactions.
75+ /// </summary>
76+ private static class Patches
77+ {
78+ /// <summary>
79+ /// Returns a transaction cookie that is used to propagate/import a distributed
80+ /// transaction on a SQL server that wants to participate in the transaction.
81+ /// </summary>
82+ public static bool GetExportCookie ( Transaction transaction , byte [ ] whereabouts , ref byte [ ] __result )
83+ {
84+ // Should the transaction be promoted using our custom promoter type?
85+ if ( transaction . PromoterType == NonMsdtcPromoterType )
86+ {
87+ __result = null ;
88+
89+ // Promote the transaction on the MSDTC that owns the transaction (source
90+ // MSDTC). System.Transactions calls only the Promote method of the promotable
91+ // single phase notification implemented by the ADO.NET provider and do not call
92+ // the MSDTC API directly which would result in a PlatformNotSupportedException
93+ // on .NET 6.
94+ byte [ ] propagationToken = transaction . GetPromotedToken ( ) ;
95+
96+ if ( propagationToken != null )
97+ {
98+ // The propagation token that the ADO.NET provider supplies is actually a
99+ // MSDTC propagation token despite the non-MSDTC promoter type.
100+
101+ // However on-premises SQL servers do not support MSDTC propagation tokens
102+ // but only transaction cookies for TDS propagate requests. So we must
103+ // create a transaction cookie now that the SQL server can understand and import.
104+
105+ // Note that Azure SQL supports sending the propagation token directly in
106+ // the TDS propagate request to make elastic transactions work natively in
107+ // .NET 6 without querying the MSDTC API.
108+
109+ // Pull the promoted transaction from the source MSDTC to the local MSDTC
110+ // (pull propagation)
111+ var transactionShim = TransactionManager . ReceiveTransaction ( propagationToken ) ;
112+
113+ // Now push the transaction from the local MSDTC to the target MSDTC
114+ // specified in the whereabouts (push propagation)
115+ __result = TransactionManager . GetExportCookie ( transactionShim , whereabouts ) ;
116+ }
117+
118+ // We got an export cookie, so do not call the original method
119+ return false ;
120+ }
121+ else
122+ {
123+ // Call the original method since we cannot handle the promoter type of the transaction
124+ return true ;
125+ }
126+ }
127+
128+ /// <summary>
129+ /// Sets a non-MSDTC promoter type for the method <see
130+ /// cref="Transaction.EnlistPromotableSinglePhase(IPromotableSinglePhaseNotification, Guid)"/>.
131+ /// </summary>
132+ public static void SetNonMsdtcPromoterType ( ref Guid promoterType )
133+ {
134+ // Set a custom promoter type for all MSDTC promotable transactions. If the promoter
135+ // type is not MSDTC then the transaction state machine in System.Transactions
136+ // cannot assume that the propagation token is a MSDTC propagation token. Therefore
137+ // the framework also does not attempt to create a distributed transaction and no
138+ // PlatformNotSupportedException will be thrown when the transaction is being
139+ // promoted. Elastic database transactions in Azure SQL work basically the same way.
140+ if ( promoterType == TransactionInterop . PromoterTypeDtc )
141+ {
142+ promoterType = NonMsdtcPromoterType ;
143+ }
144+ }
145+ }
146+ }
147+ }
0 commit comments