Skip to content

Commit

Permalink
Support System.Transactions. Fixes mysql-net#13
Browse files Browse the repository at this point in the history
  • Loading branch information
bgrainger committed Apr 15, 2017
1 parent 42e17be commit c928d35
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 6 deletions.
8 changes: 8 additions & 0 deletions docs/content/tutorials/migrating-from-connector-net.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,16 @@ MySqlConnector has some different default connection string options:
Some command line options that are supported in Connector/NET are not supported in MySqlConnector. For a full list of options that are
supported in MySqlConnector, see the [Connection Options](connection-options)

### TransactionScope

MySqlConnector adds full distributed transaction support (for client code using [`TransactionScope`](https://msdn.microsoft.com/en-us/library/system.transactions.transactionscope.aspx)),
while Connector/NET uses regular database transactions. As a result, code that uses `TransactionScope`
may execute differently with MySqlConnector. To get Connector/NET-compatible behavior, remove
`TransactionScope` and use `BeginTransaction`/`Commit` directly.

### Bugs present in Connector/NET that are fixed in MySqlConnector

* [#37283](https://bugs.mysql.com/bug.php?id=37283), [#70587](https://bugs.mysql.com/bug.php?id=70587): Distributed transactions are not supported
* [#66476](https://bugs.mysql.com/bug.php?id=66476): Connection pool uses queue instead of stack
* [#70111](https://bugs.mysql.com/bug.php?id=70111): `Async` methods execute synchronously
* [#70686](https://bugs.mysql.com/bug.php?id=70686): `TIME(3)` and `TIME(6)` fields serialize milliseconds incorrectly
Expand Down
55 changes: 49 additions & 6 deletions src/MySqlConnector/MySqlClient/MySqlConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ private async Task<MySqlTransaction> BeginDbTransactionAsync(IsolationLevel isol
throw new InvalidOperationException("Connection is not open.");
if (CurrentTransaction != null)
throw new InvalidOperationException("Transactions may not be nested.");
#if !NETSTANDARD1_3
if (m_xaTransaction != null)
throw new InvalidOperationException("Cannot begin a transaction when already enlisted in a transaction.");
#endif

string isolationLevelValue;
switch (isolationLevel)
Expand Down Expand Up @@ -76,8 +80,32 @@ private async Task<MySqlTransaction> BeginDbTransactionAsync(IsolationLevel isol
#if !NETSTANDARD1_3
public override void EnlistTransaction(System.Transactions.Transaction transaction)
{
throw new NotSupportedException("System.Transactions.Transaction is not supported. Use BeginTransaction instead.");
if (m_xaTransaction != null)
throw new MySqlException("Already enlisted in a Transaction.");
if (CurrentTransaction != null)
throw new InvalidOperationException("Can't enlist in a Transaction when there is an active MySqlTransaction.");

if (transaction != null)
{
m_xaTransaction = new MySqlXaTransaction(this);
m_xaTransaction.Start(transaction);
}
}

internal void UnenlistTransaction(MySqlXaTransaction xaTransaction)
{
if (!object.ReferenceEquals(xaTransaction, m_xaTransaction))
throw new InvalidOperationException("Active transaction is not the one being unenlisted from.");
m_xaTransaction = null;

if (m_shouldCloseWhenUnenlisted)
{
m_shouldCloseWhenUnenlisted = false;
Close();
}
}

MySqlXaTransaction m_xaTransaction;
#endif

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

SetState(ConnectionState.Connecting);

Expand All @@ -135,6 +159,11 @@ private async Task OpenAsync(IOBehavior ioBehavior, CancellationToken cancellati
SetState(ConnectionState.Closed);
throw new MySqlException("Unable to connect to any of the specified MySQL hosts.", ex);
}

#if !NETSTANDARD1_3
if (System.Transactions.Transaction.Current != null)
EnlistTransaction(System.Transactions.Transaction.Current);
#endif
}

public override string ConnectionString
Expand Down Expand Up @@ -195,7 +224,7 @@ protected override void Dispose(bool disposing)
}
finally
{
m_isDisposed = true;
m_isDisposed = !m_shouldCloseWhenUnenlisted;
base.Dispose(disposing);
}
}
Expand Down Expand Up @@ -316,6 +345,19 @@ private void VerifyNotDisposed()

private void DoClose()
{
#if !NETSTANDARD1_3
// If participating in a distributed transaction, keep the connection open so we can commit or rollback.
// This handles the common pattern of disposing a connection before disposing a TransactionScope (e.g., nested using blocks)
if (m_xaTransaction != null)
{
m_shouldCloseWhenUnenlisted = true;
return;
}
#else
// fix "field is never assigned" compiler error
m_shouldCloseWhenUnenlisted = false;
#endif

if (m_connectionState != ConnectionState.Closed)
{
try
Expand Down Expand Up @@ -355,6 +397,7 @@ private void CloseDatabase()
ConnectionState m_connectionState;
bool m_hasBeenOpened;
bool m_isDisposed;
bool m_shouldCloseWhenUnenlisted;
Dictionary<string, CachedProcedure> m_cachedProcedures;
}
}
65 changes: 65 additions & 0 deletions src/MySqlConnector/MySqlClient/MySqlXaTransaction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#if !NETSTANDARD1_3
using System;
using System.Globalization;
using System.Threading;
using System.Transactions;

namespace MySql.Data.MySqlClient
{
internal sealed class MySqlXaTransaction : IEnlistmentNotification
{
public MySqlXaTransaction(MySqlConnection connection) => m_connection = connection;

public void Start(Transaction transaction)
{
// generate an "xid" with "gtrid" (Global TRansaction ID) from the .NET Transaction and "bqual" (Branch QUALifier)
// unique to this object
var id = Interlocked.Increment(ref s_currentId);
m_xid = "'" + transaction.TransactionInformation.LocalIdentifier + "', '" + id.ToString(CultureInfo.InvariantCulture) + "'";

ExecuteXaCommand("START");

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

public void Prepare(PreparingEnlistment enlistment)
{
ExecuteXaCommand("END");
ExecuteXaCommand("PREPARE");
enlistment.Prepared();
}

public void Commit(Enlistment enlistment)
{
ExecuteXaCommand("COMMIT");
enlistment.Done();
m_connection.UnenlistTransaction(this);
}

public void Rollback(Enlistment enlistment)
{
ExecuteXaCommand("END");
ExecuteXaCommand("ROLLBACK");
enlistment.Done();
m_connection.UnenlistTransaction(this);
}

public void InDoubt(Enlistment enlistment) => throw new NotSupportedException();

private void ExecuteXaCommand(string statement)
{
using (var cmd = m_connection.CreateCommand())
{
cmd.CommandText = "XA " + statement + " " + m_xid;
cmd.ExecuteNonQuery();
}
}

static int s_currentId;

readonly MySqlConnection m_connection;
string m_xid;
}
}
#endif
1 change: 1 addition & 0 deletions tests/SideBySide/SideBySide.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

<ItemGroup Condition=" '$(TargetFramework)' == 'net462' ">
<Reference Include="System" />
<Reference Include="System.Transactions" />
<Reference Include="Microsoft.CSharp" />
</ItemGroup>

Expand Down
Loading

0 comments on commit c928d35

Please sign in to comment.