From d441efafe1af526ab0225ae7fff990c99ce9a251 Mon Sep 17 00:00:00 2001 From: Apoorv Deshmukh Date: Thu, 11 Dec 2025 18:04:45 +0530 Subject: [PATCH 1/3] Introduce app context switch for setting MSF=true by default --- .../DbConnectionStringDefaults.cs | 2 +- .../Data/SqlClient/LocalAppContextSwitches.cs | 37 ++++++++-- .../Common/LocalAppContextSwitchesHelper.cs | 43 ++++++++--- .../SqlClient/LocalAppContextSwitchesTest.cs | 1 + .../Data/SqlClient/SqlConnectionStringTest.cs | 74 +++++++++++++++++++ 5 files changed, 142 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ConnectionString/DbConnectionStringDefaults.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ConnectionString/DbConnectionStringDefaults.cs index 757b3522fc..460fa0c2cc 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ConnectionString/DbConnectionStringDefaults.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ConnectionString/DbConnectionStringDefaults.cs @@ -39,7 +39,7 @@ internal static class DbConnectionStringDefaults internal const int MaxPoolSize = 100; internal const int MinPoolSize = 0; internal const bool MultipleActiveResultSets = false; - internal const bool MultiSubnetFailover = false; + internal static bool MultiSubnetFailover => LocalAppContextSwitches.EnableMultiSubnetFailoverByDefault; internal const int PacketSize = 8000; internal const string Password = ""; internal const bool PersistSecurityInfo = false; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs index a12587411c..b41fea38db 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -27,15 +27,16 @@ private enum Tristate : byte private const string TruncateScaledDecimalString = @"Switch.Microsoft.Data.SqlClient.TruncateScaledDecimal"; private const string IgnoreServerProvidedFailoverPartnerString = @"Switch.Microsoft.Data.SqlClient.IgnoreServerProvidedFailoverPartner"; private const string EnableUserAgentString = @"Switch.Microsoft.Data.SqlClient.EnableUserAgent"; + private const string EnableMSFByDefaultString = @"Switch.Microsoft.Data.SqlClient.EnableMSFByDefaultInConnString"; - #if NET +#if NET private const string GlobalizationInvariantModeString = @"System.Globalization.Invariant"; private const string GlobalizationInvariantModeEnvironmentVariable = "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"; - #if _WINDOWS +#if _WINDOWS private const string UseManagedNetworkingOnWindowsString = "Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows"; - #endif - #else +#endif +#else private const string DisableTnirByDefaultString = @"Switch.Microsoft.Data.SqlClient.DisableTNIRByDefaultInConnectionString"; #endif @@ -52,6 +53,7 @@ private enum Tristate : byte private static Tristate s_truncateScaledDecimal; private static Tristate s_ignoreServerProvidedFailoverPartner; private static Tristate s_enableUserAgent; + private static Tristate s_multiSubnetFailoverByDefault; #if NET private static Tristate s_globalizationInvariantMode; @@ -511,6 +513,31 @@ public static bool DisableTnirByDefault return s_disableTnirByDefault == Tristate.True; } } - #endif +#endif + + /// + /// When set to true, the default value for MultiSubnetFailover connection string property + /// will be true instead of false. This enables parallel IP connection attempts for + /// improved connection times in multi-subnet environments. + /// This app context switch defaults to 'false'. + /// + public static bool EnableMultiSubnetFailoverByDefault + { + get + { + if (s_multiSubnetFailoverByDefault == Tristate.NotInitialized) + { + if (AppContext.TryGetSwitch(EnableMSFByDefaultString, out bool returnedValue) && returnedValue) + { + s_multiSubnetFailoverByDefault = Tristate.True; + } + else + { + s_multiSubnetFailoverByDefault = Tristate.False; + } + } + return s_multiSubnetFailoverByDefault == Tristate.True; + } + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs index 1798e1fcf4..c1bedaf6ee 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs @@ -33,8 +33,8 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable private readonly PropertyInfo _truncateScaledDecimalProperty; private readonly PropertyInfo _ignoreServerProvidedFailoverPartner; private readonly PropertyInfo _enableUserAgent; - - #if NET + private readonly PropertyInfo _enableMultiSubnetFailoverByDefaultProperty; +#if NET private readonly PropertyInfo _globalizationInvariantModeProperty; #endif @@ -69,8 +69,9 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable private readonly Tristate _ignoreServerProvidedFailoverPartnerOriginal; private readonly FieldInfo _enableUserAgentField; private readonly Tristate _enableUserAgentOriginal; - - #if NET + private readonly FieldInfo _multiSubnetFailoverByDefaultField; + private readonly Tristate _multiSubnetFailoverByDefaultOriginal; +#if NET private readonly FieldInfo _globalizationInvariantModeField; private readonly Tristate _globalizationInvariantModeOriginal; #endif @@ -181,7 +182,11 @@ void InitProperty(string name, out PropertyInfo property) "EnableUserAgent", out _enableUserAgent); - #if NET + InitProperty( + "EnableMultiSubnetFailoverByDefault", + out _enableMultiSubnetFailoverByDefaultProperty); + +#if NET InitProperty( "GlobalizationInvariantMode", out _globalizationInvariantModeProperty); @@ -269,7 +274,12 @@ void InitField(string name, out FieldInfo field, out Tristate value) out _enableUserAgentField, out _enableUserAgentOriginal); - #if NET + InitField( + "s_multiSubnetFailoverByDefault", + out _multiSubnetFailoverByDefaultField, + out _multiSubnetFailoverByDefaultOriginal); + +#if NET InitField( "s_globalizationInvariantMode", out _globalizationInvariantModeField, @@ -281,8 +291,8 @@ void InitField(string name, out FieldInfo field, out Tristate value) "s_useManagedNetworking", out _useManagedNetworkingField, out _useManagedNetworkingOriginal); - #endif - +#endif + #if NETFRAMEWORK InitField( "s_disableTnirByDefault", @@ -359,6 +369,10 @@ void RestoreField(FieldInfo field, Tristate value) _enableUserAgentField, _enableUserAgentOriginal); + RestoreField( + _multiSubnetFailoverByDefaultField, + _multiSubnetFailoverByDefaultOriginal); + #if NET RestoreField( _globalizationInvariantModeField, @@ -474,6 +488,11 @@ public bool EnableUserAgent get => (bool)_enableUserAgent.GetValue(null); } + public bool EnableMultiSubnetFailoverByDefault + { + get => (bool) _enableMultiSubnetFailoverByDefaultProperty.GetValue(null); + } + #if NET /// /// Access the LocalAppContextSwitches.GlobalizationInvariantMode property. @@ -608,7 +627,13 @@ public Tristate EnableUserAgentField set => SetValue(_enableUserAgentField, value); } - #if NET + public Tristate EnableMultiSubnetFailoverByDefaultField + { + get => GetValue(_multiSubnetFailoverByDefaultField); + set => SetValue(_multiSubnetFailoverByDefaultField, value); + } + +#if NET /// /// Get or set the LocalAppContextSwitches.GlobalizationInvariantMode switch value. /// diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/LocalAppContextSwitchesTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/LocalAppContextSwitchesTest.cs index c28fe18978..c92402af41 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/LocalAppContextSwitchesTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/LocalAppContextSwitchesTest.cs @@ -27,6 +27,7 @@ public void TestDefaultAppContextSwitchValues() Assert.True(LocalAppContextSwitches.UseCompatibilityAsyncBehaviour); Assert.False(LocalAppContextSwitches.UseConnectionPoolV2); Assert.False(LocalAppContextSwitches.TruncateScaledDecimal); + Assert.False(LocalAppContextSwitches.EnableMultiSubnetFailoverByDefault); #if NETFRAMEWORK Assert.False(LocalAppContextSwitches.DisableTnirByDefault); Assert.False(LocalAppContextSwitches.UseManagedNetworking); diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs index 14bc51d520..f7959a211e 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs @@ -58,6 +58,80 @@ public void TestDefaultTnir(string dataSource, bool? tnirEnabledInConnString, Tr Assert.Equal(expectedValue, connectionString.TransparentNetworkIPResolution); } #endif + /// + /// Test MSF values when set through connection string and through app context switch. + /// + [Theory] + [InlineData(true, Tristate.True, true)] + [InlineData(false, Tristate.True, false)] + [InlineData(null, Tristate.True, true)] + [InlineData(true, Tristate.False, true)] + [InlineData(false, Tristate.False, false)] + [InlineData(null, Tristate.False, false)] + [InlineData(null, Tristate.NotInitialized, false)] + public void TestDefaultMultiSubnetFailover(bool? msfInConnString, Tristate msfEnabledAppContext, bool expectedValue) + { + _appContextSwitchHelper.EnableMultiSubnetFailoverByDefaultField = msfEnabledAppContext; + + SqlConnectionStringBuilder builder = new(); + if (msfInConnString.HasValue) + { + builder.MultiSubnetFailover = msfInConnString.Value; + } + SqlConnectionString connectionString = new(builder.ConnectionString); + + Assert.Equal(expectedValue, connectionString.MultiSubnetFailover); + } + + /// + /// Tests that MultiSubnetFailover=true cannot be used with FailoverPartner. + /// + [Fact] + public void TestMultiSubnetFailoverWithFailoverPartnerThrows() + { + _appContextSwitchHelper.EnableMultiSubnetFailoverByDefaultField = Tristate.True; + + SqlConnectionStringBuilder builder = new() + { + DataSource = "server", + FailoverPartner = "partner", + InitialCatalog = "database" + }; + + Assert.Throws(() => new SqlConnectionString(builder.ConnectionString)); + } + + /// + /// Tests that when MSF is enabled by default via switch, explicitly setting it to false works. + /// + [Fact] + public void TestExplicitlyDisableMultiSubnetFailoverOverridesSwitch() + { + _appContextSwitchHelper.EnableMultiSubnetFailoverByDefaultField = Tristate.True; + + SqlConnectionStringBuilder builder = new() + { + DataSource = "server", + MultiSubnetFailover = false + }; + SqlConnectionString connectionString = new(builder.ConnectionString); + + Assert.False(connectionString.MultiSubnetFailover); + } + + /// + /// Tests SqlConnectionStringBuilder default value reflects the app context switch. + /// + [Theory] + [InlineData(Tristate.True, true)] + [InlineData(Tristate.False, false)] + [InlineData(Tristate.NotInitialized, false)] + public void TestSqlConnectionStringBuilderDefaultMultiSubnetFailover(Tristate msfEnabledAppContext, bool expectedDefault) + { + _appContextSwitchHelper.EnableMultiSubnetFailoverByDefaultField = msfEnabledAppContext; + SqlConnectionStringBuilder builder = new(); + Assert.Equal(expectedDefault, builder.MultiSubnetFailover); + } public void Dispose() { From 985e10cdb3dd70276c42b3d99bbb1db9604ebe84 Mon Sep 17 00:00:00 2001 From: Apoorv Deshmukh Date: Thu, 11 Dec 2025 18:23:58 +0530 Subject: [PATCH 2/3] Remove extra space --- .../tests/Common/LocalAppContextSwitchesHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs index c1bedaf6ee..7d90df85fe 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs @@ -490,7 +490,7 @@ public bool EnableUserAgent public bool EnableMultiSubnetFailoverByDefault { - get => (bool) _enableMultiSubnetFailoverByDefaultProperty.GetValue(null); + get => (bool)_enableMultiSubnetFailoverByDefaultProperty.GetValue(null); } #if NET From 42ffb7f9b2ccef4307a7e4d3b96efbf80ac20b69 Mon Sep 17 00:00:00 2001 From: Apoorv Deshmukh Date: Thu, 11 Dec 2025 23:56:24 +0530 Subject: [PATCH 3/3] Address review comments --- .../Data/SqlClient/LocalAppContextSwitches.cs | 12 +++---- .../Data/SqlClient/SqlConnectionStringTest.cs | 32 ------------------- 2 files changed, 6 insertions(+), 38 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs index b41fea38db..09d84e0ea7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -27,16 +27,16 @@ private enum Tristate : byte private const string TruncateScaledDecimalString = @"Switch.Microsoft.Data.SqlClient.TruncateScaledDecimal"; private const string IgnoreServerProvidedFailoverPartnerString = @"Switch.Microsoft.Data.SqlClient.IgnoreServerProvidedFailoverPartner"; private const string EnableUserAgentString = @"Switch.Microsoft.Data.SqlClient.EnableUserAgent"; - private const string EnableMSFByDefaultString = @"Switch.Microsoft.Data.SqlClient.EnableMSFByDefaultInConnString"; + private const string EnableMultiSubnetFailoverByDefaultString = @"Switch.Microsoft.Data.SqlClient.EnableMultiSubnetFailoverByDefault"; -#if NET + #if NET private const string GlobalizationInvariantModeString = @"System.Globalization.Invariant"; private const string GlobalizationInvariantModeEnvironmentVariable = "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"; -#if _WINDOWS + #if _WINDOWS private const string UseManagedNetworkingOnWindowsString = "Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows"; -#endif -#else + #endif + #else private const string DisableTnirByDefaultString = @"Switch.Microsoft.Data.SqlClient.DisableTNIRByDefaultInConnectionString"; #endif @@ -527,7 +527,7 @@ public static bool EnableMultiSubnetFailoverByDefault { if (s_multiSubnetFailoverByDefault == Tristate.NotInitialized) { - if (AppContext.TryGetSwitch(EnableMSFByDefaultString, out bool returnedValue) && returnedValue) + if (AppContext.TryGetSwitch(EnableMultiSubnetFailoverByDefaultString, out bool returnedValue) && returnedValue) { s_multiSubnetFailoverByDefault = Tristate.True; } diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs index f7959a211e..e413821932 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionStringTest.cs @@ -101,38 +101,6 @@ public void TestMultiSubnetFailoverWithFailoverPartnerThrows() Assert.Throws(() => new SqlConnectionString(builder.ConnectionString)); } - /// - /// Tests that when MSF is enabled by default via switch, explicitly setting it to false works. - /// - [Fact] - public void TestExplicitlyDisableMultiSubnetFailoverOverridesSwitch() - { - _appContextSwitchHelper.EnableMultiSubnetFailoverByDefaultField = Tristate.True; - - SqlConnectionStringBuilder builder = new() - { - DataSource = "server", - MultiSubnetFailover = false - }; - SqlConnectionString connectionString = new(builder.ConnectionString); - - Assert.False(connectionString.MultiSubnetFailover); - } - - /// - /// Tests SqlConnectionStringBuilder default value reflects the app context switch. - /// - [Theory] - [InlineData(Tristate.True, true)] - [InlineData(Tristate.False, false)] - [InlineData(Tristate.NotInitialized, false)] - public void TestSqlConnectionStringBuilderDefaultMultiSubnetFailover(Tristate msfEnabledAppContext, bool expectedDefault) - { - _appContextSwitchHelper.EnableMultiSubnetFailoverByDefaultField = msfEnabledAppContext; - SqlConnectionStringBuilder builder = new(); - Assert.Equal(expectedDefault, builder.MultiSubnetFailover); - } - public void Dispose() { // Clean up any resources if necessary