Skip to content

Commit 0d3753f

Browse files
committed
Support System.Transactions. Fixes #13
1 parent 42e17be commit 0d3753f

File tree

5 files changed

+392
-6
lines changed

5 files changed

+392
-6
lines changed

docs/content/tutorials/migrating-from-connector-net.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,16 @@ MySqlConnector has some different default connection string options:
3939
Some command line options that are supported in Connector/NET are not supported in MySqlConnector. For a full list of options that are
4040
supported in MySqlConnector, see the [Connection Options](connection-options)
4141

42+
### TransactionScope
43+
44+
MySqlConnector adds full distributed transaction support (for client code using [`TransactionScope`](https://msdn.microsoft.com/en-us/library/system.transactions.transactionscope.aspx)),
45+
while Connector/NET uses regular database transactions. As a result, code that uses `TransactionScope`
46+
may execute differently with MySqlConnector. To get Connector/NET-compatible behavior, remove
47+
`TransactionScope` and use `BeginTransaction`/`Commit` directly.
48+
4249
### Bugs present in Connector/NET that are fixed in MySqlConnector
4350

51+
* [#37283](https://bugs.mysql.com/bug.php?id=37283), [#70587](https://bugs.mysql.com/bug.php?id=70587): Distributed transactions are not supported
4452
* [#66476](https://bugs.mysql.com/bug.php?id=66476): Connection pool uses queue instead of stack
4553
* [#70111](https://bugs.mysql.com/bug.php?id=70111): `Async` methods execute synchronously
4654
* [#70686](https://bugs.mysql.com/bug.php?id=70686): `TIME(3)` and `TIME(6)` fields serialize milliseconds incorrectly

src/MySqlConnector/MySqlClient/MySqlConnection.cs

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ private async Task<MySqlTransaction> BeginDbTransactionAsync(IsolationLevel isol
3737
throw new InvalidOperationException("Connection is not open.");
3838
if (CurrentTransaction != null)
3939
throw new InvalidOperationException("Transactions may not be nested.");
40+
#if !NETSTANDARD1_3
41+
if (m_xaTransaction != null)
42+
throw new InvalidOperationException("Cannot begin a transaction when already enlisted in a transaction.");
43+
#endif
4044

4145
string isolationLevelValue;
4246
switch (isolationLevel)
@@ -76,8 +80,32 @@ private async Task<MySqlTransaction> BeginDbTransactionAsync(IsolationLevel isol
7680
#if !NETSTANDARD1_3
7781
public override void EnlistTransaction(System.Transactions.Transaction transaction)
7882
{
79-
throw new NotSupportedException("System.Transactions.Transaction is not supported. Use BeginTransaction instead.");
83+
if (m_xaTransaction != null)
84+
throw new MySqlException("Already enlisted in a Transaction.");
85+
if (CurrentTransaction != null)
86+
throw new InvalidOperationException("Can't enlist in a Transaction when there is an active MySqlTransaction.");
87+
88+
if (transaction != null)
89+
{
90+
m_xaTransaction = new MySqlXaTransaction(this);
91+
m_xaTransaction.Start(transaction);
92+
}
8093
}
94+
95+
internal void UnenlistTransaction(MySqlXaTransaction xaTransaction)
96+
{
97+
if (!object.ReferenceEquals(xaTransaction, m_xaTransaction))
98+
throw new InvalidOperationException("Active transaction is not the one being unenlisted from.");
99+
m_xaTransaction = null;
100+
101+
if (m_shouldCloseWhenUnenlisted)
102+
{
103+
m_shouldCloseWhenUnenlisted = false;
104+
Close();
105+
}
106+
}
107+
108+
MySqlXaTransaction m_xaTransaction;
81109
#endif
82110

83111
public override void Close() => DoClose();
@@ -111,10 +139,6 @@ private async Task OpenAsync(IOBehavior ioBehavior, CancellationToken cancellati
111139
VerifyNotDisposed();
112140
if (State != ConnectionState.Closed)
113141
throw new InvalidOperationException("Cannot Open when State is {0}.".FormatInvariant(State));
114-
#if !NETSTANDARD1_3
115-
if (System.Transactions.Transaction.Current != null)
116-
throw new NotSupportedException("Ambient transactions are not supported. Use BeginTransaction instead.");
117-
#endif
118142

119143
SetState(ConnectionState.Connecting);
120144

@@ -135,6 +159,11 @@ private async Task OpenAsync(IOBehavior ioBehavior, CancellationToken cancellati
135159
SetState(ConnectionState.Closed);
136160
throw new MySqlException("Unable to connect to any of the specified MySQL hosts.", ex);
137161
}
162+
163+
#if !NETSTANDARD1_3
164+
if (System.Transactions.Transaction.Current != null)
165+
EnlistTransaction(System.Transactions.Transaction.Current);
166+
#endif
138167
}
139168

140169
public override string ConnectionString
@@ -195,7 +224,7 @@ protected override void Dispose(bool disposing)
195224
}
196225
finally
197226
{
198-
m_isDisposed = true;
227+
m_isDisposed = !m_shouldCloseWhenUnenlisted;
199228
base.Dispose(disposing);
200229
}
201230
}
@@ -316,6 +345,19 @@ private void VerifyNotDisposed()
316345

317346
private void DoClose()
318347
{
348+
#if !NETSTANDARD1_3
349+
// If participating in a distributed transaction, keep the connection open so we can commit or rollback.
350+
// This handles the common pattern of disposing a connection before disposing a TransactionScope (e.g., nested using blocks)
351+
if (m_xaTransaction != null)
352+
{
353+
m_shouldCloseWhenUnenlisted = true;
354+
return;
355+
}
356+
#else
357+
// fix "field is never assigned" compiler error
358+
m_shouldCloseWhenUnenlisted = false;
359+
#endif
360+
319361
if (m_connectionState != ConnectionState.Closed)
320362
{
321363
try
@@ -355,6 +397,7 @@ private void CloseDatabase()
355397
ConnectionState m_connectionState;
356398
bool m_hasBeenOpened;
357399
bool m_isDisposed;
400+
bool m_shouldCloseWhenUnenlisted;
358401
Dictionary<string, CachedProcedure> m_cachedProcedures;
359402
}
360403
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#if !NETSTANDARD1_3
2+
using System;
3+
using System.Globalization;
4+
using System.Threading;
5+
using System.Transactions;
6+
7+
namespace MySql.Data.MySqlClient
8+
{
9+
internal sealed class MySqlXaTransaction : IEnlistmentNotification
10+
{
11+
public MySqlXaTransaction(MySqlConnection connection) => m_connection = connection;
12+
13+
public void Start(Transaction transaction)
14+
{
15+
// generate an "xid" with "gtrid" (Global TRansaction ID) from the .NET Transaction and "bqual" (Branch QUALifier)
16+
// unique to this object
17+
var id = Interlocked.Increment(ref s_currentId);
18+
m_xid = "'" + transaction.TransactionInformation.LocalIdentifier + "', '" + id.ToString(CultureInfo.InvariantCulture) + "'";
19+
20+
ExecuteXaCommand("START");
21+
22+
// TODO: Support EnlistDurable and enable recovery via "XA RECOVER"
23+
transaction.EnlistVolatile(this, EnlistmentOptions.None);
24+
}
25+
26+
public void Prepare(PreparingEnlistment enlistment)
27+
{
28+
ExecuteXaCommand("END");
29+
ExecuteXaCommand("PREPARE");
30+
enlistment.Prepared();
31+
}
32+
33+
public void Commit(Enlistment enlistment)
34+
{
35+
ExecuteXaCommand("COMMIT");
36+
enlistment.Done();
37+
m_connection.UnenlistTransaction(this);
38+
}
39+
40+
public void Rollback(Enlistment enlistment)
41+
{
42+
ExecuteXaCommand("END");
43+
ExecuteXaCommand("ROLLBACK");
44+
enlistment.Done();
45+
m_connection.UnenlistTransaction(this);
46+
}
47+
48+
public void InDoubt(Enlistment enlistment) => throw new NotSupportedException();
49+
50+
private void ExecuteXaCommand(string statement)
51+
{
52+
using (var cmd = m_connection.CreateCommand())
53+
{
54+
cmd.CommandText = "XA " + statement + " " + m_xid;
55+
cmd.ExecuteNonQuery();
56+
}
57+
}
58+
59+
static int s_currentId;
60+
61+
readonly MySqlConnection m_connection;
62+
string m_xid;
63+
}
64+
}
65+
#endif

tests/SideBySide/SideBySide.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747

4848
<ItemGroup Condition=" '$(TargetFramework)' == 'net462' ">
4949
<Reference Include="System" />
50+
<Reference Include="System.Transactions" />
5051
<Reference Include="Microsoft.CSharp" />
5152
</ItemGroup>
5253

0 commit comments

Comments
 (0)