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 System . Threading . Tasks ;
9+ using Microsoft . Data . ProviderBase ;
10+ using Microsoft . Data . SqlClient . Connection ;
11+ using Xunit ;
12+
13+ namespace Microsoft . Data . SqlClient . UnitTests . Microsoft . Data . SqlClient
14+ {
15+ /// <summary>
16+ /// Regression tests for GitHub issue #3314.
17+ ///
18+ /// Root cause: TryOpenInner() read InnerConnection twice - once for TryOpenConnection() and
19+ /// again for the cast to SqlConnectionInternal. Between those two reads another thread could
20+ /// change _innerConnection to DbConnectionClosedConnecting, which is not assignable to
21+ /// SqlConnectionInternal, causing an opaque InvalidCastException.
22+ ///
23+ /// Fix: InnerConnection is now captured into a local variable once; if it is not a
24+ /// SqlConnectionInternal an InvalidOperationException with a descriptive message is thrown
25+ /// instead of an InvalidCastException.
26+ /// </summary>
27+ public class SqlConnectionConcurrentOpenTests
28+ {
29+ private static readonly MethodInfo s_tryOpenInner = typeof ( SqlConnection )
30+ . GetMethod ( "TryOpenInner" , BindingFlags . Instance | BindingFlags . NonPublic ) ! ;
31+
32+ private static DbConnectionInternal GetConnectingSingleton ( )
33+ {
34+ return DbConnectionClosedConnecting . SingletonInstance ;
35+ }
36+
37+ private static void ForceInnerConnection ( SqlConnection connection , DbConnectionInternal innerConnectionValue )
38+ {
39+ connection . SetInnerConnectionTo ( innerConnectionValue ) ;
40+ }
41+
42+ private static bool InvokeTryOpenInner ( SqlConnection connection , TaskCompletionSource < DbConnectionInternal > retry )
43+ {
44+ try
45+ {
46+ return ( bool ) s_tryOpenInner . Invoke ( connection , [ retry ] ) ! ;
47+ }
48+ catch ( TargetInvocationException tie ) when ( tie . InnerException != null )
49+ {
50+ throw tie . InnerException ;
51+ }
52+ }
53+
54+ [ Fact ]
55+ public void InnerConnection_DbConnectionClosedConnecting_IsNotAssignableToSqlConnectionInternal ( )
56+ {
57+ DbConnectionInternal connectingSingleton = GetConnectingSingleton ( ) ;
58+
59+ Assert . False (
60+ connectingSingleton is SqlConnectionInternal ,
61+ "DbConnectionClosedConnecting must NOT be assignable to SqlConnectionInternal. " +
62+ "If it were, the race condition in #3314 would not manifest." ) ;
63+ }
64+
65+ [ Fact ]
66+ public void InnerConnection_InConnectingState_ReportsConnectingState ( )
67+ {
68+ DbConnectionInternal connectingSingleton = GetConnectingSingleton ( ) ;
69+
70+ var connection = new SqlConnection ( "Data Source=localhost" ) ;
71+ ForceInnerConnection ( connection , connectingSingleton ) ;
72+
73+ Assert . Equal ( ConnectionState . Connecting , connection . State ) ;
74+ }
75+
76+ [ Fact ]
77+ public void Open_WhenAlreadyConnecting_ThrowsInvalidOperation ( )
78+ {
79+ DbConnectionInternal connectingSingleton = GetConnectingSingleton ( ) ;
80+
81+ var connection = new SqlConnection ( "Data Source=localhost" ) ;
82+ ForceInnerConnection ( connection , connectingSingleton ) ;
83+
84+ Assert . Throws < InvalidOperationException > ( ( ) => connection . Open ( ) ) ;
85+ }
86+
87+ [ Fact ]
88+ public void TryOpenInner_WhenInnerConnectionRacesToNonSqlConnectionInternalState_ThrowsInvalidOperation_NotInvalidCast ( )
89+ {
90+ DbConnectionInternal initialConnectingState = GetConnectingSingleton ( ) ;
91+ DbConnectionInternal racedNonSqlConnectionInternalState = DbConnectionOpenBusy . SingletonInstance ;
92+
93+ var connection = new SqlConnection ( "Data Source=localhost" ) ;
94+ ForceInnerConnection ( connection , initialConnectingState ) ;
95+
96+ TaskCompletionSource < DbConnectionInternal > completedRetry = new ( ) ;
97+ completedRetry . SetResult ( racedNonSqlConnectionInternalState ) ;
98+
99+ Exception ex = Assert . ThrowsAny < Exception > ( ( ) =>
100+ {
101+ InvokeTryOpenInner ( connection , completedRetry ) ;
102+ } ) ;
103+
104+ Assert . True (
105+ ex is InvalidOperationException ,
106+ $ "Expected InvalidOperationException but got { ex . GetType ( ) . Name } : { ex . Message } . " +
107+ "The fix for #3314 must throw InvalidOperationException (not InvalidCastException) " +
108+ "when _innerConnection races to a non-SqlConnectionInternal state inside TryOpenInner." ) ;
109+
110+ Assert . Contains ( "connection" , ex . Message , StringComparison . OrdinalIgnoreCase ) ;
111+ }
112+ }
113+ }
0 commit comments