Skip to content

Commit 84a4a78

Browse files
committed
Add tests confirming InvalidCastException race in TryOpenInner (#3314)
Add functional tests that reproduce the root cause of issue #3314: when _innerConnection is DbConnectionClosedConnecting (a transitional state), the cast to SqlConnectionInternal in TryOpenInner throws InvalidCastException. This occurs when a SqlConnection is used concurrently from multiple threads. Tests: - Confirm DbConnectionClosedConnecting is not assignable to SqlConnectionInternal - Confirm the cast throws InvalidCastException - Confirm Connecting state is reported correctly - Confirm Open() on a connecting connection throws InvalidOperationException
1 parent f5942fd commit 84a4a78

1 file changed

Lines changed: 130 additions & 0 deletions

File tree

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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

Comments
 (0)