diff --git a/src/AdoNetCore.AseClient/AdoNetCore.AseClient.csproj b/src/AdoNetCore.AseClient/AdoNetCore.AseClient.csproj index 5a72650..6967ff4 100644 --- a/src/AdoNetCore.AseClient/AdoNetCore.AseClient.csproj +++ b/src/AdoNetCore.AseClient/AdoNetCore.AseClient.csproj @@ -5,5 +5,22 @@ 7 + 0.19.3 + 0.19.3 + True + True + + + True + True + Resources.resx + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + diff --git a/src/AdoNetCore.AseClient/AseConnection.cs b/src/AdoNetCore.AseClient/AseConnection.cs index 2c5283e..73e77d3 100644 --- a/src/AdoNetCore.AseClient/AseConnection.cs +++ b/src/AdoNetCore.AseClient/AseConnection.cs @@ -361,7 +361,7 @@ public override string ConnectionString public override ConnectionState State => InternalState; private ConnectionState InternalState { - get => _state; + get => _internal != null && _internal.IsDoomed ? ConnectionState.Broken : _state; set { if (_isDisposed) diff --git a/src/AdoNetCore.AseClient/AseTransaction.cs b/src/AdoNetCore.AseClient/AseTransaction.cs index e01fa86..66716df 100644 --- a/src/AdoNetCore.AseClient/AseTransaction.cs +++ b/src/AdoNetCore.AseClient/AseTransaction.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Diagnostics; namespace AdoNetCore.AseClient { @@ -141,16 +142,33 @@ public override void Commit() /// protected override void Dispose(bool disposing) { - base.Dispose(disposing); - if (_isDisposed) { return; } - - Rollback(); - - _isDisposed = true; + try + { + // Only rollback if the transaction is still open and the connection is open. For sure do not want to + // attempt to rollback a transaction on a closed or broken connection. The only other state in the + // ConnectionState that's currently used is Connecting and it doesn't seem appropriate to attempt a + // rollback from the Connecting state. + if (!_complete && _connection.State == ConnectionState.Open) + { + ExecuteRollback(); + } + } + catch (Exception ex) + { + Debug.Assert(false, "Failed to rollback transaction during dispose"); +#if NETFRAMEWORK || NETSTANDARD2_0 + Trace.TraceError(ex.ToString()); +#endif + } + finally + { + base.Dispose(disposing); + _isDisposed = true; + } } internal bool IsDisposed => _isDisposed; @@ -170,8 +188,10 @@ public override void Rollback() return; } - using (var command = _connection.CreateCommand()) - { + ExecuteRollback(); + } + private void ExecuteRollback() { + using (var command = _connection.CreateCommand()) { command.CommandText = "ROLLBACK TRANSACTION"; command.CommandType = CommandType.Text; command.Transaction = this; diff --git a/src/AdoNetCore.AseClient/Properties/Resources.Designer.cs b/src/AdoNetCore.AseClient/Properties/Resources.Designer.cs new file mode 100644 index 0000000..d704e9b --- /dev/null +++ b/src/AdoNetCore.AseClient/Properties/Resources.Designer.cs @@ -0,0 +1,64 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AdoNetCore.AseClient.Properties { + using System; + using System.Reflection; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AdoNetCore.AseClient.Properties.Resources", typeof(Resources).GetTypeInfo().Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/src/AdoNetCore.AseClient/Properties/Resources.resx b/src/AdoNetCore.AseClient/Properties/Resources.resx new file mode 100644 index 0000000..4fdb1b6 --- /dev/null +++ b/src/AdoNetCore.AseClient/Properties/Resources.resx @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/test/AdoNetCore.AseClient.Tests/Unit/AseConnectionTests.cs b/test/AdoNetCore.AseClient.Tests/Unit/AseConnectionTests.cs index 934d64e..14941a1 100644 --- a/test/AdoNetCore.AseClient.Tests/Unit/AseConnectionTests.cs +++ b/test/AdoNetCore.AseClient.Tests/Unit/AseConnectionTests.cs @@ -267,6 +267,23 @@ public void RepeatedDisposal_DoesNotThrow() connection.Dispose(); } + [Test] + public void DoomedReturnsBroken() { + var mockConnection = new Mock(); + var mockConnectionPoolManager = new Mock(); + + mockConnectionPoolManager + .Setup(x => x.Reserve(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(mockConnection.Object); + + mockConnection.SetupGet(x => x.IsDoomed).Returns(true); + + using (var connection = new AseConnection("Data Source=myASEserver;Port=5000;Database=foo;Uid=myUsername;Pwd=myPassword;", mockConnectionPoolManager.Object)) { + connection.Open(); + Assert.AreEqual(ConnectionState.Broken, connection.State); + } + } + private static IConnectionPoolManager InitMockConnectionPoolManager() { var mockConnection = new Mock(); diff --git a/test/AdoNetCore.AseClient.Tests/Unit/AseTransactionTests.cs b/test/AdoNetCore.AseClient.Tests/Unit/AseTransactionTests.cs index 6f9df0c..1bac4fe 100644 --- a/test/AdoNetCore.AseClient.Tests/Unit/AseTransactionTests.cs +++ b/test/AdoNetCore.AseClient.Tests/Unit/AseTransactionTests.cs @@ -1,4 +1,4 @@ -using System.Data; +using System.Data; using Moq; using NUnit.Framework; @@ -199,6 +199,8 @@ public void ImplicitRollback_WithValidTransaction_InteractsWithTheDbCommandCorre .Returns(mockCommandBeginTransaction.Object) .Returns(mockCommandRollbackTransaction.Object); + mockConnection.Setup(x => x.State).Returns(ConnectionState.Open); + // Act var connection = mockConnection.Object; @@ -222,6 +224,134 @@ public void ImplicitRollback_WithValidTransaction_InteractsWithTheDbCommandCorre mockCommandRollbackTransaction.Verify(); } + [Test] + public void ImplicitRollback_WithValidTransaction_DisposeFromUsing() + { + // Arrange + var mockConnection = new Mock(); + var isolationLevel = IsolationLevel.Serializable; + + var mockCommandIsolationLevel = new Mock(); + var mockCommandBeginTransaction = new Mock(); + var mockCommandRollbackTransaction = new Mock(); + + mockCommandIsolationLevel + .SetupAllProperties() + .Setup(x => x.ExecuteNonQuery()) + .Returns(0); + + mockCommandBeginTransaction + .SetupAllProperties() + .Setup(x => x.ExecuteNonQuery()) + .Returns(0); + + mockCommandRollbackTransaction + .SetupAllProperties() + .Setup(x => x.ExecuteNonQuery()) + .Returns(0); + + mockConnection + .Setup(x => x.BeginTransaction(isolationLevel)) + .Returns(() => { + // Simulate what AseConnection.BeginTransaction() does. + var t = new AseTransaction(mockConnection.Object, isolationLevel); + t.Begin(); + return t; + }); + + mockConnection + .SetupSequence(x => x.CreateCommand()) + .Returns(mockCommandIsolationLevel.Object) + .Returns(mockCommandBeginTransaction.Object) + .Returns(mockCommandRollbackTransaction.Object); + + mockConnection.Setup(x => x.State).Returns(ConnectionState.Open); + + + // Act + using (var connection = mockConnection.Object) { + using (var transaction = connection.BeginTransaction(isolationLevel)) { + // Do nothing + } + } + + // Assert + mockCommandIsolationLevel.VerifySet(x => { x.CommandText = "SET TRANSACTION ISOLATION LEVEL 3"; }); + mockCommandIsolationLevel.VerifySet(x => { x.CommandType = CommandType.Text; }); + mockCommandIsolationLevel.Verify(); + + mockCommandBeginTransaction.VerifySet(x => { x.CommandText = "BEGIN TRANSACTION"; }); + mockCommandBeginTransaction.VerifySet(x => { x.CommandType = CommandType.Text; }); + mockCommandBeginTransaction.Verify(); + + mockCommandRollbackTransaction.VerifySet(x => { x.CommandText = "ROLLBACK TRANSACTION"; }); + mockCommandRollbackTransaction.VerifySet(x => { x.CommandType = CommandType.Text; }); + mockCommandRollbackTransaction.Verify(); + } + [Test] + public void ImplicitRollback_ClosedConnection_DisposeFromUsing() { + // Arrange + var mockConnection = new Mock(); + var isolationLevel = IsolationLevel.Serializable; + + var mockCommandIsolationLevel = new Mock(); + var mockCommandBeginTransaction = new Mock(); + var mockCommandRollbackTransaction = new Mock(); + + mockCommandIsolationLevel + .SetupAllProperties() + .Setup(x => x.ExecuteNonQuery()) + .Returns(0); + + mockCommandBeginTransaction + .SetupAllProperties() + .Setup(x => x.ExecuteNonQuery()) + .Returns(0); + + mockCommandRollbackTransaction + .SetupAllProperties() + .Setup(x => x.ExecuteNonQuery()) + .Returns(0); + + mockConnection + .Setup(x => x.BeginTransaction(isolationLevel)) + .Returns(() => { + // Simulate what AseConnection.BeginTransaction() does. + var t = new AseTransaction(mockConnection.Object, isolationLevel); + t.Begin(); + return t; + }); + + mockConnection + .SetupSequence(x => x.CreateCommand()) + .Returns(mockCommandIsolationLevel.Object) + .Returns(mockCommandBeginTransaction.Object) + .Returns(mockCommandRollbackTransaction.Object); + + mockConnection.Setup(x => x.State).Returns(ConnectionState.Closed); + + + // Act + using (var connection = mockConnection.Object) { + using (var transaction = connection.BeginTransaction(isolationLevel)) { + // Do nothing + } + } + + // Assert + mockCommandIsolationLevel.VerifySet(x => { x.CommandText = "SET TRANSACTION ISOLATION LEVEL 3"; }); + mockCommandIsolationLevel.VerifySet(x => { x.CommandType = CommandType.Text; }); + mockCommandIsolationLevel.Verify(); + + mockCommandBeginTransaction.VerifySet(x => { x.CommandText = "BEGIN TRANSACTION"; }); + mockCommandBeginTransaction.VerifySet(x => { x.CommandType = CommandType.Text; }); + mockCommandBeginTransaction.Verify(); + + mockCommandRollbackTransaction.VerifySet(x => x.CommandText = "ROLLBACK TRANSACTION", Times.Never); + mockCommandRollbackTransaction.VerifySet(x => x.CommandType = CommandType.Text, Times.Never); + mockCommandRollbackTransaction.Verify(x => x.ExecuteNonQuery(), Times.Never); + } + [Test] public void RepeatedDisposal_DoesNotThrow() { @@ -272,5 +402,68 @@ public void RepeatedDisposal_DoesNotThrow() transaction.Dispose(); // Implicit rollback transaction.Dispose(); // Should do nothing } + [Test] + public void ImplicitRollback_BrokenConnection_DisposeFromUsing() { + // Arrange + var mockConnection = new Mock(); + var isolationLevel = IsolationLevel.Serializable; + + var mockCommandIsolationLevel = new Mock(); + var mockCommandBeginTransaction = new Mock(); + var mockCommandRollbackTransaction = new Mock(); + + mockCommandIsolationLevel + .SetupAllProperties() + .Setup(x => x.ExecuteNonQuery()) + .Returns(0); + + mockCommandBeginTransaction + .SetupAllProperties() + .Setup(x => x.ExecuteNonQuery()) + .Returns(0); + + mockCommandRollbackTransaction + .SetupAllProperties() + .Setup(x => x.ExecuteNonQuery()) + .Returns(0); + + mockConnection + .Setup(x => x.BeginTransaction(isolationLevel)) + .Returns(() => { + // Simulate what AseConnection.BeginTransaction() does. + var t = new AseTransaction(mockConnection.Object, isolationLevel); + t.Begin(); + return t; + }); + + mockConnection + .SetupSequence(x => x.CreateCommand()) + .Returns(mockCommandIsolationLevel.Object) + .Returns(mockCommandBeginTransaction.Object) + .Returns(mockCommandRollbackTransaction.Object); + + mockConnection.Setup(x => x.State).Returns(ConnectionState.Broken); + + + // Act + using (var connection = mockConnection.Object) { + using (var transaction = connection.BeginTransaction(isolationLevel)) { + // Do nothing + } + } + + // Assert + mockCommandIsolationLevel.VerifySet(x => { x.CommandText = "SET TRANSACTION ISOLATION LEVEL 3"; }); + mockCommandIsolationLevel.VerifySet(x => { x.CommandType = CommandType.Text; }); + mockCommandIsolationLevel.Verify(); + + mockCommandBeginTransaction.VerifySet(x => { x.CommandText = "BEGIN TRANSACTION"; }); + mockCommandBeginTransaction.VerifySet(x => { x.CommandType = CommandType.Text; }); + mockCommandBeginTransaction.Verify(); + + mockCommandRollbackTransaction.VerifySet(x => x.CommandText = "ROLLBACK TRANSACTION", Times.Never); + mockCommandRollbackTransaction.VerifySet(x => x.CommandType = CommandType.Text, Times.Never); + mockCommandRollbackTransaction.Verify(x => x.ExecuteNonQuery(), Times.Never); + } } }