@@ -177,6 +177,73 @@ public void StasisCounters_Functional()
177177 Assert . Equal ( 0 , SqlClientEventSourceProps . StasisConnections ) ;
178178 }
179179
180+ [ ConditionalFact ( typeof ( DataTestUtility ) , nameof ( DataTestUtility . AreConnStringsSetup ) , nameof ( DataTestUtility . IsNotAzureSynapse ) ) ]
181+ public void TransactedConnectionPool_VerifyActiveConnectionCounters ( )
182+ {
183+ // This test verifies that the active connection count metric never goes negative
184+ // when connections are returned to the pool while enlisted in a transaction.
185+ // This is a regression test for issue #3640 where an extra DeactivateConnection
186+ // call was causing the active connection count to go negative.
187+
188+ // Arrange
189+ var stringBuilder = new SqlConnectionStringBuilder ( DataTestUtility . TCPConnectionString )
190+ {
191+ Pooling = true ,
192+ Enlist = false ,
193+ MinPoolSize = 0 ,
194+ MaxPoolSize = 10
195+ } ;
196+
197+ // Clear pools to start fresh
198+ ClearConnectionPools ( ) ;
199+
200+ long initialActiveSoftConnections = SqlClientEventSourceProps . ActiveSoftConnections ;
201+ long initialActiveHardConnections = SqlClientEventSourceProps . ActiveHardConnections ;
202+ long initialActiveConnections = SqlClientEventSourceProps . ActiveConnections ;
203+
204+ // Act and Assert
205+ // Verify counters at each step in the lifecycle of a transacted connection
206+ using ( var txScope = new TransactionScope ( ) )
207+ {
208+ using ( var conn = new SqlConnection ( stringBuilder . ToString ( ) ) )
209+ {
210+ conn . Open ( ) ;
211+ conn . EnlistTransaction ( System . Transactions . Transaction . Current ) ;
212+
213+ if ( SupportsActiveConnectionCounters )
214+ {
215+ // Connection should be active
216+ Assert . Equal ( initialActiveSoftConnections + 1 , SqlClientEventSourceProps . ActiveSoftConnections ) ;
217+ Assert . Equal ( initialActiveHardConnections + 1 , SqlClientEventSourceProps . ActiveHardConnections ) ;
218+ Assert . Equal ( initialActiveConnections + 1 , SqlClientEventSourceProps . ActiveConnections ) ;
219+ }
220+
221+ conn . Close ( ) ;
222+
223+ // Connection is returned to pool but still in transaction (stasis)
224+ if ( SupportsActiveConnectionCounters )
225+ {
226+ // Connection should be deactivated (returned to pool)
227+ Assert . Equal ( initialActiveSoftConnections , SqlClientEventSourceProps . ActiveSoftConnections ) ;
228+ Assert . Equal ( initialActiveHardConnections + 1 , SqlClientEventSourceProps . ActiveHardConnections ) ;
229+ Assert . Equal ( initialActiveConnections , SqlClientEventSourceProps . ActiveConnections ) ;
230+ }
231+ }
232+
233+ // Completing the transaction after the connection is closed ensures that the connection
234+ // is in the transacted pool at the time the transaction ends. This verifies that the
235+ // transition from the transacted pool back to the main pool properly updates the counters.
236+ txScope . Complete ( ) ;
237+ }
238+
239+ if ( SupportsActiveConnectionCounters )
240+ {
241+ Assert . Equal ( initialActiveSoftConnections , SqlClientEventSourceProps . ActiveSoftConnections ) ;
242+ Assert . Equal ( initialActiveHardConnections + 1 , SqlClientEventSourceProps . ActiveHardConnections ) ;
243+ Assert . Equal ( initialActiveConnections , SqlClientEventSourceProps . ActiveConnections ) ;
244+ }
245+ }
246+
180247 [ ActiveIssue ( "https://github.com/dotnet/SqlClient/issues/3031" ) ]
181248 [ ConditionalFact ( typeof ( DataTestUtility ) , nameof ( DataTestUtility . AreConnStringsSetup ) ) ]
182249 public void ReclaimedConnectionsCounter_Functional ( )
0 commit comments