Describe the bug
When WaitHandleDbConnectionPool.ReplaceConnection creates a replacement successfully but the replacement throws during activation, CreateObject has already removed the original connection from _objectList and added the replacement. PrepareConnection then returns the failed replacement to the idle pool.
The original connection remains alive and checked out but is no longer tracked by _objectList, while the failed replacement remains pooled instead of being disposed. Pool ownership and metrics no longer describe the physical connections accurately, and the original connection cannot be reclaimed through ReclaimEmancipatedObjects.
If you are seeing an exception, include the full exceptions details (message and stack trace).
Exception message: The replacement's activation exception is propagated.
Stack trace: N/A
To reproduce
Use a test connection factory whose first connection activates normally and whose second connection opens successfully but throws from Activate. Acquire the first connection, then call WaitHandleDbConnectionPool.ReplaceConnection.
DbConnectionInternal original = GetConnection(pool, owner);
factory.FailReplacementActivation = true;
Assert.Throws<TestActivationException>(() =>
pool.ReplaceConnection(owner, original, timeout));
After the exception, the failed replacement is not disposed and is counted as free, while the live original is absent from the pool's tracking list.
Expected behavior
Dispose the failed replacement, restore the original connection to the pool's tracking list, and leave the original checked out for the caller's reconnect retry. Hard, soft, pooled, free, and active connection counters should reflect exactly that state.
Further technical details
Microsoft.Data.SqlClient version: current main
.NET target: .NET 9.0 reproduction; shared pool code also targets .NET 8.0 and .NET Framework 4.6.2
SQL Server version: Not applicable; deterministic unit-test reproduction uses a fake connection factory
Operating system: macOS; issue is platform-independent
Additional context
ChannelDbConnectionPool.ReplaceConnection already preserves the original until the replacement is fully activated. The WaitHandle implementation should provide the same rollback behavior.
Describe the bug
When
WaitHandleDbConnectionPool.ReplaceConnectioncreates a replacement successfully but the replacement throws during activation,CreateObjecthas already removed the original connection from_objectListand added the replacement.PrepareConnectionthen returns the failed replacement to the idle pool.The original connection remains alive and checked out but is no longer tracked by
_objectList, while the failed replacement remains pooled instead of being disposed. Pool ownership and metrics no longer describe the physical connections accurately, and the original connection cannot be reclaimed throughReclaimEmancipatedObjects.If you are seeing an exception, include the full exceptions details (message and stack trace).
To reproduce
Use a test connection factory whose first connection activates normally and whose second connection opens successfully but throws from
Activate. Acquire the first connection, then callWaitHandleDbConnectionPool.ReplaceConnection.After the exception, the failed replacement is not disposed and is counted as free, while the live original is absent from the pool's tracking list.
Expected behavior
Dispose the failed replacement, restore the original connection to the pool's tracking list, and leave the original checked out for the caller's reconnect retry. Hard, soft, pooled, free, and active connection counters should reflect exactly that state.
Further technical details
Microsoft.Data.SqlClient version: current
main.NET target: .NET 9.0 reproduction; shared pool code also targets .NET 8.0 and .NET Framework 4.6.2
SQL Server version: Not applicable; deterministic unit-test reproduction uses a fake connection factory
Operating system: macOS; issue is platform-independent
Additional context
ChannelDbConnectionPool.ReplaceConnectionalready preserves the original until the replacement is fully activated. The WaitHandle implementation should provide the same rollback behavior.