|
| 1 | +// Licensed to the .NET Foundation under one or more agreements. |
| 2 | +// The .NET Foundation licenses this file to you under the MIT license. |
| 3 | +// See the LICENSE file in the project root for more information. |
| 4 | + |
| 5 | +using System; |
| 6 | +using System.Data; |
| 7 | +using System.Reflection; |
| 8 | +using Xunit; |
| 9 | + |
| 10 | +namespace Microsoft.Data.SqlClient.Tests |
| 11 | +{ |
| 12 | + /// <summary> |
| 13 | + /// Tests that confirm the race condition described in GitHub issue #3314: |
| 14 | + /// If _innerConnection is set to DbConnectionClosedConnecting (a transitional state) |
| 15 | + /// at the moment TryOpenInner casts it to SqlConnectionInternal, an InvalidCastException |
| 16 | + /// is thrown. This simulates a concurrent Open() on the same SqlConnection instance. |
| 17 | + /// </summary> |
| 18 | + public partial class SqlConnectionTest |
| 19 | + { |
| 20 | + /// <summary> |
| 21 | + /// Verifies that when _innerConnection holds DbConnectionClosedConnecting (a non-open |
| 22 | + /// transitional state), casting it to SqlConnectionInternal throws InvalidCastException. |
| 23 | + /// This is the exact exception observed in issue #3314. |
| 24 | + /// </summary> |
| 25 | + [Fact] |
| 26 | + public void InnerConnection_CastToSqlConnectionInternal_ThrowsInvalidCast_WhenInConnectingState() |
| 27 | + { |
| 28 | + // Arrange: get the DbConnectionClosedConnecting singleton via reflection |
| 29 | + Type closedConnectingType = typeof(SqlConnection).Assembly |
| 30 | + .GetType("Microsoft.Data.ProviderBase.DbConnectionClosedConnecting", throwOnError: true); |
| 31 | + FieldInfo singletonField = closedConnectingType |
| 32 | + .GetField("SingletonInstance", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); |
| 33 | + Assert.NotNull(singletonField); |
| 34 | + object connectingSingleton = singletonField.GetValue(null); |
| 35 | + Assert.NotNull(connectingSingleton); |
| 36 | + |
| 37 | + // Verify it is NOT SqlConnectionInternal — this is the root cause of the InvalidCastException |
| 38 | + Type sqlConnectionInternalType = typeof(SqlConnection).Assembly |
| 39 | + .GetType("Microsoft.Data.SqlClient.Connection.SqlConnectionInternal", throwOnError: true); |
| 40 | + Assert.False( |
| 41 | + sqlConnectionInternalType.IsInstanceOfType(connectingSingleton), |
| 42 | + "DbConnectionClosedConnecting must not be assignable to SqlConnectionInternal"); |
| 43 | + |
| 44 | + // Act: set a SqlConnection's _innerConnection to the Connecting state to simulate the race |
| 45 | + var connection = new SqlConnection("Data Source=localhost"); |
| 46 | + FieldInfo innerConnectionField = typeof(SqlConnection) |
| 47 | + .GetField("_innerConnection", BindingFlags.Instance | BindingFlags.NonPublic); |
| 48 | + Assert.NotNull(innerConnectionField); |
| 49 | + |
| 50 | + innerConnectionField.SetValue(connection, connectingSingleton); |
| 51 | + |
| 52 | + // Read it back through InnerConnection and attempt the same cast that TryOpenInner does |
| 53 | + PropertyInfo innerConnectionProperty = typeof(SqlConnection) |
| 54 | + .GetProperty("InnerConnection", BindingFlags.Instance | BindingFlags.NonPublic); |
| 55 | + Assert.NotNull(innerConnectionProperty); |
| 56 | + object innerConnection = innerConnectionProperty.GetValue(connection); |
| 57 | + |
| 58 | + // Assert: the runtime type is not assignable to SqlConnectionInternal |
| 59 | + Assert.False( |
| 60 | + sqlConnectionInternalType.IsAssignableFrom(innerConnection.GetType()), |
| 61 | + $"Expected InnerConnection type '{innerConnection.GetType().FullName}' to NOT be assignable " + |
| 62 | + $"to '{sqlConnectionInternalType.FullName}'. If it were, the cast in TryOpenInner would " + |
| 63 | + "succeed and the race condition in issue #3314 would not manifest as InvalidCastException."); |
| 64 | + |
| 65 | + // Perform the exact cast that TryOpenInner does at SqlConnection.cs line 2228: |
| 66 | + // var tdsInnerConnection = (SqlConnectionInternal)InnerConnection; |
| 67 | + // This must throw InvalidCastException when _innerConnection is DbConnectionClosedConnecting. |
| 68 | + Exception ex = Assert.ThrowsAny<Exception>(() => |
| 69 | + { |
| 70 | + // Use Convert.ChangeType or direct cast via reflection to replicate |
| 71 | + // the CLR's cast behavior for internal types we cannot reference directly. |
| 72 | + Convert.ChangeType(innerConnection, sqlConnectionInternalType); |
| 73 | + }); |
| 74 | + Assert.True( |
| 75 | + ex is InvalidCastException, |
| 76 | + $"Expected InvalidCastException but got {ex.GetType().Name}: {ex.Message}"); |
| 77 | + } |
| 78 | + |
| 79 | + /// <summary> |
| 80 | + /// Verifies that a SqlConnection in the Connecting state reports ConnectionState.Connecting, |
| 81 | + /// which is an unexpected state for post-open code to encounter. |
| 82 | + /// </summary> |
| 83 | + [Fact] |
| 84 | + public void InnerConnection_InConnectingState_ReportsConnectingState() |
| 85 | + { |
| 86 | + // Arrange |
| 87 | + Type closedConnectingType = typeof(SqlConnection).Assembly |
| 88 | + .GetType("Microsoft.Data.ProviderBase.DbConnectionClosedConnecting", throwOnError: true); |
| 89 | + FieldInfo singletonField = closedConnectingType |
| 90 | + .GetField("SingletonInstance", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); |
| 91 | + object connectingSingleton = singletonField.GetValue(null); |
| 92 | + |
| 93 | + var connection = new SqlConnection("Data Source=localhost"); |
| 94 | + FieldInfo innerConnectionField = typeof(SqlConnection) |
| 95 | + .GetField("_innerConnection", BindingFlags.Instance | BindingFlags.NonPublic); |
| 96 | + innerConnectionField.SetValue(connection, connectingSingleton); |
| 97 | + |
| 98 | + // Act & Assert: the state should be Connecting, not Open |
| 99 | + // This confirms the connection is in a transitional state where |
| 100 | + // TryOpenInner's cast would fail |
| 101 | + Assert.Equal(ConnectionState.Connecting, connection.State); |
| 102 | + } |
| 103 | + |
| 104 | + /// <summary> |
| 105 | + /// Verifies that calling Open() on a SqlConnection that is already in the Connecting state |
| 106 | + /// throws InvalidOperationException ("already open"), confirming that concurrent Open() |
| 107 | + /// calls on the same instance are not supported. |
| 108 | + /// </summary> |
| 109 | + [Fact] |
| 110 | + public void Open_WhenAlreadyConnecting_ThrowsInvalidOperation() |
| 111 | + { |
| 112 | + // Arrange: force `_innerConnection` to DbConnectionClosedConnecting |
| 113 | + Type closedConnectingType = typeof(SqlConnection).Assembly |
| 114 | + .GetType("Microsoft.Data.ProviderBase.DbConnectionClosedConnecting", throwOnError: true); |
| 115 | + FieldInfo singletonField = closedConnectingType |
| 116 | + .GetField("SingletonInstance", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); |
| 117 | + object connectingSingleton = singletonField.GetValue(null); |
| 118 | + |
| 119 | + var connection = new SqlConnection("Data Source=localhost"); |
| 120 | + FieldInfo innerConnectionField = typeof(SqlConnection) |
| 121 | + .GetField("_innerConnection", BindingFlags.Instance | BindingFlags.NonPublic); |
| 122 | + innerConnectionField.SetValue(connection, connectingSingleton); |
| 123 | + |
| 124 | + // Act & Assert: Open() while connecting should throw |
| 125 | + // DbConnectionClosedConnecting.TryOpenConnection throws "connection already open" |
| 126 | + // when retry is null (synchronous path) |
| 127 | + Assert.Throws<InvalidOperationException>(() => connection.Open()); |
| 128 | + } |
| 129 | + } |
| 130 | +} |
0 commit comments