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);
+ }
}
}