From 8916abd85d60cc121fd78d8e53361ab1d74c7816 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Thu, 29 Jan 2026 14:32:56 -0800 Subject: [PATCH 1/3] Correct behavior. --- .../SqlClient/SqlInternalConnectionTds.cs | 18 ++++++++++- .../SqlClient/SqlInternalConnectionTds.cs | 18 ++++++++++- .../ConnectionEnhancedRoutingTests.cs | 30 +++++++------------ .../tools/TDS/TDS.Servers/RoutingTdsServer.cs | 22 ++++++++++++++ 4 files changed, 66 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 9ce4eccb71..cfd42f3e60 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -1618,6 +1618,14 @@ private void LoginNoFailover(ServerInfo serverInfo, if (RoutingInfo != null) { + // Check if we received enhanced routing info, but not the ack for the feature. + // In this case, we should ignore the routing info and connect to the current server. + if (!string.IsNullOrEmpty(RoutingInfo.DatabaseName) && !IsEnhancedRoutingSupportEnabled) + { + RoutingInfo = null; + break; + } + SqlClientEventSource.Log.TryTraceEvent(" Routed to {0}", serverInfo.ExtendedServerName); if (routingAttempts > MaxNumberOfRedirectRoute) { @@ -1879,6 +1887,14 @@ TimeoutTimer timeout int routingAttempts = 0; while (RoutingInfo != null) { + // Check if we received enhanced routing info, but not the ack for the feature. + // In this case, we should ignore the routing info and connect to the current server. + if (!string.IsNullOrEmpty(RoutingInfo.DatabaseName) && !IsEnhancedRoutingSupportEnabled) + { + RoutingInfo = null; + continue; + } + if (routingAttempts > MaxNumberOfRedirectRoute) { throw SQL.ROR_RecursiveRoutingNotSupported(this, MaxNumberOfRedirectRoute); @@ -2723,7 +2739,7 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) internal void OnFeatureExtAck(int featureId, byte[] data) { - if (RoutingInfo != null && featureId != TdsEnums.FEATUREEXT_SQLDNSCACHING) + if (RoutingInfo != null && featureId != TdsEnums.FEATUREEXT_SQLDNSCACHING && featureId != TdsEnums.FEATUREEXT_ENHANCEDROUTINGSUPPORT) { return; } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 8388133898..857aca2f81 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -1648,6 +1648,14 @@ private void LoginNoFailover(ServerInfo serverInfo, if (RoutingInfo != null) { + // Check if we received enhanced routing info, but not the ack for the feature. + // In this case, we should ignore the routing info and connect to the current server. + if (!string.IsNullOrEmpty(RoutingInfo.DatabaseName) && !IsEnhancedRoutingSupportEnabled) + { + RoutingInfo = null; + break; + } + SqlClientEventSource.Log.TryTraceEvent(" Routed to {0}", serverInfo.ExtendedServerName); if (routingAttempts > MaxNumberOfRedirectRoute) { @@ -1933,6 +1941,14 @@ TimeoutTimer timeout int routingAttempts = 0; while (RoutingInfo != null) { + // Check if we received enhanced routing info, but not the ack for the feature. + // In this case, we should ignore the routing info and connect to the current server. + if (!string.IsNullOrEmpty(RoutingInfo.DatabaseName) && !IsEnhancedRoutingSupportEnabled) + { + RoutingInfo = null; + continue; + } + if (routingAttempts > MaxNumberOfRedirectRoute) { throw SQL.ROR_RecursiveRoutingNotSupported(this, MaxNumberOfRedirectRoute); @@ -2766,7 +2782,7 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) internal void OnFeatureExtAck(int featureId, byte[] data) { - if (RoutingInfo != null && featureId != TdsEnums.FEATUREEXT_SQLDNSCACHING) + if (RoutingInfo != null && featureId != TdsEnums.FEATUREEXT_SQLDNSCACHING && featureId != TdsEnums.FEATUREEXT_ENHANCEDROUTINGSUPPORT) { return; } diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionEnhancedRoutingTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionEnhancedRoutingTests.cs index 7852f91f31..afd30e13a8 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionEnhancedRoutingTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionEnhancedRoutingTests.cs @@ -116,28 +116,24 @@ public void ServerIgnoresEnhancedRoutingRequest() server.Start(); string routingDatabaseName = Guid.NewGuid().ToString(); - bool clientProvidedCorrectDatabase = false; - server.OnLogin7Validated = loginToken => - { - clientProvidedCorrectDatabase = null == loginToken.Database; - }; RoutingTdsServer router = new( new RoutingTdsServerArguments() { RoutingTCPHost = "localhost", RoutingTCPPort = (ushort)server.EndPoint.Port, + RoutingDatabaseName = routingDatabaseName, RequireReadOnly = false }); router.Start(); router.EnhancedRoutingBehavior = FeatureExtensionBehavior.DoNotAcknowledge; - string connectionString = (new SqlConnectionStringBuilder() + string connectionString = new SqlConnectionStringBuilder() { DataSource = $"localhost,{router.EndPoint.Port}", Encrypt = false, ConnectTimeout = 10000 - }).ConnectionString; + }.ConnectionString; // Act using SqlConnection connection = new(connectionString); @@ -145,12 +141,11 @@ public void ServerIgnoresEnhancedRoutingRequest() // Assert Assert.Equal(ConnectionState.Open, connection.State); - Assert.Equal($"localhost,{server.EndPoint.Port}", ((SqlInternalConnectionTds)connection.InnerConnection).RoutingDestination); + Assert.Null(((SqlInternalConnectionTds)connection.InnerConnection).RoutingDestination); Assert.Equal("master", connection.Database); - Assert.True(clientProvidedCorrectDatabase); Assert.Equal(1, router.PreLoginCount); - Assert.Equal(1, server.PreLoginCount); + Assert.Equal(0, server.PreLoginCount); } [Fact] @@ -161,28 +156,24 @@ public void ServerRejectsEnhancedRoutingRequest() server.Start(); string routingDatabaseName = Guid.NewGuid().ToString(); - bool clientProvidedCorrectDatabase = false; - server.OnLogin7Validated = loginToken => - { - clientProvidedCorrectDatabase = null == loginToken.Database; - }; RoutingTdsServer router = new( new RoutingTdsServerArguments() { RoutingTCPHost = "localhost", RoutingTCPPort = (ushort)server.EndPoint.Port, + RoutingDatabaseName = routingDatabaseName, RequireReadOnly = false }); router.Start(); router.EnhancedRoutingBehavior = FeatureExtensionBehavior.Disabled; - string connectionString = (new SqlConnectionStringBuilder() + string connectionString = new SqlConnectionStringBuilder() { DataSource = $"localhost,{router.EndPoint.Port}", Encrypt = false, ConnectTimeout = 10000 - }).ConnectionString; + }.ConnectionString; // Act using SqlConnection connection = new(connectionString); @@ -190,11 +181,10 @@ public void ServerRejectsEnhancedRoutingRequest() // Assert Assert.Equal(ConnectionState.Open, connection.State); - Assert.Equal($"localhost,{server.EndPoint.Port}", ((SqlInternalConnectionTds)connection.InnerConnection).RoutingDestination); + Assert.Null(((SqlInternalConnectionTds)connection.InnerConnection).RoutingDestination); Assert.Equal("master", connection.Database); - Assert.True(clientProvidedCorrectDatabase); Assert.Equal(1, router.PreLoginCount); - Assert.Equal(1, server.PreLoginCount); + Assert.Equal(0, server.PreLoginCount); } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServer.cs index cdfc754e49..b34e682d2a 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServer.cs @@ -196,6 +196,28 @@ protected override TDSMessageCollection OnAuthenticationCompleted(ITDSServerSess targetMessage.Insert(insertIndex, routingToken); } + if (EnhancedRoutingBehavior != FeatureExtensionBehavior.DoNotAcknowledge) + { + TDSMessage targetMessage = responseMessageCollection[0]; + + // Create the option data + byte[] data = EnhancedRoutingBehavior == FeatureExtensionBehavior.Enabled ? [1] : [0]; + TDSFeatureExtAckGenericOption enhancedRoutingSupportOption = new TDSFeatureExtAckGenericOption(TDSFeatureID.EnhancedRoutingSupport, (uint)data.Length, data); + + TDSFeatureExtAckToken featureExtAckToken = new TDSFeatureExtAckToken(enhancedRoutingSupportOption); + // Add it before DONE token if possible, but simplest is to add to end and let the sorting logic handle it or just append + // Ideally, find DONE token and insert before it + int doneIndex = targetMessage.FindIndex(t => t is TDSDoneToken); + if (doneIndex >= 0) + { + targetMessage.Insert(doneIndex, featureExtAckToken); + } + else + { + targetMessage.Add(featureExtAckToken); + } + } + return responseMessageCollection; } From e68c0aace8be1f3d971dce1adc3d63bb6d730c89 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Thu, 29 Jan 2026 15:27:54 -0800 Subject: [PATCH 2/3] Update tests to reduce redundancy. --- .../ConnectionEnhancedRoutingTests.cs | 208 ++++++------------ 1 file changed, 72 insertions(+), 136 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionEnhancedRoutingTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionEnhancedRoutingTests.cs index afd30e13a8..e0cec1110f 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionEnhancedRoutingTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionEnhancedRoutingTests.cs @@ -16,127 +16,59 @@ namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests; [Collection("SimulatedServerTests")] public class ConnectionEnhancedRoutingTests { - [Fact] - public void RoutedConnection() + /// + /// Tests that a connection is routed to the target server when enhanced routing is enabled. + /// Uses Theory to test both sync and async code paths. + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task RoutedConnection(bool useAsync) { // Arrange - using TdsServer server = new(new()); - server.Start(); + using TestRoutingServers servers = new(FeatureExtensionBehavior.Enabled); - string routingDatabaseName = Guid.NewGuid().ToString(); bool clientProvidedCorrectDatabase = false; - server.OnLogin7Validated = loginToken => + servers.TargetServer.OnLogin7Validated = loginToken => { - clientProvidedCorrectDatabase = routingDatabaseName == loginToken.Database; + clientProvidedCorrectDatabase = servers.RoutingDatabaseName == loginToken.Database; }; - RoutingTdsServer router = new( - new RoutingTdsServerArguments() - { - RoutingTCPHost = "localhost", - RoutingTCPPort = (ushort)server.EndPoint.Port, - RoutingDatabaseName = routingDatabaseName, - RequireReadOnly = false - }); - router.Start(); - router.EnhancedRoutingBehavior = FeatureExtensionBehavior.Enabled; - - string connectionString = (new SqlConnectionStringBuilder() - { - DataSource = $"localhost,{router.EndPoint.Port}", - Encrypt = false, - ConnectTimeout = 10000 - }).ConnectionString; - // Act - using SqlConnection connection = new(connectionString); - connection.Open(); - - // Assert - Assert.Equal(ConnectionState.Open, connection.State); - Assert.Equal($"localhost,{server.EndPoint.Port}", ((SqlInternalConnectionTds)connection.InnerConnection).RoutingDestination); - Assert.Equal(routingDatabaseName, connection.Database); - Assert.True(clientProvidedCorrectDatabase); - - Assert.Equal(1, router.PreLoginCount); - Assert.Equal(1, server.PreLoginCount); - } - - [Fact] - public async Task RoutedAsyncConnection() - { - // Arrange - using TdsServer server = new(new()); - server.Start(); - - string routingDatabaseName = Guid.NewGuid().ToString(); - bool clientProvidedCorrectDatabase = false; - server.OnLogin7Validated = loginToken => + using SqlConnection connection = new(servers.ConnectionString); + if (useAsync) { - clientProvidedCorrectDatabase = routingDatabaseName == loginToken.Database; - }; - - RoutingTdsServer router = new( - new RoutingTdsServerArguments() - { - RoutingTCPHost = "localhost", - RoutingTCPPort = (ushort)server.EndPoint.Port, - RoutingDatabaseName = routingDatabaseName, - RequireReadOnly = false - }); - router.Start(); - router.EnhancedRoutingBehavior = FeatureExtensionBehavior.Enabled; - - string connectionString = (new SqlConnectionStringBuilder() + await connection.OpenAsync(); + } + else { - DataSource = $"localhost,{router.EndPoint.Port}", - Encrypt = false, - ConnectTimeout = 10000 - }).ConnectionString; - - // Act - using SqlConnection connection = new(connectionString); - await connection.OpenAsync(); + connection.Open(); + } // Assert Assert.Equal(ConnectionState.Open, connection.State); - Assert.Equal($"localhost,{server.EndPoint.Port}", ((SqlInternalConnectionTds)connection.InnerConnection).RoutingDestination); - Assert.Equal(routingDatabaseName, connection.Database); + Assert.Equal($"localhost,{servers.TargetServer.EndPoint.Port}", ((SqlInternalConnectionTds)connection.InnerConnection).RoutingDestination); + Assert.Equal(servers.RoutingDatabaseName, connection.Database); Assert.True(clientProvidedCorrectDatabase); - Assert.Equal(1, router.PreLoginCount); - Assert.Equal(1, server.PreLoginCount); + Assert.Equal(1, servers.Router.PreLoginCount); + Assert.Equal(1, servers.TargetServer.PreLoginCount); } - [Fact] - public void ServerIgnoresEnhancedRoutingRequest() + /// + /// Tests that a connection is NOT routed when the server does not acknowledge the enhanced routing feature + /// or has it disabled. Covers both DoNotAcknowledge and Disabled behaviors. + /// + [Theory] + [InlineData(FeatureExtensionBehavior.DoNotAcknowledge)] + [InlineData(FeatureExtensionBehavior.Disabled)] + public void ServerDoesNotRoute(FeatureExtensionBehavior behavior) { // Arrange - using TdsServer server = new(new()); - server.Start(); - - string routingDatabaseName = Guid.NewGuid().ToString(); - - RoutingTdsServer router = new( - new RoutingTdsServerArguments() - { - RoutingTCPHost = "localhost", - RoutingTCPPort = (ushort)server.EndPoint.Port, - RoutingDatabaseName = routingDatabaseName, - RequireReadOnly = false - }); - router.Start(); - router.EnhancedRoutingBehavior = FeatureExtensionBehavior.DoNotAcknowledge; - - string connectionString = new SqlConnectionStringBuilder() - { - DataSource = $"localhost,{router.EndPoint.Port}", - Encrypt = false, - ConnectTimeout = 10000 - }.ConnectionString; + using TestRoutingServers servers = new(behavior); // Act - using SqlConnection connection = new(connectionString); + using SqlConnection connection = new(servers.ConnectionString); connection.Open(); // Assert @@ -144,47 +76,51 @@ public void ServerIgnoresEnhancedRoutingRequest() Assert.Null(((SqlInternalConnectionTds)connection.InnerConnection).RoutingDestination); Assert.Equal("master", connection.Database); - Assert.Equal(1, router.PreLoginCount); - Assert.Equal(0, server.PreLoginCount); + Assert.Equal(1, servers.Router.PreLoginCount); + Assert.Equal(0, servers.TargetServer.PreLoginCount); } - [Fact] - public void ServerRejectsEnhancedRoutingRequest() + /// + /// Helper class that encapsulates the setup of a routing TDS server and target TDS server + /// for enhanced routing tests. + /// + private sealed class TestRoutingServers : IDisposable { - // Arrange - using TdsServer server = new(new()); - server.Start(); - - string routingDatabaseName = Guid.NewGuid().ToString(); + public TdsServer TargetServer { get; } + public RoutingTdsServer Router { get; } + public string RoutingDatabaseName { get; } + public string ConnectionString { get; } - RoutingTdsServer router = new( - new RoutingTdsServerArguments() - { - RoutingTCPHost = "localhost", - RoutingTCPPort = (ushort)server.EndPoint.Port, - RoutingDatabaseName = routingDatabaseName, - RequireReadOnly = false - }); - router.Start(); - router.EnhancedRoutingBehavior = FeatureExtensionBehavior.Disabled; - - string connectionString = new SqlConnectionStringBuilder() + public TestRoutingServers(FeatureExtensionBehavior enhancedRoutingBehavior) { - DataSource = $"localhost,{router.EndPoint.Port}", - Encrypt = false, - ConnectTimeout = 10000 - }.ConnectionString; - - // Act - using SqlConnection connection = new(connectionString); - connection.Open(); - - // Assert - Assert.Equal(ConnectionState.Open, connection.State); - Assert.Null(((SqlInternalConnectionTds)connection.InnerConnection).RoutingDestination); - Assert.Equal("master", connection.Database); + RoutingDatabaseName = Guid.NewGuid().ToString(); + + TargetServer = new TdsServer(new()); + TargetServer.Start(); + + Router = new RoutingTdsServer( + new RoutingTdsServerArguments() + { + RoutingTCPHost = "localhost", + RoutingTCPPort = (ushort)TargetServer.EndPoint.Port, + RoutingDatabaseName = RoutingDatabaseName, + RequireReadOnly = false + }); + Router.Start(); + Router.EnhancedRoutingBehavior = enhancedRoutingBehavior; + + ConnectionString = new SqlConnectionStringBuilder() + { + DataSource = $"localhost,{Router.EndPoint.Port}", + Encrypt = false, + ConnectTimeout = 10000 + }.ConnectionString; + } - Assert.Equal(1, router.PreLoginCount); - Assert.Equal(0, server.PreLoginCount); + public void Dispose() + { + Router?.Dispose(); + TargetServer?.Dispose(); + } } } From dc948acb726622c5a46d68dba0b04ee7b969a8e5 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Thu, 29 Jan 2026 15:51:05 -0800 Subject: [PATCH 3/3] Remove duplicate feature ext ack. --- .../tools/TDS/TDS.Servers/RoutingTdsServer.cs | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServer.cs index b34e682d2a..cdfc754e49 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServer.cs @@ -196,28 +196,6 @@ protected override TDSMessageCollection OnAuthenticationCompleted(ITDSServerSess targetMessage.Insert(insertIndex, routingToken); } - if (EnhancedRoutingBehavior != FeatureExtensionBehavior.DoNotAcknowledge) - { - TDSMessage targetMessage = responseMessageCollection[0]; - - // Create the option data - byte[] data = EnhancedRoutingBehavior == FeatureExtensionBehavior.Enabled ? [1] : [0]; - TDSFeatureExtAckGenericOption enhancedRoutingSupportOption = new TDSFeatureExtAckGenericOption(TDSFeatureID.EnhancedRoutingSupport, (uint)data.Length, data); - - TDSFeatureExtAckToken featureExtAckToken = new TDSFeatureExtAckToken(enhancedRoutingSupportOption); - // Add it before DONE token if possible, but simplest is to add to end and let the sorting logic handle it or just append - // Ideally, find DONE token and insert before it - int doneIndex = targetMessage.FindIndex(t => t is TDSDoneToken); - if (doneIndex >= 0) - { - targetMessage.Insert(doneIndex, featureExtAckToken); - } - else - { - targetMessage.Add(featureExtAckToken); - } - } - return responseMessageCollection; }