Skip to content

Commit 14329b6

Browse files
committed
Add UseXaTransactions option. Fixes #254
1 parent 2b1bac6 commit 14329b6

File tree

10 files changed

+288
-98
lines changed

10 files changed

+288
-98
lines changed

docs/content/connection-options.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,4 +343,12 @@ These are the other options that MySqlConnector supports. They are set to sensib
343343
<td>true</td>
344344
<td>When false, the connection reports found rows instead of changed (affected) rows.</td>
345345
</tr>
346+
<tr>
347+
<td>Use XA Transactions, UseXaTransactions</td>
348+
<td>true</td>
349+
<td>When <code>true</code> (default), using <code>TransactionScope</code> or <code>MySqlConnection.EnlistTransaction</code>
350+
will use a <a href="https://dev.mysql.com/doc/refman/8.0/en/xa.html">XA Transaction</a>. This allows true
351+
distributed transactions, but may not be compatible with server replication; there are <a href="https://dev.mysql.com/doc/refman/8.0/en/xa-restrictions.html">other limitations</a>.
352+
When set to <code>false</code>, regular MySQL transactions are used, just like Connector/NET.</td>
353+
</tr>
346354
</table>

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,11 @@ supported in MySqlConnector, see the [Connection Options](connection-options).
6666

6767
### TransactionScope
6868

69-
MySqlConnector adds full distributed transaction support (for client code using [`TransactionScope`](https://msdn.microsoft.com/en-us/library/system.transactions.transactionscope.aspx)),
70-
while Connector/NET uses regular database transactions. As a result, code that uses `TransactionScope`
71-
may execute differently with MySqlConnector. To get Connector/NET-compatible behavior, remove
72-
`TransactionScope` and use `BeginTransaction`/`Commit` directly.
69+
MySqlConnector adds full distributed transaction support (for client code using [`System.Transactions.Transaction`](https://docs.microsoft.com/en-us/dotnet/api/system.transactions.transaction
70+
)),
71+
while Connector/NET uses regular database transactions. As a result, code that uses `TransactionScope` or `MySqlConnection.EnlistTransaction`
72+
may execute differently with MySqlConnector. To get Connector/NET-compatible behavior, set
73+
`UseXaTransactions=false` in your connection string.
7374

7475
### MySqlConnection
7576

src/MySqlConnector/Core/ConnectionSettings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
8383
TreatTinyAsBoolean = csb.TreatTinyAsBoolean;
8484
UseAffectedRows = csb.UseAffectedRows;
8585
UseCompression = csb.UseCompression;
86+
UseXaTransactions = csb.UseXaTransactions;
8687
}
8788

8889
private static MySqlGuidFormat GetEffectiveGuidFormat(MySqlGuidFormat guidFormat, bool oldGuids)
@@ -162,6 +163,7 @@ private static MySqlGuidFormat GetEffectiveGuidFormat(MySqlGuidFormat guidFormat
162163
public bool TreatTinyAsBoolean { get; }
163164
public bool UseAffectedRows { get; }
164165
public bool UseCompression { get; }
166+
public bool UseXaTransactions { get; }
165167

166168
public byte[] ConnectionAttributes { get; set; }
167169

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#if !NETSTANDARD1_3
2+
using System;
3+
using System.Transactions;
4+
using MySql.Data.MySqlClient;
5+
6+
namespace MySqlConnector.Core
7+
{
8+
internal abstract class ImplicitTransactionBase : IEnlistmentNotification
9+
{
10+
public MySqlConnection Connection { get; }
11+
12+
public void Start(Transaction transaction)
13+
{
14+
Transaction = transaction;
15+
OnStart();
16+
Transaction.EnlistVolatile(this, EnlistmentOptions.None);
17+
}
18+
19+
void IEnlistmentNotification.Prepare(PreparingEnlistment enlistment)
20+
{
21+
OnPrepare(enlistment);
22+
enlistment.Prepared();
23+
}
24+
25+
void IEnlistmentNotification.Commit(Enlistment enlistment)
26+
{
27+
OnCommit(enlistment);
28+
enlistment.Done();
29+
Connection.UnenlistTransaction(this, Transaction);
30+
Transaction = null;
31+
}
32+
33+
void IEnlistmentNotification.Rollback(Enlistment enlistment)
34+
{
35+
OnRollback(enlistment);
36+
enlistment.Done();
37+
Connection.UnenlistTransaction(this, Transaction);
38+
Transaction = null;
39+
}
40+
41+
public void InDoubt(Enlistment enlistment) => throw new NotImplementedException();
42+
43+
protected ImplicitTransactionBase(MySqlConnection connection) => Connection = connection;
44+
45+
protected Transaction Transaction { get; private set; }
46+
47+
protected abstract void OnStart();
48+
protected abstract void OnPrepare(PreparingEnlistment enlistment);
49+
protected abstract void OnCommit(Enlistment enlistment);
50+
protected abstract void OnRollback(Enlistment enlistment);
51+
}
52+
}
53+
#endif
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#if !NETSTANDARD1_3
2+
using System.Transactions;
3+
using MySql.Data.MySqlClient;
4+
5+
namespace MySqlConnector.Core
6+
{
7+
internal sealed class StandardImplicitTransaction : ImplicitTransactionBase
8+
{
9+
public StandardImplicitTransaction(MySqlConnection connection) : base(connection)
10+
{
11+
}
12+
13+
protected override void OnStart()
14+
{
15+
m_transaction = Connection.BeginTransaction();
16+
}
17+
18+
protected override void OnPrepare(PreparingEnlistment enlistment)
19+
{
20+
}
21+
22+
protected override void OnCommit(Enlistment enlistment)
23+
{
24+
m_transaction.Commit();
25+
m_transaction = null;
26+
}
27+
28+
protected override void OnRollback(Enlistment enlistment)
29+
{
30+
m_transaction.Rollback();
31+
m_transaction = null;
32+
}
33+
34+
MySqlTransaction m_transaction;
35+
}
36+
}
37+
#endif

src/MySqlConnector/Core/MySqlXaTransaction.cs renamed to src/MySqlConnector/Core/XaImplicitTransaction.cs

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,47 @@
11
#if !NETSTANDARD1_3
2-
using System;
32
using System.Globalization;
43
using System.Threading;
54
using System.Transactions;
65
using MySql.Data.MySqlClient;
76

87
namespace MySqlConnector.Core
98
{
10-
internal sealed class MySqlXaTransaction : IEnlistmentNotification
9+
internal sealed class XaImplicitTransaction : ImplicitTransactionBase
1110
{
12-
public MySqlXaTransaction(MySqlConnection connection) => Connection = connection;
13-
14-
public MySqlConnection Connection { get; }
15-
16-
public void Start(Transaction transaction)
11+
public XaImplicitTransaction(MySqlConnection connection)
12+
: base(connection)
1713
{
18-
m_transaction = transaction;
14+
}
1915

16+
protected override void OnStart()
17+
{
2018
// generate an "xid" with "gtrid" (Global TRansaction ID) from the .NET Transaction and "bqual" (Branch QUALifier)
2119
// unique to this object
2220
var id = Interlocked.Increment(ref s_currentId);
23-
m_xid = "'" + transaction.TransactionInformation.LocalIdentifier + "', '" + id.ToString(CultureInfo.InvariantCulture) + "'";
21+
m_xid = "'" + Transaction.TransactionInformation.LocalIdentifier + "', '" + id.ToString(CultureInfo.InvariantCulture) + "'";
2422

2523
ExecuteXaCommand("START");
2624

2725
// TODO: Support EnlistDurable and enable recovery via "XA RECOVER"
28-
transaction.EnlistVolatile(this, EnlistmentOptions.None);
2926
}
3027

31-
public void Prepare(PreparingEnlistment enlistment)
28+
protected override void OnPrepare(PreparingEnlistment enlistment)
3229
{
3330
ExecuteXaCommand("END");
3431
ExecuteXaCommand("PREPARE");
35-
enlistment.Prepared();
3632
}
3733

38-
public void Commit(Enlistment enlistment)
34+
protected override void OnCommit(Enlistment enlistment)
3935
{
4036
ExecuteXaCommand("COMMIT");
41-
enlistment.Done();
42-
Connection.UnenlistTransaction(this, m_transaction);
43-
m_transaction = null;
4437
}
4538

46-
public void Rollback(Enlistment enlistment)
39+
protected override void OnRollback(Enlistment enlistment)
4740
{
4841
ExecuteXaCommand("END");
4942
ExecuteXaCommand("ROLLBACK");
50-
enlistment.Done();
51-
Connection.UnenlistTransaction(this, m_transaction);
52-
m_transaction = null;
5343
}
5444

55-
public void InDoubt(Enlistment enlistment) => throw new NotSupportedException();
56-
5745
private void ExecuteXaCommand(string statement)
5846
{
5947
using (var cmd = Connection.CreateCommand())
@@ -65,7 +53,6 @@ private void ExecuteXaCommand(string statement)
6553

6654
static int s_currentId;
6755

68-
Transaction m_transaction;
6956
string m_xid;
7057
}
7158
}

src/MySqlConnector/MySql.Data.MySqlClient/MySqlConnection.cs

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ private async Task<MySqlTransaction> BeginDbTransactionAsync(IsolationLevel isol
4343
if (CurrentTransaction != null)
4444
throw new InvalidOperationException("Transactions may not be nested.");
4545
#if !NETSTANDARD1_3
46-
if (m_xaTransaction != null)
46+
if (m_implicitTransaction != null)
4747
throw new InvalidOperationException("Cannot begin a transaction when already enlisted in a transaction.");
4848
#endif
4949

@@ -85,7 +85,7 @@ private async Task<MySqlTransaction> BeginDbTransactionAsync(IsolationLevel isol
8585
#if !NETSTANDARD1_3
8686
public override void EnlistTransaction(System.Transactions.Transaction transaction)
8787
{
88-
if (m_xaTransaction != null)
88+
if (m_implicitTransaction != null)
8989
throw new MySqlException("Already enlisted in a Transaction.");
9090
if (CurrentTransaction != null)
9191
throw new InvalidOperationException("Can't enlist in a Transaction when there is an active MySqlTransaction.");
@@ -101,13 +101,24 @@ public override void EnlistTransaction(System.Transactions.Transaction transacti
101101
// can reuse the existing connection
102102
DoClose(changeState: false);
103103
m_session = existingConnection.DetachSession();
104-
m_xaTransaction = existingConnection.m_xaTransaction;
104+
m_implicitTransaction = existingConnection.m_implicitTransaction;
105105
}
106106
else
107107
{
108-
var xaTransaction = new MySqlXaTransaction(this);
109-
xaTransaction.Start(transaction);
110-
m_xaTransaction = xaTransaction;
108+
ImplicitTransactionBase implicitTransaction;
109+
if (m_connectionSettings.UseXaTransactions)
110+
{
111+
implicitTransaction = new XaImplicitTransaction(this);
112+
}
113+
else
114+
{
115+
if (existingConnection != null)
116+
throw new NotSupportedException("Multiple simultaneous connections or connections with different connection strings inside the same transaction are not supported when UseXaTransactions=False.");
117+
implicitTransaction = new StandardImplicitTransaction(this);
118+
}
119+
120+
implicitTransaction.Start(transaction);
121+
m_implicitTransaction = implicitTransaction;
111122

112123
if (existingConnection == null)
113124
lock (s_lock)
@@ -116,11 +127,11 @@ public override void EnlistTransaction(System.Transactions.Transaction transacti
116127
}
117128
}
118129

119-
internal void UnenlistTransaction(MySqlXaTransaction xaTransaction, System.Transactions.Transaction transaction)
130+
internal void UnenlistTransaction(ImplicitTransactionBase implicitTransaction, System.Transactions.Transaction transaction)
120131
{
121-
if (!object.ReferenceEquals(xaTransaction, m_xaTransaction))
132+
if (!object.ReferenceEquals(implicitTransaction, m_implicitTransaction))
122133
throw new InvalidOperationException("Active transaction is not the one being unenlisted from.");
123-
m_xaTransaction = null;
134+
m_implicitTransaction = null;
124135

125136
if (m_shouldCloseWhenUnenlisted)
126137
{
@@ -154,7 +165,7 @@ private ServerSession DetachSession()
154165
return session;
155166
}
156167

157-
MySqlXaTransaction m_xaTransaction;
168+
ImplicitTransactionBase m_implicitTransaction;
158169
#endif
159170

160171
public override void Close() => DoClose(changeState: true);
@@ -428,7 +439,11 @@ internal async Task<CachedProcedure> GetCachedProcedure(IOBehavior ioBehavior, s
428439
internal DateTimeKind DateTimeKind => m_connectionSettings.DateTimeKind;
429440
internal int DefaultCommandTimeout => GetConnectionSettings().DefaultCommandTimeout;
430441
internal MySqlGuidFormat GuidFormat => m_connectionSettings.GuidFormat;
442+
#if NETSTANDARD1_3
431443
internal bool IgnoreCommandTransaction => m_connectionSettings.IgnoreCommandTransaction;
444+
#else
445+
internal bool IgnoreCommandTransaction => m_connectionSettings.IgnoreCommandTransaction || m_implicitTransaction is StandardImplicitTransaction;
446+
#endif
432447
internal bool IgnorePrepare => m_connectionSettings.IgnorePrepare;
433448
internal bool TreatTinyAsBoolean => m_connectionSettings.TreatTinyAsBoolean;
434449
internal IOBehavior AsyncIOBehavior => GetConnectionSettings().ForceSynchronous ? IOBehavior.Synchronous : IOBehavior.Asynchronous;
@@ -548,13 +563,13 @@ private void DoClose(bool changeState)
548563
#if !NETSTANDARD1_3
549564
// If participating in a distributed transaction, keep the connection open so we can commit or rollback.
550565
// This handles the common pattern of disposing a connection before disposing a TransactionScope (e.g., nested using blocks)
551-
if (m_xaTransaction != null)
566+
if (m_implicitTransaction != null)
552567
{
553568
// make sure all DB work is done
554569
m_activeReader?.Dispose();
555570
m_activeReader = null;
556571

557-
if (object.ReferenceEquals(m_xaTransaction.Connection, this))
572+
if (object.ReferenceEquals(m_implicitTransaction.Connection, this))
558573
{
559574
// if this was the original connection in the transaction, simply defer closing
560575
m_shouldCloseWhenUnenlisted = true;
@@ -563,7 +578,7 @@ private void DoClose(bool changeState)
563578
else
564579
{
565580
// reattach the session to the transaction's original connection
566-
m_xaTransaction.Connection.AttachSession(m_session);
581+
m_implicitTransaction.Connection.AttachSession(m_session);
567582
m_session = null;
568583
}
569584
}

src/MySqlConnector/MySql.Data.MySqlClient/MySqlConnectionStringBuilder.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,12 @@ public bool UseCompression
285285
set => MySqlConnectionStringOption.UseCompression.SetValue(this, value);
286286
}
287287

288+
public bool UseXaTransactions
289+
{
290+
get => MySqlConnectionStringOption.UseXaTransactions.GetValue(this);
291+
set => MySqlConnectionStringOption.UseXaTransactions.SetValue(this, value);
292+
}
293+
288294
// Other Methods
289295
public override bool ContainsKey(string key)
290296
{
@@ -381,6 +387,7 @@ internal abstract class MySqlConnectionStringOption
381387
public static readonly MySqlConnectionStringOption<bool> TreatTinyAsBoolean;
382388
public static readonly MySqlConnectionStringOption<bool> UseAffectedRows;
383389
public static readonly MySqlConnectionStringOption<bool> UseCompression;
390+
public static readonly MySqlConnectionStringOption<bool> UseXaTransactions;
384391

385392
public static MySqlConnectionStringOption TryGetOptionForKey(string key) =>
386393
s_options.TryGetValue(key, out var option) ? option : null;
@@ -587,6 +594,10 @@ static MySqlConnectionStringOption()
587594
AddOption(UseCompression = new MySqlConnectionStringOption<bool>(
588595
keys: new[] { "Compress", "Use Compression", "UseCompression" },
589596
defaultValue: false));
597+
598+
AddOption(UseXaTransactions = new MySqlConnectionStringOption<bool>(
599+
keys: new[] { "Use XA Transactions", "UseXaTransactions" },
600+
defaultValue: true));
590601
}
591602

592603
static readonly Dictionary<string, MySqlConnectionStringOption> s_options;

tests/MySqlConnector.Tests/MySqlConnectionStringBuilderTests.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ public void Defaults()
6767
Assert.False(csb.UseAffectedRows);
6868
#else
6969
Assert.True(csb.UseAffectedRows);
70+
#endif
71+
#if !BASELINE
72+
Assert.True(csb.UseXaTransactions);
7073
#endif
7174
}
7275

@@ -106,6 +109,7 @@ public void ParseConnectionString()
106109
"load balance=random;" +
107110
"guidformat=timeswapbinary16;" +
108111
"server spn=mariadb/[email protected];" +
112+
"use xa transactions=false;" +
109113
#endif
110114
"ignore prepare=false;" +
111115
"interactive=true;" +
@@ -154,6 +158,7 @@ public void ParseConnectionString()
154158
Assert.Equal(MySqlLoadBalance.Random, csb.LoadBalance);
155159
Assert.Equal(MySqlGuidFormat.TimeSwapBinary16, csb.GuidFormat);
156160
Assert.Equal("mariadb/[email protected]", csb.ServerSPN);
161+
Assert.False(csb.UseXaTransactions);
157162
#endif
158163
Assert.False(csb.IgnorePrepare);
159164
Assert.True(csb.InteractiveSession);

0 commit comments

Comments
 (0)