From 4c0acd3d40baf7dabab43c9ed48efc8c1385f8b4 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Mon, 29 Sep 2025 13:04:00 -0700 Subject: [PATCH 01/20] Add feature extension, request by default, process ack. --- .../SqlClient/SqlInternalConnectionTds.cs | 21 +++++++++++++++++-- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 18 ++++++++++++++++ .../SqlClient/SqlInternalConnectionTds.cs | 19 ++++++++++++++++- .../src/Microsoft/Data/SqlClient/TdsEnums.cs | 4 +++- 4 files changed, 58 insertions(+), 4 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 6eedfd2fa9..3bb8e8857f 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 @@ -208,11 +208,14 @@ internal bool IsDNSCachingBeforeRedirectSupported // Json Support Flag internal bool IsJsonSupportEnabled = false; + // Vector Support Flag + internal bool IsVectorSupportEnabled = false; + // User Agent Flag internal bool IsUserAgentEnabled = true; - // Vector Support Flag - internal bool IsVectorSupportEnabled = false; + // Enhanced Routing Support Flag + internal bool IsEnhancedRoutingSupportEnabled = false; // TCE flags internal byte _tceVersionSupported; @@ -1410,6 +1413,7 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, requestedFeatures |= TdsEnums.FeatureExtension.SQLDNSCaching; requestedFeatures |= TdsEnums.FeatureExtension.JsonSupport; requestedFeatures |= TdsEnums.FeatureExtension.VectorSupport; + requestedFeatures |= TdsEnums.FeatureExtension.EnhancedRoutingSupport; #if DEBUG requestedFeatures |= TdsEnums.FeatureExtension.UserAgent; @@ -3014,6 +3018,19 @@ internal void OnFeatureExtAck(int featureId, byte[] data) break; } + case TdsEnums.FEATUREEXT_ENHANCEDROUTINGSUPPORT: + { + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for ENHANCEDROUTINGSUPPORT", ObjectID); + if (data.Length != 1) + { + SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown token for ENHANCEDROUTINGSUPPORT", ObjectID); + throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); + } + + IsEnhancedRoutingSupportEnabled = data[0] == 1; + break; + } + default: { // Unknown feature ack diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index 5809f304fe..fcc34e89d2 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -8738,6 +8738,19 @@ internal int WriteVectorSupportFeatureRequest(bool write) return len; } + internal static int WriteEnhancedRoutingSupportFeatureRequest(bool write) + { + const int len = 1; + + if (write) + { + // Write Feature ID + _physicalStateObj.WriteByte(TdsEnums.FEATUREEXT_ENHANCEDROUTINGSUPPORT); + } + + return len; + } + /// /// Writes the User Agent feature request to the physical state object. /// The request includes the feature ID, feature data length, version number and encoded JSON payload. @@ -9109,6 +9122,11 @@ private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, length += WriteVectorSupportFeatureRequest(write); } + if ((requestedFeatures & TdsEnums.FeatureExtension.EnhancedRoutingSupport) != 0) + { + length += WriteEnhancedRoutingSupportFeatureRequest(write); + } + length++; // for terminator if (write) { 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 9df86e4fd2..388190d357 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 @@ -215,6 +215,9 @@ internal bool IsDNSCachingBeforeRedirectSupported // User Agent Flag internal bool IsUserAgentEnabled = true; + // Enhanced Routing Support Flag + internal bool IsEnhancedRoutingSupportEnabled = false; + // TCE flags internal byte _tceVersionSupported; @@ -1418,8 +1421,9 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, requestedFeatures |= TdsEnums.FeatureExtension.SQLDNSCaching; requestedFeatures |= TdsEnums.FeatureExtension.JsonSupport; requestedFeatures |= TdsEnums.FeatureExtension.VectorSupport; + requestedFeatures |= TdsEnums.FeatureExtension.EnhancedRoutingSupport; - #if DEBUG + #if DEBUG requestedFeatures |= TdsEnums.FeatureExtension.UserAgent; #endif @@ -3057,6 +3061,19 @@ internal void OnFeatureExtAck(int featureId, byte[] data) break; } + case TdsEnums.FEATUREEXT_ENHANCEDROUTINGSUPPORT: + { + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received feature extension acknowledgement for ENHANCEDROUTINGSUPPORT", ObjectID); + if (data.Length != 1) + { + SqlClientEventSource.Log.TryTraceEvent(" {0}, Unknown token for ENHANCEDROUTINGSUPPORT", ObjectID); + throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); + } + + IsEnhancedRoutingSupportEnabled = data[0] == 1; + break; + } + default: { // Unknown feature ack diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs index 3113e19625..100d6c0742 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -241,8 +241,9 @@ public enum EnvChangeType : byte public const byte FEATUREEXT_SQLDNSCACHING = 0x0B; public const byte FEATUREEXT_JSONSUPPORT = 0x0D; public const byte FEATUREEXT_VECTORSUPPORT = 0x0E; + public const byte FEATUREEXT_ENHANCEDROUTINGSUPPORT = 0x0F; // TODO: re-verify if this byte competes with another feature - public const byte FEATUREEXT_USERAGENT = 0x0F; + public const byte FEATUREEXT_USERAGENT = 0x10; [Flags] public enum FeatureExtension : uint @@ -258,6 +259,7 @@ public enum FeatureExtension : uint SQLDNSCaching = 1 << (TdsEnums.FEATUREEXT_SQLDNSCACHING - 1), JsonSupport = 1 << (TdsEnums.FEATUREEXT_JSONSUPPORT - 1), VectorSupport = 1 << (TdsEnums.FEATUREEXT_VECTORSUPPORT - 1), + EnhancedRoutingSupport = 1 << (TdsEnums.FEATUREEXT_ENHANCEDROUTINGSUPPORT - 1), UserAgent = 1 << (TdsEnums.FEATUREEXT_USERAGENT - 1) } From 21363705c976bd5dcf4b6102aa4af06e7cfe7d3d Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Mon, 29 Sep 2025 16:08:01 -0700 Subject: [PATCH 02/20] Add support to netfx. Add simulated server tests. --- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 12 +- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 26 ++++ .../FeatureExtensionNegotiationTests.cs | 114 ++++++++++++++++++ .../TDS/TDS.EndPoint/ITDSServerSession.cs | 5 + .../FeatureExtensionEnablementTriState.cs | 14 +++ .../tools/TDS/TDS.Servers/GenericTdsServer.cs | 44 ++++++- .../TDS.Servers/GenericTdsServerSession.cs | 5 + .../tools/TDS/TDS.Servers/TDS.Servers.csproj | 22 ---- .../tests/tools/TDS/TDS/TDSFeatureID.cs | 5 + 9 files changed, 222 insertions(+), 25 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtensionNegotiationTests.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FeatureExtensionEnablementTriState.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index fcc34e89d2..658223b29a 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -8738,14 +8738,22 @@ internal int WriteVectorSupportFeatureRequest(bool write) return len; } - internal static int WriteEnhancedRoutingSupportFeatureRequest(bool write) + /// + /// Writes the Enhanced Routing Support feature request to the physical state object. + /// The request does not contain a payload. + /// + /// If true, writes the feature request to the physical state object. + /// The length of the feature request in bytes. + internal int WriteEnhancedRoutingSupportFeatureRequest(bool write) { - const int len = 1; + const int len = 5; if (write) { // Write Feature ID _physicalStateObj.WriteByte(TdsEnums.FEATUREEXT_ENHANCEDROUTINGSUPPORT); + // We don't send any data + WriteInt(0, _physicalStateObj); } return len; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index 4af0129a11..7781bbc97b 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -8798,6 +8798,27 @@ internal int WriteVectorSupportFeatureRequest(bool write) return len; } + /// + /// Writes the Enhanced Routing Support feature request to the physical state object. + /// The request does not contain a payload. + /// + /// If true, writes the feature request to the physical state object. + /// The length of the feature request in bytes. + internal int WriteEnhancedRoutingSupportFeatureRequest(bool write) + { + const int len = 5; + + if (write) + { + // Write Feature ID + _physicalStateObj.WriteByte(TdsEnums.FEATUREEXT_ENHANCEDROUTINGSUPPORT); + // We don't send any data + WriteInt(0, _physicalStateObj); + } + + return len; + } + /// /// Writes the User Agent feature request to the physical state object. /// The request includes the feature ID, feature data length, version number and encoded JSON payload. @@ -9178,6 +9199,11 @@ private int ApplyFeatureExData(TdsEnums.FeatureExtension requestedFeatures, length += WriteVectorSupportFeatureRequest(write); } + if ((requestedFeatures & TdsEnums.FeatureExtension.EnhancedRoutingSupport) != 0) + { + length += WriteEnhancedRoutingSupportFeatureRequest(write); + } + length++; // for terminator if (write) { diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtensionNegotiationTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtensionNegotiationTests.cs new file mode 100644 index 0000000000..8486a19a75 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtensionNegotiationTests.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using Microsoft.SqlServer.TDS; +using Microsoft.SqlServer.TDS.Login7; +using Microsoft.SqlServer.TDS.Servers; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests; + +[Collection("SimulatedServerTests")] +public class FeatureExtensionNegotiationTests : IClassFixture +{ + TdsServer server; + string connectionString; + + public FeatureExtensionNegotiationTests(SimulatedServerFixture fixture) + { + server = fixture.TdsServer; + SqlConnectionStringBuilder builder = new() + { + DataSource = $"localhost,{server.EndPoint.Port}", + Encrypt = SqlConnectionEncryptOption.Optional, + Pooling = false + }; + connectionString = builder.ConnectionString; + } + + [Fact] + public void EnhancedRouting_EnabledByServer_ShouldBeEnabled() + { + // Arrange + server.EnableEnhancedRouting = FeatureExtensionEnablementTriState.Enabled; + + bool clientFeatureExtensionFound = false; + server.OnLogin7Validated = loginToken => + { + var token = loginToken.FeatureExt + .OfType() + .FirstOrDefault(t => t.FeatureID == TDSFeatureID.EnhancedRoutingSupport); + + + // Test should fail if no UserAgent FE token is found + Assert.NotNull(token); + + Assert.Equal((byte)TDSFeatureID.EnhancedRoutingSupport, (byte)token.FeatureID); + + clientFeatureExtensionFound = true; + }; + + using SqlConnection sqlConnection = new(connectionString); + + // Act + sqlConnection.Open(); + + + // Assert + Assert.True(clientFeatureExtensionFound, "Client did not request the Enhanced Routing feature extension."); + Assert.True(((SqlInternalConnectionTds)sqlConnection.InnerConnection).IsEnhancedRoutingSupportEnabled, + "Enhanced Routing should be enabled when the server supports it."); + } + + [Fact] + public void EnhancedRouting_DisabledByServer_ShouldBeDisabled() + { + // Arrange + server.EnableEnhancedRouting = FeatureExtensionEnablementTriState.Disabled; + + using SqlConnection sqlConnection = new(connectionString); + + // Act + sqlConnection.Open(); + + + // Assert + Assert.False(((SqlInternalConnectionTds)sqlConnection.InnerConnection).IsEnhancedRoutingSupportEnabled, + "Enhanced Routing should be disabled when the server does not support it."); + } + + [Fact] + public void EnhancedRouting_NotAcknowledgedByServer_ShouldBeDisabled() + { + // Arrange + server.EnableEnhancedRouting = FeatureExtensionEnablementTriState.DoNotAcknowledge; + + using SqlConnection sqlConnection = new(connectionString); + + // Act + sqlConnection.Open(); + + + // Assert + Assert.False(((SqlInternalConnectionTds)sqlConnection.InnerConnection).IsEnhancedRoutingSupportEnabled, + "Enhanced Routing should be disabled when the server does not acknowledge it."); + } +} + +public class SimulatedServerFixture : IDisposable +{ + public SimulatedServerFixture() { + TdsServer = new TdsServer(); + TdsServer.Start(); + } + + public void Dispose() + { + TdsServer.Dispose(); + } + + public TdsServer TdsServer { get; private set; } +} diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs index 9b5b7804b4..1ac499d229 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.EndPoint/ITDSServerSession.cs @@ -93,5 +93,10 @@ public interface ITDSServerSession /// Indicates whether the client supports Vector column type /// bool IsVectorSupportEnabled { get; set; } + + /// + /// Indicates whether the client supports Enhanced Routing + /// + bool IsEnhancedRoutingSupportEnabled { get; set; } } } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FeatureExtensionEnablementTriState.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FeatureExtensionEnablementTriState.cs new file mode 100644 index 0000000000..67acbfd5c8 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/FeatureExtensionEnablementTriState.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +namespace Microsoft.SqlServer.TDS.Servers +{ + public enum FeatureExtensionEnablementTriState + { + Disabled, + Enabled, + DoNotAcknowledge + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs index c17726fb8b..0e867ae1cf 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServer.cs @@ -117,6 +117,8 @@ public GenericTdsServer(T arguments, QueryEngine queryEngine) /// public byte ServerSupportedVectorFeatureExtVersion { get; set; } = DefaultSupportedVectorFeatureExtVersion; + public FeatureExtensionEnablementTriState EnableEnhancedRouting { get; set; } = FeatureExtensionEnablementTriState.Disabled; + public OnAuthenticationCompletedDelegate OnAuthenticationResponseCompleted { private get; set; } public OnLogin7ValidatedDelegate OnLogin7Validated { private get; set; } @@ -315,6 +317,12 @@ public virtual TDSMessageCollection OnLogin7Request(ITDSServerSession session, T break; } + case TDSFeatureID.EnhancedRoutingSupport: + { + session.IsEnhancedRoutingSupportEnabled = true; + break; + } + default: { // Do nothing @@ -681,6 +689,40 @@ protected virtual TDSMessageCollection OnAuthenticationCompleted(ITDSServerSessi } } + if (session.IsEnhancedRoutingSupportEnabled && + EnableEnhancedRouting != FeatureExtensionEnablementTriState.DoNotAcknowledge) + { + // Create ack data (1 byte: IsEnabled) + byte[] data = new byte[1]; + + if (EnableEnhancedRouting == FeatureExtensionEnablementTriState.Enabled) + { + data[0] = 1; + } + else + { + data[0] = 0; + } + + // Create enhanced routing support as a generic feature extension option + TDSFeatureExtAckGenericOption enhancedRoutingSupportOption = new TDSFeatureExtAckGenericOption(TDSFeatureID.EnhancedRoutingSupport, (uint)data.Length, data); + + // Look for feature extension token + TDSFeatureExtAckToken featureExtAckToken = (TDSFeatureExtAckToken)responseMessage.Where(t => t is TDSFeatureExtAckToken).FirstOrDefault(); + + if (featureExtAckToken == null) + { + // Create feature extension ack token + featureExtAckToken = new TDSFeatureExtAckToken(enhancedRoutingSupportOption); + responseMessage.Add(featureExtAckToken); + } + else + { + // Update the existing token + featureExtAckToken.Options.Add(enhancedRoutingSupportOption); + } + } + if (!string.IsNullOrEmpty(Arguments.FailoverPartner)) { envChange = new TDSEnvChangeToken(TDSEnvChangeTokenType.RealTimeLogShipping, Arguments.FailoverPartner); @@ -893,7 +935,7 @@ private byte[] _GenerateRandomBytes(int count) { rng.GetBytes(randomBytes); } - + return randomBytes; } diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServerSession.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServerSession.cs index 2730fa02df..cf69e13588 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServerSession.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/GenericTdsServerSession.cs @@ -124,6 +124,11 @@ public class GenericTdsServerSession : ITDSServerSession /// public bool IsVectorSupportEnabled { get; set; } + /// + /// Indicates whether this session supports enhanced routing + /// + public bool IsEnhancedRoutingSupportEnabled { get; set; } + #region Session Options /// diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDS.Servers.csproj b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDS.Servers.csproj index c689554310..fa7f3fff4d 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDS.Servers.csproj +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/TDS.Servers.csproj @@ -5,31 +5,9 @@ {978063D3-FBB5-4E10-8C45-67C90BE1B931} netstandard2.0 false - false $(ObjFolder)$(Configuration).$(Platform)\$(AssemblyName) $(BinFolder)$(Configuration).$(Platform)\$(AssemblyName) - - - - - - - - - - - - - - - - - - - - - TDS.EndPoint diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDSFeatureID.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDSFeatureID.cs index 6bb6fbc8d2..958e879e52 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDSFeatureID.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDSFeatureID.cs @@ -29,6 +29,11 @@ public enum TDSFeatureID : byte /// VectorSupport = 0x0E, + /// + /// Enhance Routing Support + /// + EnhancedRoutingSupport = 0x0F, + /// /// End of the list /// From 40af1cfb143230afac28fa83f741e70edb412256 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 11:29:47 -0700 Subject: [PATCH 03/20] review changes --- .../SqlClient/SqlInternalConnectionTds.cs | 1 + .../SqlClient/SqlInternalConnectionTds.cs | 1 + .../src/Microsoft/Data/SqlClient/TdsParser.cs | 8 ++--- .../FeatureExtensionNegotiationTests.cs | 36 +++++++++---------- 4 files changed, 23 insertions(+), 23 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 3bb8e8857f..d07ec3c7c3 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 @@ -3027,6 +3027,7 @@ internal void OnFeatureExtAck(int featureId, byte[] data) throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); } + // A value of 1 indicates that the server supports the feature. IsEnhancedRoutingSupportEnabled = data[0] == 1; break; } 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 388190d357..ee7b04e0a5 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 @@ -3070,6 +3070,7 @@ internal void OnFeatureExtAck(int featureId, byte[] data) throw SQL.ParsingError(ParsingErrorState.CorruptedTdsStream); } + // A value of 1 indicates that the server supports the feature. IsEnhancedRoutingSupportEnabled = data[0] == 1; break; } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index 7781bbc97b..aef45e0b3d 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -8799,11 +8799,11 @@ internal int WriteVectorSupportFeatureRequest(bool write) } /// - /// Writes the Enhanced Routing Support feature request to the physical state object. - /// The request does not contain a payload. + /// Writes the ENHANCEDROUTINGSUPPORT extension to the physical state object. + /// The extension does not contain a payload. /// - /// If true, writes the feature request to the physical state object. - /// The length of the feature request in bytes. + /// If true, writes the feature extension to the physical state object. + /// The length of the feature extension in bytes. internal int WriteEnhancedRoutingSupportFeatureRequest(bool write) { const int len = 5; diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtensionNegotiationTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtensionNegotiationTests.cs index 8486a19a75..8c309662c6 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtensionNegotiationTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/FeatureExtensionNegotiationTests.cs @@ -14,29 +14,30 @@ namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests; [Collection("SimulatedServerTests")] public class FeatureExtensionNegotiationTests : IClassFixture { - TdsServer server; - string connectionString; + private TdsServer _server; + private string _connectionString; public FeatureExtensionNegotiationTests(SimulatedServerFixture fixture) { - server = fixture.TdsServer; + _server = fixture.TdsServer; SqlConnectionStringBuilder builder = new() { - DataSource = $"localhost,{server.EndPoint.Port}", + DataSource = $"localhost,{_server.EndPoint.Port}", Encrypt = SqlConnectionEncryptOption.Optional, + // Disable connection pooling to avoid recycling a connection that has already negotiated feature extensions Pooling = false }; - connectionString = builder.ConnectionString; + _connectionString = builder.ConnectionString; } [Fact] public void EnhancedRouting_EnabledByServer_ShouldBeEnabled() { // Arrange - server.EnableEnhancedRouting = FeatureExtensionEnablementTriState.Enabled; + _server.EnableEnhancedRouting = FeatureExtensionEnablementTriState.Enabled; bool clientFeatureExtensionFound = false; - server.OnLogin7Validated = loginToken => + _server.OnLogin7Validated = loginToken => { var token = loginToken.FeatureExt .OfType() @@ -51,50 +52,47 @@ public void EnhancedRouting_EnabledByServer_ShouldBeEnabled() clientFeatureExtensionFound = true; }; - using SqlConnection sqlConnection = new(connectionString); + using SqlConnection sqlConnection = new(_connectionString); // Act sqlConnection.Open(); // Assert - Assert.True(clientFeatureExtensionFound, "Client did not request the Enhanced Routing feature extension."); - Assert.True(((SqlInternalConnectionTds)sqlConnection.InnerConnection).IsEnhancedRoutingSupportEnabled, - "Enhanced Routing should be enabled when the server supports it."); + Assert.True(clientFeatureExtensionFound); + Assert.True(((SqlInternalConnectionTds)sqlConnection.InnerConnection).IsEnhancedRoutingSupportEnabled); } [Fact] public void EnhancedRouting_DisabledByServer_ShouldBeDisabled() { // Arrange - server.EnableEnhancedRouting = FeatureExtensionEnablementTriState.Disabled; + _server.EnableEnhancedRouting = FeatureExtensionEnablementTriState.Disabled; - using SqlConnection sqlConnection = new(connectionString); + using SqlConnection sqlConnection = new(_connectionString); // Act sqlConnection.Open(); // Assert - Assert.False(((SqlInternalConnectionTds)sqlConnection.InnerConnection).IsEnhancedRoutingSupportEnabled, - "Enhanced Routing should be disabled when the server does not support it."); + Assert.False(((SqlInternalConnectionTds)sqlConnection.InnerConnection).IsEnhancedRoutingSupportEnabled); } [Fact] public void EnhancedRouting_NotAcknowledgedByServer_ShouldBeDisabled() { // Arrange - server.EnableEnhancedRouting = FeatureExtensionEnablementTriState.DoNotAcknowledge; + _server.EnableEnhancedRouting = FeatureExtensionEnablementTriState.DoNotAcknowledge; - using SqlConnection sqlConnection = new(connectionString); + using SqlConnection sqlConnection = new(_connectionString); // Act sqlConnection.Open(); // Assert - Assert.False(((SqlInternalConnectionTds)sqlConnection.InnerConnection).IsEnhancedRoutingSupportEnabled, - "Enhanced Routing should be disabled when the server does not acknowledge it."); + Assert.False(((SqlInternalConnectionTds)sqlConnection.InnerConnection).IsEnhancedRoutingSupportEnabled); } } From 924218ae01964a07232047eb196a35c6a37af07a Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 12:01:29 -0700 Subject: [PATCH 04/20] Add enhanced routing to TdsEnums. --- .../src/Microsoft/Data/SqlClient/TdsEnums.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs index 100d6c0742..252fe4b234 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -190,6 +190,7 @@ internal static class TdsEnums public const byte ENV_SPRESETCONNECTIONACK = 18; // SP_Reset_Connection ack public const byte ENV_USERINSTANCE = 19; // User Instance public const byte ENV_ROUTING = 20; // Routing (ROR) information + public const byte ENV_ENHANCEDROUTING = 21; // Enhanced Routing (ROR) information public enum EnvChangeType : byte { From fabad0a17a33b1fd9c55657c944ba030a1fae01e Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 12:01:58 -0700 Subject: [PATCH 05/20] Remove unused envchange enum. --- .../src/Microsoft/Data/SqlClient/TdsEnums.cs | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs index 252fe4b234..e62901defe 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -192,29 +192,6 @@ internal static class TdsEnums public const byte ENV_ROUTING = 20; // Routing (ROR) information public const byte ENV_ENHANCEDROUTING = 21; // Enhanced Routing (ROR) information - public enum EnvChangeType : byte - { - ENVCHANGE_DATABASE = ENV_DATABASE, - ENVCHANGE_LANG = ENV_LANG, - ENVCHANGE_CHARSET = ENV_CHARSET, - ENVCHANGE_PACKETSIZE = ENV_PACKETSIZE, - ENVCHANGE_LOCALEID = ENV_LOCALEID, - ENVCHANGE_COMPFLAGS = ENV_COMPFLAGS, - ENVCHANGE_COLLATION = ENV_COLLATION, - ENVCHANGE_BEGINTRAN = ENV_BEGINTRAN, - ENVCHANGE_COMMITTRAN = ENV_COMMITTRAN, - ENVCHANGE_ROLLBACKTRAN = ENV_ROLLBACKTRAN, - ENVCHANGE_ENLISTDTC = ENV_ENLISTDTC, - ENVCHANGE_DEFECTDTC = ENV_DEFECTDTC, - ENVCHANGE_LOGSHIPNODE = ENV_LOGSHIPNODE, - ENVCHANGE_PROMOTETRANSACTION = ENV_PROMOTETRANSACTION, - ENVCHANGE_TRANSACTIONMANAGERADDRESS = ENV_TRANSACTIONMANAGERADDRESS, - ENVCHANGE_TRANSACTIONENDED = ENV_TRANSACTIONENDED, - ENVCHANGE_SPRESETCONNECTIONACK = ENV_SPRESETCONNECTIONACK, - ENVCHANGE_USERINSTANCE = ENV_USERINSTANCE, - ENVCHANGE_ROUTING = ENV_ROUTING - } - // done status stream bit masks public const int DONE_MORE = 0x0001; // more command results coming public const int DONE_ERROR = 0x0002; // error in command batch From 702ecf1f5d887de7230cde9cd59805f42e64cf44 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 12:55:36 -0700 Subject: [PATCH 06/20] Add database name to RoutingInfo dto. --- .../Data/SqlClient/TdsParserHelperClasses.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs index 75eed599ce..934e375c51 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs @@ -90,11 +90,18 @@ internal class RoutingInfo internal ushort Port { get; private set; } internal string ServerName { get; private set; } - internal RoutingInfo(byte protocol, ushort port, string servername) + /// + /// The DatabaseName property is only used when routing via an EnhancedRouting ENVCHANGE token. + /// It is not used when routing via the normal Routing ENVCHANGE token. + /// + internal string DatabaseName { get; private set; } + + internal RoutingInfo(byte protocol, ushort port, string serverName, string databaseName = null) { Protocol = protocol; Port = port; - ServerName = servername; + ServerName = serverName; + DatabaseName = databaseName; } } From 91bade23791290bb0bdd43e85ecb543a920c1d3c Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 12:56:46 -0700 Subject: [PATCH 07/20] Add new user facing exception message for invalid enhanced routing info. --- .../src/Microsoft/Data/SqlClient/SqlUtil.cs | 9 +++++++++ .../src/Resources/Strings.Designer.cs | 13 ++++++++++++- .../src/Resources/Strings.resx | 3 +++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs index 55e41147b6..aa937efab2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs @@ -1315,6 +1315,15 @@ internal static Exception ROR_InvalidRoutingInfo(SqlInternalConnectionTds intern return exc; } + internal static Exception ROR_InvalidEnhancedRoutingInfo(SqlInternalConnectionTds internalConnection) + { + SqlErrorCollection errors = new SqlErrorCollection(); + errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_InvalidEnhancedRoutingInfo)), "", 0)); + SqlException exc = SqlException.CreateException(errors, null, internalConnection, innerException: null, batchCommand: null); + exc._doNotReconnect = true; + return exc; + } + internal static Exception ROR_TimeoutAfterRoutingInfo(SqlInternalConnectionTds internalConnection) { SqlErrorCollection errors = new SqlErrorCollection(); diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs index 16cf52c86d..d404bcbf2d 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs @@ -11793,7 +11793,18 @@ internal static string SQLROR_InvalidRoutingInfo { return ResourceManager.GetString("SQLROR_InvalidRoutingInfo", resourceCulture); } } - + + /// + /// Looks up a localized string similar to "Invalid enhanced routing information received.". + /// + internal static string SQLROR_InvalidEnhancedRoutingInfo + { + get + { + return ResourceManager.GetString("SQLROR_InvalidEnhancedRoutingInfo", resourceCulture); + } + } + /// /// Looks up a localized string similar to Too many redirections have occurred. Only {0} redirections per login is allowed.. /// diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx index 2eb501cec8..983b896a5f 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx @@ -4359,6 +4359,9 @@ Invalid routing information received. + + Invalid enhanced routing information received. + Server provided routing information, but timeout already expired. From eb8d271bc58f8b5a30d6f4ecc041869f4a7cc21d Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 12:57:57 -0700 Subject: [PATCH 08/20] Add enhanced routing token processing from stream. --- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 179 ++++++++++++++---- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 95 ++++++++++ 2 files changed, 233 insertions(+), 41 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index 658223b29a..948c51b5de 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -3073,51 +3073,63 @@ private TdsOperationStatus TryProcessEnvChange(int tokenLength, TdsParserStateOb break; case TdsEnums.ENV_ROUTING: - ushort newLength; - result = stateObj.TryReadUInt16(out newLength); - if (result != TdsOperationStatus.Done) - { - return result; - } - env._newLength = newLength; - byte protocol; - result = stateObj.TryReadByte(out protocol); - if (result != TdsOperationStatus.Done) { - return result; - } - ushort port; - result = stateObj.TryReadUInt16(out port); - if (result != TdsOperationStatus.Done) - { - return result; - } - ushort serverLen; - result = stateObj.TryReadUInt16(out serverLen); - if (result != TdsOperationStatus.Done) - { - return result; - } - string serverName; - result = stateObj.TryReadString(serverLen, out serverName); - if (result != TdsOperationStatus.Done) - { - return result; - } - env._newRoutingInfo = new RoutingInfo(protocol, port, serverName); - ushort oldLength; - result = stateObj.TryReadUInt16(out oldLength); - if (result != TdsOperationStatus.Done) - { - return result; + ushort newLength; + result = stateObj.TryReadUInt16(out newLength); + if (result != TdsOperationStatus.Done) + { + return result; + } + env._newLength = newLength; + byte protocol; + result = stateObj.TryReadByte(out protocol); + if (result != TdsOperationStatus.Done) + { + return result; + } + ushort port; + result = stateObj.TryReadUInt16(out port); + if (result != TdsOperationStatus.Done) + { + return result; + } + ushort serverLen; + result = stateObj.TryReadUInt16(out serverLen); + if (result != TdsOperationStatus.Done) + { + return result; + } + string serverName; + result = stateObj.TryReadString(serverLen, out serverName); + if (result != TdsOperationStatus.Done) + { + return result; + } + env._newRoutingInfo = new RoutingInfo(protocol, port, serverName); + ushort oldLength; + result = stateObj.TryReadUInt16(out oldLength); + if (result != TdsOperationStatus.Done) + { + return result; + } + result = stateObj.TrySkipBytes(oldLength); + if (result != TdsOperationStatus.Done) + { + return result; + } + env._length = env._newLength + oldLength + 5; // 5=2*sizeof(UInt16)+sizeof(byte) [token+newLength+oldLength] + break; } - result = stateObj.TrySkipBytes(oldLength); - if (result != TdsOperationStatus.Done) + + case TdsEnums.ENV_ENHANCEDROUTING: { - return result; + result = TryProcessEnhancedRoutingToken(stateObj, env); + if (result != TdsOperationStatus.Done) + { + return result; + } + break; } - env._length = env._newLength + oldLength + 5; // 5=2*sizeof(UInt16)+sizeof(byte) [token+newLength+oldLength] - break; default: Debug.Fail("Unknown environment change token: " + env._type); @@ -3130,6 +3142,91 @@ private TdsOperationStatus TryProcessEnvChange(int tokenLength, TdsParserStateOb return TdsOperationStatus.Done; } + /// + /// Reads an enhanced routing token from the stream and populates the provided object. + /// + /// The state object containing the stream. + /// The object to populate. + /// Returns a to indicate the status of the operation. + private TdsOperationStatus TryProcessEnhancedRoutingToken(TdsParserStateObject stateObj, SqlEnvChange env) + { + ushort newLength; + TdsOperationStatus result = stateObj.TryReadUInt16(out newLength); + if (result != TdsOperationStatus.Done) + { + return result; + } + + env._newLength = newLength; + + byte protocol; + result = stateObj.TryReadByte(out protocol); + if (result != TdsOperationStatus.Done) + { + return result; + } + + ushort port; + result = stateObj.TryReadUInt16(out port); + if (result != TdsOperationStatus.Done) + { + return result; + } + + ushort serverLen; + result = stateObj.TryReadUInt16(out serverLen); + if (result != TdsOperationStatus.Done) + { + return result; + } + + string serverName; + result = stateObj.TryReadString(serverLen, out serverName); + if (result != TdsOperationStatus.Done) + { + return result; + } + + ushort databaseLen; + result = stateObj.TryReadUInt16(out databaseLen); + if (result != TdsOperationStatus.Done) + { + return result; + } + + string databaseName; + result = stateObj.TryReadString(databaseLen, out databaseName); + if (result != TdsOperationStatus.Done) + { + return result; + } + + env._newRoutingInfo = new RoutingInfo(protocol, port, serverName, databaseName); + + // The enhanced routing token does not include an old value payload + // read/skip as necessary to clear the required length field and any data. + ushort oldLength; + result = stateObj.TryReadUInt16(out oldLength); + if (result != TdsOperationStatus.Done) + { + return result; + } + + // oldLength should be 0, but skip any data if present + result = stateObj.TrySkipBytes(oldLength); + if (result != TdsOperationStatus.Done) + { + return result; + } + + // Set the total length of the token + // total = headers + size of new value + size of old value + // headers = token id+newLengthHeader+oldLengthHeader = sizeof(byte) + 2*sizeof(UInt16) = 5 + env._length = 5 + env._newLength + oldLength; + + return TdsOperationStatus.Done; + } + private TdsOperationStatus TryReadTwoBinaryFields(SqlEnvChange env, TdsParserStateObject stateObj) { // Used by ProcessEnvChangeToken diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index aef45e0b3d..238b7e354a 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -3120,6 +3120,16 @@ private TdsOperationStatus TryProcessEnvChange(int tokenLength, TdsParserStateOb env._length = env._newLength + oldLength + 5; // 5=2*sizeof(UInt16)+sizeof(byte) [token+newLength+oldLength] break; + case TdsEnums.ENV_ENHANCEDROUTING: + { + result = TryProcessEnhancedRoutingToken(stateObj, env); + if (result != TdsOperationStatus.Done) + { + return result; + } + break; + } + default: Debug.Fail("Unknown environment change token: " + env._type); break; @@ -3131,6 +3141,91 @@ private TdsOperationStatus TryProcessEnvChange(int tokenLength, TdsParserStateOb return TdsOperationStatus.Done; } + /// + /// Reads an enhanced routing token from the stream and populates the provided object. + /// + /// The state object containing the stream. + /// The object to populate. + /// Returns a to indicate the status of the operation. + private TdsOperationStatus TryProcessEnhancedRoutingToken(TdsParserStateObject stateObj, SqlEnvChange env) + { + ushort newLength; + TdsOperationStatus result = stateObj.TryReadUInt16(out newLength); + if (result != TdsOperationStatus.Done) + { + return result; + } + + env._newLength = newLength; + + byte protocol; + result = stateObj.TryReadByte(out protocol); + if (result != TdsOperationStatus.Done) + { + return result; + } + + ushort port; + result = stateObj.TryReadUInt16(out port); + if (result != TdsOperationStatus.Done) + { + return result; + } + + ushort serverLen; + result = stateObj.TryReadUInt16(out serverLen); + if (result != TdsOperationStatus.Done) + { + return result; + } + + string serverName; + result = stateObj.TryReadString(serverLen, out serverName); + if (result != TdsOperationStatus.Done) + { + return result; + } + + ushort databaseLen; + result = stateObj.TryReadUInt16(out databaseLen); + if (result != TdsOperationStatus.Done) + { + return result; + } + + string databaseName; + result = stateObj.TryReadString(databaseLen, out databaseName); + if (result != TdsOperationStatus.Done) + { + return result; + } + + env._newRoutingInfo = new RoutingInfo(protocol, port, serverName, databaseName); + + // The enhanced routing token does not include an old value payload + // read/skip as necessary to clear the required length field and any data. + ushort oldLength; + result = stateObj.TryReadUInt16(out oldLength); + if (result != TdsOperationStatus.Done) + { + return result; + } + + // oldLength should be 0, but skip any data if present + result = stateObj.TrySkipBytes(oldLength); + if (result != TdsOperationStatus.Done) + { + return result; + } + + // Set the total length of the token + // total = headers + size of new value + size of old value + // headers = token id+newLengthHeader+oldLengthHeader = sizeof(byte) + 2*sizeof(UInt16) = 5 + env._length = 5 + env._newLength + oldLength; + + return TdsOperationStatus.Done; + } + private TdsOperationStatus TryReadTwoBinaryFields(SqlEnvChange env, TdsParserStateObject stateObj) { // Used by ProcessEnvChangeToken From 5aff3451ad268b3ed70e2bfd9c46008aeedfb082 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 12:59:08 -0700 Subject: [PATCH 09/20] Add logic to validate and store enhanced routing info on the internal connection. --- .../SqlClient/SqlInternalConnectionTds.cs | 21 +++++++++++++++++++ .../SqlClient/SqlInternalConnectionTds.cs | 21 +++++++++++++++++++ 2 files changed, 42 insertions(+) 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 d07ec3c7c3..60581612be 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 @@ -2281,6 +2281,27 @@ internal void OnEnvChange(SqlEnvChange rec) RoutingInfo = rec._newRoutingInfo; break; + case TdsEnums.ENV_ENHANCEDROUTING: + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received enhanced routing info", ObjectID); + + if (!IsEnhancedRoutingSupportEnabled) + { + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Enhanced routing disabled, ignoring enhanced routing info", ObjectID); + break; + } + + if (string.IsNullOrEmpty(rec._newRoutingInfo.ServerName) || + string.IsNullOrEmpty(rec._newRoutingInfo.DatabaseName) || + rec._newRoutingInfo.Protocol != 0 || + rec._newRoutingInfo.Port == 0) + { + throw SQL.ROR_InvalidEnhancedRoutingInfo(this); + } + + RoutingInfo = rec._newRoutingInfo; + + break; + default: Debug.Fail("Missed token in EnvChange!"); break; 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 ee7b04e0a5..f6c2bfa10c 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 @@ -2335,6 +2335,27 @@ internal void OnEnvChange(SqlEnvChange rec) RoutingInfo = rec._newRoutingInfo; break; + case TdsEnums.ENV_ENHANCEDROUTING: + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received enhanced routing info", ObjectID); + + if (!IsEnhancedRoutingSupportEnabled) + { + SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Enhanced routing disabled, ignoring enhanced routing info", ObjectID); + break; + } + + if (string.IsNullOrEmpty(rec._newRoutingInfo.ServerName) || + string.IsNullOrEmpty(rec._newRoutingInfo.DatabaseName) || + rec._newRoutingInfo.Protocol != 0 || + rec._newRoutingInfo.Port == 0) + { + throw SQL.ROR_InvalidEnhancedRoutingInfo(this); + } + + RoutingInfo = rec._newRoutingInfo; + + break; + default: Debug.Fail("Missed token in EnvChange!"); break; From b85042102d407b697acc07e00a2a1051542c2bf3 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 13:35:42 -0700 Subject: [PATCH 10/20] Convert some fields to auto properties to reduce clutter. --- .../SqlClient/SqlInternalConnectionTds.cs | 46 +++++-------------- .../SqlClient/SqlInternalConnectionTds.cs | 46 +++++-------------- 2 files changed, 24 insertions(+), 68 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 60581612be..81f0713eaa 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 @@ -319,7 +319,6 @@ internal SessionData CurrentSessionData private string _originalDatabase; private string _originalLanguage; private string _currentLanguage; - private int _currentPacketSize; private int _asyncCommandCount; // number of async Begins minus number of async Ends. // FOR SSE @@ -442,9 +441,6 @@ internal SqlConnectionTimeoutErrorInternal TimeoutErrorInternal // OTHER STATE VARIABLES AND REFERENCES internal Guid _clientConnectionId = Guid.Empty; - // Routing information (ROR) - private Guid _originalClientConnectionId = Guid.Empty; - private string _routingDestination = null; private readonly TimeoutTimer _timeout; // although the new password is generally not used it must be passed to the ctor @@ -608,21 +604,9 @@ internal Guid ClientConnectionId } } - internal Guid OriginalClientConnectionId - { - get - { - return _originalClientConnectionId; - } - } + internal Guid OriginalClientConnectionId { get; private set; } = Guid.Empty; - internal string RoutingDestination - { - get - { - return _routingDestination; - } - } + internal string RoutingDestination { get; private set; } internal RoutingInfo RoutingInfo { get; private set; } = null; @@ -690,13 +674,7 @@ internal override bool Is2008OrNewer } } - internal int PacketSize - { - get - { - return _currentPacketSize; - } - } + internal int PacketSize { get; private set; } internal TdsParser Parser { @@ -1298,7 +1276,7 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, // gather all the settings the user set in the connection string or // properties and do the login CurrentDatabase = server.ResolvedDatabaseName; - _currentPacketSize = ConnectionOptions.PacketSize; + PacketSize = ConnectionOptions.PacketSize; _currentLanguage = ConnectionOptions.CurrentLanguage; int timeoutInSeconds = 0; @@ -1345,7 +1323,7 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, login.useReplication = ConnectionOptions.Replication; login.useSSPI = ConnectionOptions.IntegratedSecurity // Treat AD Integrated like Windows integrated when against a non-FedAuth endpoint || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && !_fedAuthRequired); - login.packetSize = _currentPacketSize; + login.packetSize = PacketSize; login.newPassword = newPassword; login.readOnlyIntent = ConnectionOptions.ApplicationIntent == ApplicationIntent.ReadOnly; login.credential = _credential; @@ -1653,11 +1631,11 @@ private void LoginNoFailover(ServerInfo serverInfo, serverInfo = new ServerInfo(ConnectionOptions, RoutingInfo, serverInfo.ResolvedServerName, serverInfo.ServerSPN); _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.RoutingDestination); - _originalClientConnectionId = _clientConnectionId; - _routingDestination = serverInfo.UserServerName; + OriginalClientConnectionId = _clientConnectionId; + RoutingDestination = serverInfo.UserServerName; // restore properties that could be changed by the environment tokens - _currentPacketSize = ConnectionOptions.PacketSize; + PacketSize = ConnectionOptions.PacketSize; _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; CurrentDatabase = _originalDatabase = ConnectionOptions.InitialCatalog; ServerProvidedFailoverPartner = null; @@ -1920,11 +1898,11 @@ TimeoutTimer timeout currentServerInfo = new ServerInfo(ConnectionOptions, RoutingInfo, currentServerInfo.ResolvedServerName, currentServerInfo.ServerSPN); _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.RoutingDestination); - _originalClientConnectionId = _clientConnectionId; - _routingDestination = currentServerInfo.UserServerName; + OriginalClientConnectionId = _clientConnectionId; + RoutingDestination = currentServerInfo.UserServerName; // restore properties that could be changed by the environment tokens - _currentPacketSize = connectionOptions.PacketSize; + PacketSize = connectionOptions.PacketSize; _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; CurrentDatabase = _originalDatabase = connectionOptions.InitialCatalog; ServerProvidedFailoverPartner = null; @@ -2208,7 +2186,7 @@ internal void OnEnvChange(SqlEnvChange rec) break; case TdsEnums.ENV_PACKETSIZE: - _currentPacketSize = int.Parse(rec._newValue, CultureInfo.InvariantCulture); + PacketSize = int.Parse(rec._newValue, CultureInfo.InvariantCulture); break; case TdsEnums.ENV_COLLATION: 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 f6c2bfa10c..70b72b626f 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 @@ -331,7 +331,6 @@ internal SessionData CurrentSessionData private string _originalDatabase; private string _originalLanguage; private string _currentLanguage; - private int _currentPacketSize; private int _asyncCommandCount; // number of async Begins minus number of async Ends. // FOR SSE @@ -454,9 +453,6 @@ internal SqlConnectionTimeoutErrorInternal TimeoutErrorInternal // OTHER STATE VARIABLES AND REFERENCES internal Guid _clientConnectionId = Guid.Empty; - // Routing information (ROR) - private Guid _originalClientConnectionId = Guid.Empty; - private string _routingDestination = null; private readonly TimeoutTimer _timeout; // although the new password is generally not used it must be passed to the ctor @@ -618,21 +614,9 @@ internal Guid ClientConnectionId } } - internal Guid OriginalClientConnectionId - { - get - { - return _originalClientConnectionId; - } - } + internal Guid OriginalClientConnectionId { get; private set; } = Guid.Empty; - internal string RoutingDestination - { - get - { - return _routingDestination; - } - } + internal string RoutingDestination { get; private set; } internal RoutingInfo RoutingInfo { get; private set; } = null; @@ -700,13 +684,7 @@ internal override bool Is2008OrNewer } } - internal int PacketSize - { - get - { - return _currentPacketSize; - } - } + internal int PacketSize { get; private set; } internal TdsParser Parser { @@ -1306,7 +1284,7 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, // gather all the settings the user set in the connection string or // properties and do the login CurrentDatabase = server.ResolvedDatabaseName; - _currentPacketSize = ConnectionOptions.PacketSize; + PacketSize = ConnectionOptions.PacketSize; _currentLanguage = ConnectionOptions.CurrentLanguage; int timeoutInSeconds = 0; @@ -1353,7 +1331,7 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, login.useReplication = ConnectionOptions.Replication; login.useSSPI = ConnectionOptions.IntegratedSecurity // Treat AD Integrated like Windows integrated when against a non-FedAuth endpoint || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && !_fedAuthRequired); - login.packetSize = _currentPacketSize; + login.packetSize = PacketSize; login.newPassword = newPassword; login.readOnlyIntent = ConnectionOptions.ApplicationIntent == ApplicationIntent.ReadOnly; login.credential = _credential; @@ -1683,11 +1661,11 @@ private void LoginNoFailover(ServerInfo serverInfo, serverInfo = new ServerInfo(ConnectionOptions, RoutingInfo, serverInfo.ResolvedServerName, serverInfo.ServerSPN); _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.RoutingDestination); - _originalClientConnectionId = _clientConnectionId; - _routingDestination = serverInfo.UserServerName; + OriginalClientConnectionId = _clientConnectionId; + RoutingDestination = serverInfo.UserServerName; // restore properties that could be changed by the environment tokens - _currentPacketSize = ConnectionOptions.PacketSize; + PacketSize = ConnectionOptions.PacketSize; _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; CurrentDatabase = _originalDatabase = ConnectionOptions.InitialCatalog; ServerProvidedFailoverPartner = null; @@ -1974,11 +1952,11 @@ TimeoutTimer timeout currentServerInfo = new ServerInfo(ConnectionOptions, RoutingInfo, currentServerInfo.ResolvedServerName, currentServerInfo.ServerSPN); _timeoutErrorInternal.SetInternalSourceType(SqlConnectionInternalSourceType.RoutingDestination); - _originalClientConnectionId = _clientConnectionId; - _routingDestination = currentServerInfo.UserServerName; + OriginalClientConnectionId = _clientConnectionId; + RoutingDestination = currentServerInfo.UserServerName; // restore properties that could be changed by the environment tokens - _currentPacketSize = connectionOptions.PacketSize; + PacketSize = connectionOptions.PacketSize; _currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage; CurrentDatabase = _originalDatabase = connectionOptions.InitialCatalog; ServerProvidedFailoverPartner = null; @@ -2267,7 +2245,7 @@ internal void OnEnvChange(SqlEnvChange rec) break; case TdsEnums.ENV_PACKETSIZE: - _currentPacketSize = int.Parse(rec._newValue, CultureInfo.InvariantCulture); + PacketSize = int.Parse(rec._newValue, CultureInfo.InvariantCulture); break; case TdsEnums.ENV_COLLATION: From 2fc8175599c778a04caa0b5b943fc53aa7e94ea0 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 14:02:20 -0700 Subject: [PATCH 11/20] Clean up argument casting in RoutingTdsServer. --- .../tools/TDS/TDS.Servers/RoutingTdsServer.cs | 197 ++++++++---------- 1 file changed, 83 insertions(+), 114 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 8e119a54cd..20d7bbca28 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 @@ -42,27 +42,20 @@ public override TDSMessageCollection OnPreLoginRequest(ITDSServerSession session // Delegate to the base class TDSMessageCollection response = base.OnPreLoginRequest(session, request); - // Check if arguments are of the routing server - if (Arguments is RoutingTdsServerArguments) + // Check if routing is configured during login + if (Arguments.RouteOnPacket == TDSMessageType.TDS7Login) { - // Cast to routing server arguments - RoutingTdsServerArguments serverArguments = Arguments as RoutingTdsServerArguments; - - // Check if routing is configured during login - if (serverArguments.RouteOnPacket == TDSMessageType.TDS7Login) + // Check if pre-login response is contained inside the first message + if (response.Count > 0 && response[0].Any(t => t is TDSPreLoginToken)) { - // Check if pre-login response is contained inside the first message - if (response.Count > 0 && response[0].Any(t => t is TDSPreLoginToken)) - { - // Find the first prelogin token - TDSPreLoginToken preLoginResponse = (TDSPreLoginToken)response[0].Where(t => t is TDSPreLoginToken).First(); - - // Inflate pre-login request from the message - TDSPreLoginToken preLoginRequest = request[0] as TDSPreLoginToken; - - // Update MARS with the requested value - preLoginResponse.IsMARS = preLoginRequest.IsMARS; - } + // Find the first prelogin token + TDSPreLoginToken preLoginResponse = (TDSPreLoginToken)response[0].Where(t => t is TDSPreLoginToken).First(); + + // Inflate pre-login request from the message + TDSPreLoginToken preLoginRequest = request[0] as TDSPreLoginToken; + + // Update MARS with the requested value + preLoginResponse.IsMARS = preLoginRequest.IsMARS; } } @@ -77,48 +70,41 @@ public override TDSMessageCollection OnLogin7Request(ITDSServerSession session, // Inflate login7 request from the message TDSLogin7Token loginRequest = request[0] as TDSLogin7Token; - // Check if arguments are of the routing server - if (Arguments is RoutingTdsServerArguments) + // Check filter + if (Arguments.RequireReadOnly && (loginRequest.TypeFlags.ReadOnlyIntent != TDSLogin7TypeFlagsReadOnlyIntent.ReadOnly)) { - // Cast to routing server arguments - RoutingTdsServerArguments ServerArguments = Arguments as RoutingTdsServerArguments; - - // Check filter - if (ServerArguments.RequireReadOnly && (loginRequest.TypeFlags.ReadOnlyIntent != TDSLogin7TypeFlagsReadOnlyIntent.ReadOnly)) - { - // Log request - TDSUtilities.Log(Arguments.Log, "Request", loginRequest); + // Log request + TDSUtilities.Log(Arguments.Log, "Request", loginRequest); - // Prepare ERROR token with the denial details - TDSErrorToken errorToken = new TDSErrorToken(18456, 1, 14, "Received application intent: " + loginRequest.TypeFlags.ReadOnlyIntent.ToString(), Arguments.ServerName); + // Prepare ERROR token with the denial details + TDSErrorToken errorToken = new TDSErrorToken(18456, 1, 14, "Received application intent: " + loginRequest.TypeFlags.ReadOnlyIntent.ToString(), Arguments.ServerName); - // Log response - TDSUtilities.Log(Arguments.Log, "Response", errorToken); + // Log response + TDSUtilities.Log(Arguments.Log, "Response", errorToken); - // Serialize the error token into the response packet - TDSMessage responseMessage = new TDSMessage(TDSMessageType.Response, errorToken); + // Serialize the error token into the response packet + TDSMessage responseMessage = new TDSMessage(TDSMessageType.Response, errorToken); - // Prepare ERROR token for the final decision - errorToken = new TDSErrorToken(18456, 1, 14, "Read-Only application intent is required for routing", Arguments.ServerName); + // Prepare ERROR token for the final decision + errorToken = new TDSErrorToken(18456, 1, 14, "Read-Only application intent is required for routing", Arguments.ServerName); - // Log response - TDSUtilities.Log(Arguments.Log, "Response", errorToken); + // Log response + TDSUtilities.Log(Arguments.Log, "Response", errorToken); - // Serialize the error token into the response packet - responseMessage.Add(errorToken); + // Serialize the error token into the response packet + responseMessage.Add(errorToken); - // Create DONE token - TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Error); + // Create DONE token + TDSDoneToken doneToken = new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Error); - // Log response - TDSUtilities.Log(Arguments.Log, "Response", doneToken); + // Log response + TDSUtilities.Log(Arguments.Log, "Response", doneToken); - // Serialize DONE token into the response packet - responseMessage.Add(doneToken); + // Serialize DONE token into the response packet + responseMessage.Add(doneToken); - // Return a single message in the collection - return new TDSMessageCollection(responseMessage); - } + // Return a single message in the collection + return new TDSMessageCollection(responseMessage); } // Delegate to the base class @@ -135,44 +121,37 @@ public override TDSMessageCollection OnSQLBatchRequest(ITDSServerSession session // Delegate to the base class to produce the response first TDSMessageCollection batchResponse = base.OnSQLBatchRequest(session, request); - // Check if arguments are of routing server - if (Arguments is RoutingTdsServerArguments) + // Check routing condition + if (Arguments.RouteOnPacket == TDSMessageType.SQLBatch) { - // Cast to routing server arguments - RoutingTdsServerArguments ServerArguments = Arguments as RoutingTdsServerArguments; - - // Check routing condition - if (ServerArguments.RouteOnPacket == TDSMessageType.SQLBatch) - { - // Construct routing token - TDSPacketToken routingToken = CreateRoutingToken(); + // Construct routing token + TDSPacketToken routingToken = CreateRoutingToken(); - // Log response - TDSUtilities.Log(Arguments.Log, "Response", routingToken); + // Log response + TDSUtilities.Log(Arguments.Log, "Response", routingToken); - // Insert the routing token at the beginning of the response - batchResponse[0].Insert(0, routingToken); - } - else - { - // Get the first response message - TDSMessage responseMessage = batchResponse[0]; + // Insert the routing token at the beginning of the response + batchResponse[0].Insert(0, routingToken); + } + else + { + // Get the first response message + TDSMessage responseMessage = batchResponse[0]; - // Reset the content of the first message - responseMessage.Clear(); + // Reset the content of the first message + responseMessage.Clear(); - // Prepare ERROR token with the denial details - responseMessage.Add(new TDSErrorToken(11111, 1, 14, "Client should have been routed by now", Arguments.ServerName)); + // Prepare ERROR token with the denial details + responseMessage.Add(new TDSErrorToken(11111, 1, 14, "Client should have been routed by now", Arguments.ServerName)); - // Log response - TDSUtilities.Log(Arguments.Log, "Response", responseMessage[0]); + // Log response + TDSUtilities.Log(Arguments.Log, "Response", responseMessage[0]); - // Prepare DONE token - responseMessage.Add(new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Error)); + // Prepare DONE token + responseMessage.Add(new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Error)); - // Log response - TDSUtilities.Log(Arguments.Log, "Response", responseMessage[1]); - } + // Log response + TDSUtilities.Log(Arguments.Log, "Response", responseMessage[1]); } // Register only one message with the collection @@ -187,41 +166,34 @@ protected override TDSMessageCollection OnAuthenticationCompleted(ITDSServerSess // Delegate to the base class TDSMessageCollection responseMessageCollection = base.OnAuthenticationCompleted(session); - // Check if arguments are of routing server - if (Arguments is RoutingTdsServerArguments) + // Check routing condition + if (Arguments.RouteOnPacket == TDSMessageType.TDS7Login) { - // Cast to routing server arguments - RoutingTdsServerArguments serverArguments = Arguments as RoutingTdsServerArguments; - - // Check routing condition - if (serverArguments.RouteOnPacket == TDSMessageType.TDS7Login) - { - // Construct routing token - TDSPacketToken routingToken = CreateRoutingToken(); + // Construct routing token + TDSPacketToken routingToken = CreateRoutingToken(); - // Log response - TDSUtilities.Log(Arguments.Log, "Response", routingToken); + // Log response + TDSUtilities.Log(Arguments.Log, "Response", routingToken); - // Get the first message - TDSMessage targetMessage = responseMessageCollection[0]; + // Get the first message + TDSMessage targetMessage = responseMessageCollection[0]; - // Index at which to insert the routing token - int insertIndex = targetMessage.Count - 1; + // Index at which to insert the routing token + int insertIndex = targetMessage.Count - 1; - // VSTS# 1021027 - Read-Only Routing yields TDS protocol error - // Resolution: Send TDS FeatureExtAct token before TDS ENVCHANGE token with routing information - TDSPacketToken featureExtAckToken = targetMessage.Find(t => t is TDSFeatureExtAckToken); + // VSTS# 1021027 - Read-Only Routing yields TDS protocol error + // Resolution: Send TDS FeatureExtAct token before TDS ENVCHANGE token with routing information + TDSPacketToken featureExtAckToken = targetMessage.Find(t => t is TDSFeatureExtAckToken); - // Check if found - if (featureExtAckToken != null) - { - // Find token position - insertIndex = targetMessage.IndexOf(featureExtAckToken); - } - - // Insert right before the done token - targetMessage.Insert(insertIndex, routingToken); + // Check if found + if (featureExtAckToken != null) + { + // Find token position + insertIndex = targetMessage.IndexOf(featureExtAckToken); } + + // Insert right before the done token + targetMessage.Insert(insertIndex, routingToken); } return responseMessageCollection; @@ -232,16 +204,13 @@ protected override TDSMessageCollection OnAuthenticationCompleted(ITDSServerSess /// protected TDSPacketToken CreateRoutingToken() { - // Cast to routing server arguments - RoutingTdsServerArguments ServerArguments = Arguments as RoutingTdsServerArguments; - // Construct routing token value TDSRoutingEnvChangeTokenValue routingInfo = new TDSRoutingEnvChangeTokenValue(); // Read the values and populate routing info - routingInfo.Protocol = (TDSRoutingEnvChangeTokenValueType)ServerArguments.RoutingProtocol; - routingInfo.ProtocolProperty = ServerArguments.RoutingTCPPort; - routingInfo.AlternateServer = ServerArguments.RoutingTCPHost; + routingInfo.Protocol = (TDSRoutingEnvChangeTokenValueType)Arguments.RoutingProtocol; + routingInfo.ProtocolProperty = Arguments.RoutingTCPPort; + routingInfo.AlternateServer = Arguments.RoutingTCPHost; // Construct routing token return new TDSEnvChangeToken(TDSEnvChangeTokenType.Routing, routingInfo); From 7dcafb89c725ba3af2fb482b8afd16a9d0428588 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 14:02:56 -0700 Subject: [PATCH 12/20] Add database name to routing server arguments. --- .../tools/TDS/TDS.Servers/RoutingTdsServerArguments.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServerArguments.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServerArguments.cs index 95fe97f4f2..64431f9796 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServerArguments.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS.Servers/RoutingTdsServerArguments.cs @@ -24,6 +24,12 @@ public class RoutingTdsServerArguments : TdsServerArguments /// public string RoutingTCPHost { get; set; } = string.Empty; + /// + /// Database name to use at the routed location. + /// Should only be set when doing enhanced routing. + /// + public string RoutingDatabaseName { get; set; } = string.Empty; + /// /// Packet on which routing should occur /// From 1d9b7dd8615e7b0917bee5f6a57e65eb7fc8d39a Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 14:12:06 -0700 Subject: [PATCH 13/20] Add enhanced routing envchange token value for use by simulated server tests. --- .../TdsEnhancedRoutingEnvChangeTokenValue.cs | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/EnvChange/TdsEnhancedRoutingEnvChangeTokenValue.cs diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/EnvChange/TdsEnhancedRoutingEnvChangeTokenValue.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/EnvChange/TdsEnhancedRoutingEnvChangeTokenValue.cs new file mode 100644 index 0000000000..8b3b9fac89 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/EnvChange/TdsEnhancedRoutingEnvChangeTokenValue.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; + +namespace Microsoft.SqlServer.TDS.EnvChange +{ + /// + /// Token value that represents enhanced routing information + /// + public class TdsEnhancedRoutingEnvChangeTokenValue : IInflatable, IDeflatable + { + /// + /// Protocol to use when connecting to the target server + /// + public TDSRoutingEnvChangeTokenValueType Protocol { get; set; } + + /// + /// Protocol details + /// + public object ProtocolProperty { get; set; } + + /// + /// Location of the target server + /// + public string AlternateServer { get; set; } + + /// + /// Database to connect to at the target server + /// + public string AlternateDatabase { get; set; } + + /// + /// Default constructor + /// + public TdsEnhancedRoutingEnvChangeTokenValue() + { + } + + /// + /// Initialization constructor + /// + public TdsEnhancedRoutingEnvChangeTokenValue( + TDSRoutingEnvChangeTokenValueType protocol, + object protocolProperty, + string alternateServer, + string alternateDatabase) + { + Protocol = protocol; + ProtocolProperty = protocolProperty; + AlternateServer = alternateServer; + AlternateDatabase = alternateDatabase; + } + + /// + /// Inflate the token + /// + /// Stream to inflate the token from + /// TRUE if inflation is complete + public virtual bool Inflate(Stream source) + { + // Read protocol value + Protocol = (TDSRoutingEnvChangeTokenValueType)source.ReadByte(); + + // Based on the protocol type read the rest of the token + switch (Protocol) + { + case TDSRoutingEnvChangeTokenValueType.TCP: + { + // Read port + ProtocolProperty = TDSUtilities.ReadUShort(source); + AlternateServer = TDSUtilities.ReadString(source, (ushort)(TDSUtilities.ReadUShort(source) * 2)); + AlternateServer = TDSUtilities.ReadString(source, (ushort)(TDSUtilities.ReadUShort(source) * 2)); + + break; + } + default: + { + throw new Exception("Unrecognized routing protocol"); + } + } + + // Inflation is complete + return true; + } + + /// + /// Deflate the token + /// + /// Stream to deflate token to + public virtual void Deflate(Stream destination) + { + // Write protocol value + destination.WriteByte((byte)Protocol); + + // Based on the protocol type read the rest of the token + switch (Protocol) + { + default: + case TDSRoutingEnvChangeTokenValueType.TCP: + { + // Write port + TDSUtilities.WriteUShort(destination, (ushort)ProtocolProperty); + + // Write alternate server name length + TDSUtilities.WriteUShort(destination, (ushort)(string.IsNullOrEmpty(AlternateServer) ? 0 : AlternateServer.Length)); + + // Write alternate server name + TDSUtilities.WriteString(destination, AlternateServer); + + TDSUtilities.WriteString(destination, AlternateDatabase); + + break; + } + } + } + + /// + /// Override string representation method + /// + public override string ToString() + { + return $"Protocol: {Protocol}; Protocol Property: {ProtocolProperty}; Alternate Server: {AlternateServer}; Alternate Database: {AlternateDatabase}"; + } + } +} From 2d422fb9920c553c1c64d67287f00a747dec1546 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 14:16:04 -0700 Subject: [PATCH 14/20] Add enhanced routing envchange type to enum --- .../tests/tools/TDS/TDS/EnvChange/TDSEnvChangeTokenType.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/EnvChange/TDSEnvChangeTokenType.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/EnvChange/TDSEnvChangeTokenType.cs index b4e1ae96d3..5396fe9f41 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/EnvChange/TDSEnvChangeTokenType.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/EnvChange/TDSEnvChangeTokenType.cs @@ -27,6 +27,7 @@ public enum TDSEnvChangeTokenType : byte TransactionEnded = 17, ResetConnectionAcknowledgement = 18, UserInstance = 19, - Routing = 20 + Routing = 20, + EnhancedRouting = 21 } } From 2456f288677267103bdd80e63ab3750331599467 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 14:16:44 -0700 Subject: [PATCH 15/20] Return enhanced routing token when server is configured with a routing database value. --- .../tools/TDS/TDS.Servers/RoutingTdsServer.cs | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 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 20d7bbca28..09e5537adb 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 @@ -204,16 +204,32 @@ protected override TDSMessageCollection OnAuthenticationCompleted(ITDSServerSess /// protected TDSPacketToken CreateRoutingToken() { - // Construct routing token value - TDSRoutingEnvChangeTokenValue routingInfo = new TDSRoutingEnvChangeTokenValue(); + if (string.IsNullOrEmpty(Arguments.RoutingDatabaseName)) + { + // Construct routing token value + TDSRoutingEnvChangeTokenValue routingInfo = new TDSRoutingEnvChangeTokenValue(); + + // Read the values and populate routing info + routingInfo.Protocol = (TDSRoutingEnvChangeTokenValueType)Arguments.RoutingProtocol; + routingInfo.ProtocolProperty = Arguments.RoutingTCPPort; + routingInfo.AlternateServer = Arguments.RoutingTCPHost; - // Read the values and populate routing info - routingInfo.Protocol = (TDSRoutingEnvChangeTokenValueType)Arguments.RoutingProtocol; - routingInfo.ProtocolProperty = Arguments.RoutingTCPPort; - routingInfo.AlternateServer = Arguments.RoutingTCPHost; + // Construct routing token + return new TDSEnvChangeToken(TDSEnvChangeTokenType.Routing, routingInfo); + } else + { + // Construct enhanced routing token value + TdsEnhancedRoutingEnvChangeTokenValue routingInfo = new TdsEnhancedRoutingEnvChangeTokenValue(); - // Construct routing token - return new TDSEnvChangeToken(TDSEnvChangeTokenType.Routing, routingInfo); + // Read the values and populate routing info + routingInfo.Protocol = (TDSRoutingEnvChangeTokenValueType)Arguments.RoutingProtocol; + routingInfo.ProtocolProperty = Arguments.RoutingTCPPort; + routingInfo.AlternateServer = Arguments.RoutingTCPHost; + routingInfo.AlternateDatabase = Arguments.RoutingDatabaseName; + + // Construct routing token + return new TDSEnvChangeToken(TDSEnvChangeTokenType.EnhancedRouting, routingInfo); + } } } } From 0b6289d38e50fb08e713aa858776558f650aeeef Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 14:19:46 -0700 Subject: [PATCH 16/20] Enable default compile items for TDS proj. --- .../tests/tools/TDS/TDS/TDS.csproj | 98 ------------------- 1 file changed, 98 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDS.csproj b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDS.csproj index 58f7caef5f..2aefea4642 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDS.csproj +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/TDS.csproj @@ -5,105 +5,7 @@ {8DC9D1A0-351B-47BC-A90F-B9DA542550E9} netstandard2.0 false - false $(ObjFolder)$(Configuration).$(Platform)\$(AssemblyName) $(BinFolder)$(Configuration).$(Platform)\$(AssemblyName) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 8cd2270cac3b281719b7056cf8e233ad867ee587 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 14:20:19 -0700 Subject: [PATCH 17/20] Clean up envchange token initialization. --- .../tools/TDS/TDS.Servers/RoutingTdsServer.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 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 09e5537adb..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 @@ -207,25 +207,25 @@ protected TDSPacketToken CreateRoutingToken() if (string.IsNullOrEmpty(Arguments.RoutingDatabaseName)) { // Construct routing token value - TDSRoutingEnvChangeTokenValue routingInfo = new TDSRoutingEnvChangeTokenValue(); - - // Read the values and populate routing info - routingInfo.Protocol = (TDSRoutingEnvChangeTokenValueType)Arguments.RoutingProtocol; - routingInfo.ProtocolProperty = Arguments.RoutingTCPPort; - routingInfo.AlternateServer = Arguments.RoutingTCPHost; + TDSRoutingEnvChangeTokenValue routingInfo = new TDSRoutingEnvChangeTokenValue() + { + Protocol = (TDSRoutingEnvChangeTokenValueType)Arguments.RoutingProtocol, + ProtocolProperty = Arguments.RoutingTCPPort, + AlternateServer = Arguments.RoutingTCPHost, + }; // Construct routing token return new TDSEnvChangeToken(TDSEnvChangeTokenType.Routing, routingInfo); } else { // Construct enhanced routing token value - TdsEnhancedRoutingEnvChangeTokenValue routingInfo = new TdsEnhancedRoutingEnvChangeTokenValue(); - - // Read the values and populate routing info - routingInfo.Protocol = (TDSRoutingEnvChangeTokenValueType)Arguments.RoutingProtocol; - routingInfo.ProtocolProperty = Arguments.RoutingTCPPort; - routingInfo.AlternateServer = Arguments.RoutingTCPHost; - routingInfo.AlternateDatabase = Arguments.RoutingDatabaseName; + TdsEnhancedRoutingEnvChangeTokenValue routingInfo = new TdsEnhancedRoutingEnvChangeTokenValue() + { + Protocol = (TDSRoutingEnvChangeTokenValueType)Arguments.RoutingProtocol, + ProtocolProperty = Arguments.RoutingTCPPort, + AlternateServer = Arguments.RoutingTCPHost, + AlternateDatabase = Arguments.RoutingDatabaseName + }; // Construct routing token return new TDSEnvChangeToken(TDSEnvChangeTokenType.EnhancedRouting, routingInfo); From b1aee5ed36aff234d533086cf736fdad5f1d2c58 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 15:50:44 -0700 Subject: [PATCH 18/20] Fix ResolvedDatabaseName update. --- .../Data/SqlClient/SqlInternalConnectionTds.cs | 17 ++++++++++------- .../Data/SqlClient/SqlInternalConnectionTds.cs | 18 +++++++++++------- 2 files changed, 21 insertions(+), 14 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 81f0713eaa..3f329156aa 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 @@ -2261,12 +2261,6 @@ internal void OnEnvChange(SqlEnvChange rec) case TdsEnums.ENV_ENHANCEDROUTING: SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received enhanced routing info", ObjectID); - - if (!IsEnhancedRoutingSupportEnabled) - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Enhanced routing disabled, ignoring enhanced routing info", ObjectID); - break; - } if (string.IsNullOrEmpty(rec._newRoutingInfo.ServerName) || string.IsNullOrEmpty(rec._newRoutingInfo.DatabaseName) || @@ -3151,10 +3145,19 @@ internal ServerInfo(SqlConnectionString userOptions, RoutingInfo routing, string { UserServerName = string.Format(CultureInfo.InvariantCulture, "{0},{1}", routing.ServerName, routing.Port); } + + if (routing == null || routing.DatabaseName == null) + { + ResolvedDatabaseName = userOptions.InitialCatalog; + } + else + { + ResolvedDatabaseName = routing.DatabaseName; + } + PreRoutingServerName = preRoutingServerName; UserProtocol = TdsEnums.TCP; SetDerivedNames(UserProtocol, UserServerName); - ResolvedDatabaseName = userOptions.InitialCatalog; ServerSPN = serverSPN; } 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 70b72b626f..ca554559d8 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 @@ -2316,12 +2316,6 @@ internal void OnEnvChange(SqlEnvChange rec) case TdsEnums.ENV_ENHANCEDROUTING: SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Received enhanced routing info", ObjectID); - if (!IsEnhancedRoutingSupportEnabled) - { - SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, Enhanced routing disabled, ignoring enhanced routing info", ObjectID); - break; - } - if (string.IsNullOrEmpty(rec._newRoutingInfo.ServerName) || string.IsNullOrEmpty(rec._newRoutingInfo.DatabaseName) || rec._newRoutingInfo.Protocol != 0 || @@ -3194,10 +3188,20 @@ internal ServerInfo(SqlConnectionString userOptions, RoutingInfo routing, string { UserServerName = string.Format(CultureInfo.InvariantCulture, "{0},{1}", routing.ServerName, routing.Port); } + + if (routing == null || routing.DatabaseName == null) + { + ResolvedDatabaseName = userOptions.InitialCatalog; + } + else + { + ResolvedDatabaseName = routing.DatabaseName; + } + PreRoutingServerName = preRoutingServerName; UserProtocol = TdsEnums.TCP; SetDerivedNames(UserProtocol, UserServerName); - ResolvedDatabaseName = userOptions.InitialCatalog; + ServerSPN = serverSPN; } From 05ca8189bb10b61d4c78e513222ad11e3a5acd85 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 15:51:40 -0700 Subject: [PATCH 19/20] Fix enhanced routing inflation and deflation for simulated servers. --- .../TDS/TDS/EnvChange/TDSEnvChangeToken.cs | 50 +++++++++++++++++++ .../TdsEnhancedRoutingEnvChangeTokenValue.cs | 4 +- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/EnvChange/TDSEnvChangeToken.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/EnvChange/TDSEnvChangeToken.cs index d8c3655037..1c2837de31 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/EnvChange/TDSEnvChangeToken.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/EnvChange/TDSEnvChangeToken.cs @@ -161,6 +161,33 @@ public override bool Inflate(Stream source) throw new Exception("Non-zero old value for routing information"); } + break; + } + case TDSEnvChangeTokenType.EnhancedRouting: + { + // Read the new value length + ushort valueLength = TDSUtilities.ReadUShort(source); + + // Update token length + tokenLength -= 2; // sizeof(ushort) + + // Instantiate new value + NewValue = new TdsEnhancedRoutingEnvChangeTokenValue(); + + // Inflate new value + if (!(NewValue as TdsEnhancedRoutingEnvChangeTokenValue).Inflate(source)) + { + // We should never reach this point + throw new Exception("Routing information inflation failed"); + } + + // Read always-zero old value unsigned short + if (TDSUtilities.ReadUShort(source) != 0) + { + // We should never reach this point + throw new Exception("Non-zero old value for routing information"); + } + break; } case TDSEnvChangeTokenType.SQLCollation: @@ -295,6 +322,29 @@ public override void Deflate(Stream destination) // Write zero for the old value length TDSUtilities.WriteUShort(cache, 0); + break; + } + case TDSEnvChangeTokenType.EnhancedRouting: + { + // Create a sub-cache to determine the value length + MemoryStream subCache = new MemoryStream(); + + // Check if new value exists + if (NewValue != null) + { + // Deflate token value + (NewValue as TdsEnhancedRoutingEnvChangeTokenValue).Deflate(subCache); + } + + // Write new value length + TDSUtilities.WriteUShort(cache, (ushort)subCache.Length); + + // Write new value + subCache.WriteTo(cache); + + // Write zero for the old value length + TDSUtilities.WriteUShort(cache, 0); + break; } case TDSEnvChangeTokenType.SQLCollation: diff --git a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/EnvChange/TdsEnhancedRoutingEnvChangeTokenValue.cs b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/EnvChange/TdsEnhancedRoutingEnvChangeTokenValue.cs index 8b3b9fac89..1c6c47efc3 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/EnvChange/TdsEnhancedRoutingEnvChangeTokenValue.cs +++ b/src/Microsoft.Data.SqlClient/tests/tools/TDS/TDS/EnvChange/TdsEnhancedRoutingEnvChangeTokenValue.cs @@ -72,7 +72,7 @@ public virtual bool Inflate(Stream source) // Read port ProtocolProperty = TDSUtilities.ReadUShort(source); AlternateServer = TDSUtilities.ReadString(source, (ushort)(TDSUtilities.ReadUShort(source) * 2)); - AlternateServer = TDSUtilities.ReadString(source, (ushort)(TDSUtilities.ReadUShort(source) * 2)); + AlternateDatabase = TDSUtilities.ReadString(source, (ushort)(TDSUtilities.ReadUShort(source) * 2)); break; } @@ -110,6 +110,8 @@ public virtual void Deflate(Stream destination) // Write alternate server name TDSUtilities.WriteString(destination, AlternateServer); + // Write alternate database name length + TDSUtilities.WriteUShort(destination, (ushort)(string.IsNullOrEmpty(AlternateDatabase) ? 0 : AlternateDatabase.Length)); TDSUtilities.WriteString(destination, AlternateDatabase); break; From 3486a2fbcaf9be09e3c3bda2f1189096f16afe94 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Tue, 30 Sep 2025 15:53:43 -0700 Subject: [PATCH 20/20] Add simulated server tests. --- .../ConnectionEnhancedRoutingTests.cs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionEnhancedRoutingTests.cs diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionEnhancedRoutingTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionEnhancedRoutingTests.cs new file mode 100644 index 0000000000..8578c35fa9 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionEnhancedRoutingTests.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Data; +using System.Threading.Tasks; +using Microsoft.SqlServer.TDS.Servers; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests.SimulatedServerTests; + +/// +/// Tests connection routing using the enhanced routing feature extension and envchange token +/// +[Collection("SimulatedServerTests")] +public class ConnectionEnhancedRoutingTests +{ + [Fact] + public void RoutedConnection() + { + // Arrange + using TdsServer server = new(new() + { + Log = Console.Out + }); + server.Start(); + + string routingDatabaseName = Guid.NewGuid().ToString(); + bool clientProvidedCorrectDatabase = false; + server.OnLogin7Validated = loginToken => + { + clientProvidedCorrectDatabase = routingDatabaseName == loginToken.Database; + }; + + RoutingTdsServer router = new( + new RoutingTdsServerArguments() + { + Log = Console.Out, + RoutingTCPHost = "localhost", + RoutingTCPPort = (ushort)server.EndPoint.Port, + RoutingDatabaseName = routingDatabaseName, + RequireReadOnly = false + }); + router.Start(); + router.EnableEnhancedRouting = FeatureExtensionEnablementTriState.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() + { + Log = Console.Out + }); + server.Start(); + + string routingDatabaseName = Guid.NewGuid().ToString(); + bool clientProvidedCorrectDatabase = false; + server.OnLogin7Validated = loginToken => + { + clientProvidedCorrectDatabase = routingDatabaseName == loginToken.Database; + }; + + RoutingTdsServer router = new( + new RoutingTdsServerArguments() + { + Log = Console.Out, + RoutingTCPHost = "localhost", + RoutingTCPPort = (ushort)server.EndPoint.Port, + RoutingDatabaseName = routingDatabaseName, + RequireReadOnly = false + }); + router.Start(); + router.EnableEnhancedRouting = FeatureExtensionEnablementTriState.Enabled; + + string connectionString = (new SqlConnectionStringBuilder() + { + DataSource = $"localhost,{router.EndPoint.Port}", + Encrypt = false, + ConnectTimeout = 10000 + }).ConnectionString; + + // Act + using SqlConnection connection = new(connectionString); + await connection.OpenAsync(); + + // 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); + } +}