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 ca5ebb2226..793c981a7e 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 702be5f842..ecba075025 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -22,6 +22,7 @@ private enum Tristate : byte internal const string LegacyVarTimeZeroScaleBehaviourString = @"Switch.Microsoft.Data.SqlClient.LegacyVarTimeZeroScaleBehaviour"; internal const string UseConnectionPoolV2String = @"Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2"; private const string IgnoreServerProvidedFailoverPartnerString = @"Switch.Microsoft.Data.SqlClient.IgnoreServerProvidedFailoverPartner"; + private const string EnableMultiSubnetFailoverByDefaultString = @"Switch.Microsoft.Data.SqlClient.EnableMultiSubnetFailoverByDefault"; // this field is accessed through reflection in tests and should not be renamed or have the type changed without refactoring NullRow related tests private static Tristate s_legacyRowVersionNullBehavior; @@ -32,6 +33,7 @@ private enum Tristate : byte private static Tristate s_legacyVarTimeZeroScaleBehaviour; private static Tristate s_useConnectionPoolV2; private static Tristate s_ignoreServerProvidedFailoverPartner; + private static Tristate s_multiSubnetFailoverByDefault; #if NET @@ -263,5 +265,30 @@ public static bool IgnoreServerProvidedFailoverPartner return s_ignoreServerProvidedFailoverPartner == Tristate.True; } } + + /// + /// 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(EnableMultiSubnetFailoverByDefaultString, 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/src/Microsoft/Data/SqlClient/SqlConnectionString.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionString.cs index b96a946ac7..67adcf0674 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionString.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionString.cs @@ -45,7 +45,7 @@ internal static class DEFAULT internal const bool MARS = DbConnectionStringDefaults.MultipleActiveResultSets; internal const int Max_Pool_Size = DbConnectionStringDefaults.MaxPoolSize; internal const int Min_Pool_Size = DbConnectionStringDefaults.MinPoolSize; - internal const bool MultiSubnetFailover = DbConnectionStringDefaults.MultiSubnetFailover; + internal static bool MultiSubnetFailover => DbConnectionStringDefaults.MultiSubnetFailover; internal const int Packet_Size = DbConnectionStringDefaults.PacketSize; internal const string Password = DbConnectionStringDefaults.Password; internal const bool Persist_Security_Info = DbConnectionStringDefaults.PersistSecurityInfo; diff --git a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs index 734a4377fa..e28b9e288e 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs @@ -32,6 +32,7 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable #if NETFRAMEWORK private readonly PropertyInfo _disableTnirByDefaultProperty; #endif + private readonly PropertyInfo _enableMultiSubnetFailoverByDefaultProperty; // These fields are used to capture the original switch values. private readonly FieldInfo _legacyRowVersionNullBehaviorField; @@ -48,10 +49,13 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable private readonly Tristate _useConnectionPoolV2Original; private readonly FieldInfo _ignoreServerProvidedFailoverPartnerField; private readonly Tristate _ignoreServerProvidedFailoverPartnerOriginal; -#if NETFRAMEWORK + #if NETFRAMEWORK private readonly FieldInfo _disableTnirByDefaultField; private readonly Tristate _disableTnirByDefaultOriginal; -#endif + #endif + private readonly FieldInfo _multiSubnetFailoverByDefaultField; + private readonly Tristate _multiSubnetFailoverByDefaultOriginal; + #endregion @@ -139,6 +143,10 @@ void InitProperty(string name, out PropertyInfo property) out _disableTnirByDefaultProperty); #endif + InitProperty( + "EnableMultiSubnetFailoverByDefault", + out _enableMultiSubnetFailoverByDefaultProperty); + // A local helper to capture the original value of a switch. void InitField(string name, out FieldInfo field, out Tristate value) { @@ -195,6 +203,11 @@ void InitField(string name, out FieldInfo field, out Tristate value) out _disableTnirByDefaultField, out _disableTnirByDefaultOriginal); #endif + + InitField( + "s_multiSubnetFailoverByDefault", + out _multiSubnetFailoverByDefaultField, + out _multiSubnetFailoverByDefaultOriginal); } /// @@ -255,6 +268,10 @@ void RestoreField(FieldInfo field, Tristate value) _disableTnirByDefaultOriginal); #endif + RestoreField( + _multiSubnetFailoverByDefaultField, + _multiSubnetFailoverByDefaultOriginal); + if (failedFields.Count > 0) { throw new Exception( @@ -327,6 +344,11 @@ public bool DisableTnirByDefault } #endif + public bool EnableMultiSubnetFailoverByDefault + { + get => (bool)_enableMultiSubnetFailoverByDefaultProperty.GetValue(null); + } + // These properties get or set the like-named underlying switch field value. // // They all fail the test if the value cannot be retrieved or set. @@ -408,6 +430,12 @@ public Tristate DisableTnirByDefaultField } #endif + public Tristate EnableMultiSubnetFailoverByDefaultField + { + get => GetValue(_multiSubnetFailoverByDefaultField); + set => SetValue(_multiSubnetFailoverByDefaultField, value); + } + #endregion #region Private Helpers diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/LocalAppContextSwitchesTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/LocalAppContextSwitchesTests.cs index 30896e545e..bd4dcb2b0c 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/LocalAppContextSwitchesTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/LocalAppContextSwitchesTests.cs @@ -20,6 +20,7 @@ public class LocalAppContextSwitchesTests #if NETFRAMEWORK [InlineData("DisableTnirByDefault", false)] #endif + [InlineData("EnableMultiSubnetFailoverByDefault", false)] public void DefaultSwitchValue(string property, bool expectedDefaultValue) { var switchesType = typeof(SqlCommand).Assembly.GetType("Microsoft.Data.SqlClient.LocalAppContextSwitches"); 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 52bc386875..601201d197 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 @@ -17,7 +17,7 @@ public SqlConnectionStringTest() _appContextSwitchHelper = new LocalAppContextSwitchesHelper(); } -#if NETFRAMEWORK + #if NETFRAMEWORK [Theory] [InlineData("test.database.windows.net", true, Tristate.True, true)] [InlineData("test.database.windows.net", false, Tristate.True, false)] @@ -60,7 +60,50 @@ public void TestDefaultTnir(string dataSource, bool? tnirEnabledInConnString, Tr // Assert Assert.Equal(expectedValue, connectionString.TransparentNetworkIPResolution); } -#endif + #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)); + } public void Dispose() {