Skip to content

Commit 1cf9dfc

Browse files
committed
Initial commit
0 parents  commit 1cf9dfc

21 files changed

+1008
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
bin/
2+
obj/
3+
.vs

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2022 Softwarehelden GmbH & Co. KG
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

NativeMethods.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
using System.Threading;
4+
5+
namespace Softwarehelden.Transactions.Oletx
6+
{
7+
/// <summary>
8+
/// Provides native methods via P/Invoke.
9+
/// </summary>
10+
internal static class NativeMethods
11+
{
12+
/// <summary>
13+
/// https://referencesource.microsoft.com/#System.Transactions/System/Transactions/Oletx/DtcInterfaces.cs,14
14+
/// </summary>
15+
private const string MethodName = "GetNotificationFactory";
16+
17+
private delegate int GetNotificationFactoryDelegate(SafeHandle notificationEventHandle, out IDtcProxyShimFactory ppProxyShimFactory);
18+
19+
/// <summary>
20+
/// Gets the notification factory used to connect to the native MSDTC proxy.
21+
/// </summary>
22+
/// <remarks>
23+
/// This method calls a native method via P/Invoke from the .NET framework assembly that
24+
/// must be installed on the machine.
25+
/// </remarks>
26+
internal static IDtcProxyShimFactory GetNotificationFactory(string path)
27+
{
28+
var module = LoadLibrary(path);
29+
30+
if (module == IntPtr.Zero)
31+
{
32+
throw new Exception($"Failed to load the .NET framework assembly '{path}': {Marshal.GetLastWin32Error()}");
33+
}
34+
35+
var procAddress = GetProcAddress(module, MethodName);
36+
37+
if (procAddress == IntPtr.Zero)
38+
{
39+
FreeLibrary(module);
40+
41+
throw new Exception($"Failed to load the native method '{MethodName}' from .NET framework assembly '{path}': {Marshal.GetLastWin32Error()}");
42+
}
43+
44+
var getNotificationFactoryDelegate = (GetNotificationFactoryDelegate)Marshal.GetDelegateForFunctionPointer(
45+
procAddress,
46+
typeof(GetNotificationFactoryDelegate)
47+
);
48+
49+
using (var shimWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset))
50+
{
51+
_ = getNotificationFactoryDelegate(shimWaitHandle.SafeWaitHandle, out var result);
52+
53+
return result;
54+
}
55+
}
56+
57+
[DllImport("kernel32.dll", SetLastError = true)]
58+
private static extern void FreeLibrary(IntPtr module);
59+
60+
[DllImport("kernel32.dll", SetLastError = true)]
61+
private static extern IntPtr GetProcAddress(IntPtr module, string proc);
62+
63+
[DllImport("kernel32.dll", SetLastError = true)]
64+
private static extern IntPtr LoadLibrary(string module);
65+
}
66+
}

OletxPatcher.cs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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

Comments
 (0)