From 97d320825f394ef632e70884d5558cd121d686d9 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Thu, 17 Jul 2025 07:40:56 +0100 Subject: [PATCH 1/9] Move globalization invariant mode detection to LocalAppContextSwitches --- .../Microsoft/Data/SqlClient/SqlConnection.cs | 53 +------------ .../Data/SqlClient/LocalAppContextSwitches.cs | 77 +++++++++++++++++-- 2 files changed, 71 insertions(+), 59 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs index ce5e960f8d..b1290ea526 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -31,13 +31,6 @@ namespace Microsoft.Data.SqlClient [DesignerCategory("")] public sealed partial class SqlConnection : DbConnection, ICloneable { - private enum CultureCheckState : uint - { - Unknown = 0, - Standard = 1, - Invariant = 2 - } - private bool _AsyncCommandInProgress; // SQLStatistics support @@ -75,9 +68,6 @@ private enum CultureCheckState : uint // using SqlConnection.Open() method. internal bool _applyTransientFaultHandling = false; - // status of invariant culture environment check - private static CultureCheckState _cultureCheckState; - // System column encryption key store providers are added by default private static readonly Dictionary s_systemColumnEncryptionKeyStoreProviders = new(capacity: 3, comparer: StringComparer.OrdinalIgnoreCase) @@ -1956,48 +1946,9 @@ private bool TryOpen(TaskCompletionSource retry, SqlConnec { SqlConnectionString connectionOptions = (SqlConnectionString)ConnectionOptions; - if (_cultureCheckState != CultureCheckState.Standard) + if (LocalAppContextSwitches.GlobalizationInvariantMode) { - // .NET Core 2.0 and up supports a Globalization Invariant Mode to reduce the size of - // required libraries for applications which don't need globalization support. SqlClient - // requires those libraries for core functionality and will throw exceptions later if they - // are not present. Throwing on open with a meaningful message helps identify the issue. - if (_cultureCheckState == CultureCheckState.Unknown) - { - // check if invariant state has been set by appcontext switch directly - if (AppContext.TryGetSwitch("System.Globalization.Invariant", out bool isEnabled) && isEnabled) - { - _cultureCheckState = CultureCheckState.Invariant; - } - else - { - // check if invariant state has been set through environment variables - string envValue = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"); - if (string.Equals(envValue, bool.TrueString, StringComparison.OrdinalIgnoreCase) || string.Equals(envValue, "1", StringComparison.OrdinalIgnoreCase)) - { - _cultureCheckState = CultureCheckState.Invariant; - } - else - { - // if it hasn't been manually set it could still apply if the os doesn't have - // icu libs installed or is a native binary with icu support trimmed away - // netcore 3.1 to net5 do not throw in attempting to create en-us in inariant mode - // net6 and greater will throw so catch and infer invariant mode from the exception - try - { - _cultureCheckState = CultureInfo.GetCultureInfo("en-US").EnglishName.Contains("Invariant") ? CultureCheckState.Invariant : CultureCheckState.Standard; - } - catch (CultureNotFoundException) - { - _cultureCheckState = CultureCheckState.Invariant; - } - } - } - } - if (_cultureCheckState == CultureCheckState.Invariant) - { - throw SQL.GlobalizationInvariantModeNotSupported(); - } + throw SQL.GlobalizationInvariantModeNotSupported(); } _applyTransientFaultHandling = (!overrides.HasFlag(SqlConnectionOverrides.OpenWithoutRetry) && connectionOptions != null && connectionOptions.ConnectRetryCount > 0); 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 dfd94453f2..bcac2b1bea 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -15,14 +15,14 @@ private enum Tristate : byte True = 2 } - internal const string MakeReadAsyncBlockingString = @"Switch.Microsoft.Data.SqlClient.MakeReadAsyncBlocking"; - internal const string LegacyRowVersionNullString = @"Switch.Microsoft.Data.SqlClient.LegacyRowVersionNullBehavior"; - internal const string SuppressInsecureTlsWarningString = @"Switch.Microsoft.Data.SqlClient.SuppressInsecureTLSWarning"; - internal const string UseMinimumLoginTimeoutString = @"Switch.Microsoft.Data.SqlClient.UseOneSecFloorInTimeoutCalculationDuringLogin"; - internal const string LegacyVarTimeZeroScaleBehaviourString = @"Switch.Microsoft.Data.SqlClient.LegacyVarTimeZeroScaleBehaviour"; - internal const string UseCompatibilityProcessSniString = @"Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni"; - internal const string UseCompatibilityAsyncBehaviourString = @"Switch.Microsoft.Data.SqlClient.UseCompatibilityAsyncBehaviour"; - internal const string UseConnectionPoolV2String = @"Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2"; + private const string MakeReadAsyncBlockingString = @"Switch.Microsoft.Data.SqlClient.MakeReadAsyncBlocking"; + private const string LegacyRowVersionNullString = @"Switch.Microsoft.Data.SqlClient.LegacyRowVersionNullBehavior"; + private const string SuppressInsecureTlsWarningString = @"Switch.Microsoft.Data.SqlClient.SuppressInsecureTLSWarning"; + private const string UseMinimumLoginTimeoutString = @"Switch.Microsoft.Data.SqlClient.UseOneSecFloorInTimeoutCalculationDuringLogin"; + private const string LegacyVarTimeZeroScaleBehaviourString = @"Switch.Microsoft.Data.SqlClient.LegacyVarTimeZeroScaleBehaviour"; + private const string UseCompatibilityProcessSniString = @"Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni"; + private const string UseCompatibilityAsyncBehaviourString = @"Switch.Microsoft.Data.SqlClient.UseCompatibilityAsyncBehaviour"; + private const string UseConnectionPoolV2String = @"Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2"; // 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; @@ -296,5 +296,66 @@ public static bool UseConnectionPoolV2 return s_useConnectionPoolV2 == Tristate.True; } } + +#if NET + private const string GlobalizationInvariantModeString = @"System.Globalization.Invariant"; + private const string GlobalizationInvariantModeEnvironmentVariable = "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"; + + private static Tristate s_globalizationInvariantMode; + + /// + /// .NET Core 2.0 and up supports Globalization Invariant mode, which reduces the size of the required libraries for + /// applications which don't need globalization support. SqlClient requires those libraries for core functionality, + /// and will throw exceptions later if they are not present. This switch allows SqlClient to detect this mode early. + /// + public static bool GlobalizationInvariantMode + { + get + { + if (s_globalizationInvariantMode == Tristate.NotInitialized) + { + // Check if invariant mode is has been set by the AppContext switch directly + if (AppContext.TryGetSwitch(GlobalizationInvariantModeString, out bool returnedValue) && returnedValue) + { + s_globalizationInvariantMode = Tristate.True; + } + else + { + // If the switch is not set, we check the environment variable as the first fallback + string envValue = Environment.GetEnvironmentVariable(GlobalizationInvariantModeEnvironmentVariable); + + if (string.Equals(envValue, bool.TrueString, StringComparison.OrdinalIgnoreCase) || string.Equals(envValue, "1", StringComparison.OrdinalIgnoreCase)) + { + s_globalizationInvariantMode = Tristate.True; + } + else + { + // If this hasn't been manually set, it could still apply if the OS doesn't have ICU libraries installed, + // or if the application is a native binary with ICU support trimmed away. + // .NET 3.1 to 5.0 do not throw in attempting to create en-US in invariant mode, but .NET 6+ does. In + // such cases, catch and infer invariant mode from the exception. + try + { + s_globalizationInvariantMode = System.Globalization.CultureInfo.GetCultureInfo("en-US").EnglishName.Contains("Invariant") + ? Tristate.True + : Tristate.False; + } + catch (System.Globalization.CultureNotFoundException) + { + // If the culture is not found, it means we are in invariant mode + s_globalizationInvariantMode = Tristate.True; + } + } + } + } + return s_globalizationInvariantMode == Tristate.True; + } + } +#else + /// + /// .NET Framework does not support Globalization Invariant mode, so this will always be false. + /// + public const bool GlobalizationInvariantMode = false; +#endif } } From b98ff4d23b8ffeb570844111051879c785a3ba4b Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Thu, 17 Jul 2025 23:26:49 +0100 Subject: [PATCH 2/9] Move TruncateScaledDecimal switch to LocalAppContextSwitches --- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 14 ++--------- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 14 ++--------- .../Data/SqlClient/LocalAppContextSwitches.cs | 24 +++++++++++++++++++ .../SqlClient/LocalAppContextSwitchesTest.cs | 1 + 4 files changed, 29 insertions(+), 24 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 4b6a82b7ba..c7c3d507d3 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 @@ -204,16 +204,6 @@ internal SqlInternalConnectionTds Connection } } - private static bool EnableTruncateSwitch - { - get - { - bool value; - value = AppContext.TryGetSwitch(enableTruncateSwitch, out value) ? value : false; - return value; - } - } - internal SqlInternalTransaction CurrentTransaction { get @@ -7653,7 +7643,7 @@ internal static SqlDecimal AdjustSqlDecimalScale(SqlDecimal d, int newScale) { if (d.Scale != newScale) { - bool round = !EnableTruncateSwitch; + bool round = !LocalAppContextSwitches.TruncateScaledDecimal; return SqlDecimal.AdjustScale(d, newScale - d.Scale, round); } @@ -7666,7 +7656,7 @@ internal static decimal AdjustDecimalScale(decimal value, int newScale) if (newScale != oldScale) { - bool round = !EnableTruncateSwitch; + bool round = !LocalAppContextSwitches.TruncateScaledDecimal; SqlDecimal num = new SqlDecimal(value); num = SqlDecimal.AdjustScale(num, newScale - oldScale, round); return num.Value; 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 f89613204f..382ec08f45 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 @@ -205,16 +205,6 @@ internal SqlInternalConnectionTds Connection } } - private static bool EnableTruncateSwitch - { - get - { - bool value; - value = AppContext.TryGetSwitch(enableTruncateSwitch, out value) ? value : false; - return value; - } - } - internal SqlInternalTransaction CurrentTransaction { get @@ -7849,7 +7839,7 @@ internal static SqlDecimal AdjustSqlDecimalScale(SqlDecimal d, int newScale) { if (d.Scale != newScale) { - bool round = !EnableTruncateSwitch; + bool round = !LocalAppContextSwitches.TruncateScaledDecimal; return SqlDecimal.AdjustScale(d, newScale - d.Scale, round); } @@ -7862,7 +7852,7 @@ internal static decimal AdjustDecimalScale(decimal value, int newScale) if (newScale != oldScale) { - bool round = !EnableTruncateSwitch; + bool round = !LocalAppContextSwitches.TruncateScaledDecimal; SqlDecimal num = new SqlDecimal(value); num = SqlDecimal.AdjustScale(num, newScale - oldScale, round); return num.Value; 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 bcac2b1bea..6a74344c89 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -23,6 +23,7 @@ private enum Tristate : byte private const string UseCompatibilityProcessSniString = @"Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni"; private const string UseCompatibilityAsyncBehaviourString = @"Switch.Microsoft.Data.SqlClient.UseCompatibilityAsyncBehaviour"; private const string UseConnectionPoolV2String = @"Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2"; + private const string TruncateScaledDecimalString = @"Switch.Microsoft.Data.SqlClient.TruncateScaledDecimal"; // 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; @@ -34,6 +35,7 @@ private enum Tristate : byte private static Tristate s_useCompatibilityProcessSni; private static Tristate s_useCompatibilityAsyncBehaviour; private static Tristate s_useConnectionPoolV2; + private static Tristate s_truncateScaledDecimal; #if NET static LocalAppContextSwitches() @@ -297,6 +299,28 @@ public static bool UseConnectionPoolV2 } } + /// + /// When set to true, TdsParser will truncate (rather than round) decimal and SqlDecimal values when scaling them. + /// + public static bool TruncateScaledDecimal + { + get + { + if (s_truncateScaledDecimal == Tristate.NotInitialized) + { + if (AppContext.TryGetSwitch(TruncateScaledDecimalString, out bool returnedValue) && returnedValue) + { + s_truncateScaledDecimal = Tristate.True; + } + else + { + s_truncateScaledDecimal = Tristate.False; + } + } + return s_truncateScaledDecimal == Tristate.True; + } + } + #if NET private const string GlobalizationInvariantModeString = @"System.Globalization.Invariant"; private const string GlobalizationInvariantModeEnvironmentVariable = "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"; 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 90ee094f8f..45e9415865 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 @@ -25,6 +25,7 @@ public void TestDefaultAppContextSwitchValues() Assert.False(LocalAppContextSwitches.UseCompatibilityProcessSni); Assert.False(LocalAppContextSwitches.UseCompatibilityAsyncBehaviour); Assert.False(LocalAppContextSwitches.UseConnectionPoolV2); + Assert.False(LocalAppContextSwitches.TruncateScaledDecimal); #if NETFRAMEWORK Assert.False(LocalAppContextSwitches.DisableTnirByDefault); #endif From 96f9dbcd3b589e5a6673991a7edd63eac476c1fa Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Fri, 18 Jul 2025 07:40:08 +0100 Subject: [PATCH 3/9] Move UseManagedSNI to LocalAppContextSwitches --- .../Data/SqlClient/TdsParser.Windows.cs | 2 +- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 6 +-- .../Sql/SqlDataSourceEnumerator.Windows.cs | 2 +- .../DbConnectionPoolIdentity.Windows.cs | 2 +- .../Data/SqlClient/LocalAppContextSwitches.cs | 37 +++++++++++++++++++ .../Data/SqlClient/TdsParserStateObject.cs | 4 +- .../TdsParserStateObjectFactory.Unix.cs | 3 -- .../TdsParserStateObjectFactory.Windows.cs | 30 +++++---------- .../SqlClient/LocalAppContextSwitchesTest.cs | 8 +++- 9 files changed, 60 insertions(+), 34 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs index 2c924b3b79..8074fda5a7 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs @@ -11,7 +11,7 @@ internal sealed partial class TdsParser { internal void PostReadAsyncForMars() { - if (TdsParserStateObjectFactory.UseManagedSNI) + if (LocalAppContextSwitches.UseManagedNetworking) return; // HACK HACK HACK - for Async only 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 c7c3d507d3..856b94605f 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 @@ -621,7 +621,7 @@ internal void EnableMars() // Cache physical stateObj and connection. _pMarsPhysicalConObj = _physicalStateObj; - if (TdsParserStateObjectFactory.UseManagedSNI) + if (LocalAppContextSwitches.UseManagedNetworking) _pMarsPhysicalConObj.IncrementPendingCallbacks(); uint info = 0; @@ -1479,7 +1479,7 @@ internal SqlError ProcessSNIError(TdsParserStateObject stateObj) * !=null | == 0 | replace text left of errorMessage */ - if (TdsParserStateObjectFactory.UseManagedSNI) + if (LocalAppContextSwitches.UseManagedNetworking) { Debug.Assert(!string.IsNullOrEmpty(details.ErrorMessage) || details.SniErrorNumber != 0, "Empty error message received from SNI"); SqlClientEventSource.Log.TryAdvancedTraceEvent(" Empty error message received from SNI. Error Message = {0}, SNI Error Number ={1}", details.ErrorMessage, details.SniErrorNumber); @@ -1528,7 +1528,7 @@ internal SqlError ProcessSNIError(TdsParserStateObject stateObj) } else { - if (TdsParserStateObjectFactory.UseManagedSNI) + if (LocalAppContextSwitches.UseManagedNetworking) { // SNI error. Append additional error message info if available and hasn't been included. string sniLookupMessage = SQL.GetSNIErrorMessage(details.SniErrorNumber); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceEnumerator.Windows.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceEnumerator.Windows.cs index 25caa8911b..f92e9beb32 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceEnumerator.Windows.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Sql/SqlDataSourceEnumerator.Windows.cs @@ -15,7 +15,7 @@ private partial DataTable GetDataSourcesInternal() #if NETFRAMEWORK return SqlDataSourceEnumeratorNativeHelper.GetDataSources(); #else - return SqlClient.TdsParserStateObjectFactory.UseManagedSNI ? SqlDataSourceEnumeratorManagedHelper.GetDataSources() : SqlDataSourceEnumeratorNativeHelper.GetDataSources(); + return SqlClient.LocalAppContextSwitches.UseManagedNetworking ? SqlDataSourceEnumeratorManagedHelper.GetDataSources() : SqlDataSourceEnumeratorNativeHelper.GetDataSources(); #endif } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolIdentity.Windows.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolIdentity.Windows.cs index 99783ff3c7..0c6fd07503 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolIdentity.Windows.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolIdentity.Windows.cs @@ -28,7 +28,7 @@ internal static WindowsIdentity GetCurrentWindowsIdentity() #else internal static DbConnectionPoolIdentity GetCurrent() { - return TdsParserStateObjectFactory.UseManagedSNI ? GetCurrentManaged() : GetCurrentNative(); + return LocalAppContextSwitches.UseManagedNetworking ? GetCurrentManaged() : GetCurrentNative(); } #endif 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 6a74344c89..81846bff73 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -324,8 +324,10 @@ public static bool TruncateScaledDecimal #if NET private const string GlobalizationInvariantModeString = @"System.Globalization.Invariant"; private const string GlobalizationInvariantModeEnvironmentVariable = "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"; + internal const string UseManagedNetworkingOnWindowsString = "Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows"; private static Tristate s_globalizationInvariantMode; + private static Tristate s_useManagedNetworkingOnWindows; /// /// .NET Core 2.0 and up supports Globalization Invariant mode, which reduces the size of the required libraries for @@ -375,11 +377,46 @@ public static bool GlobalizationInvariantMode return s_globalizationInvariantMode == Tristate.True; } } + + /// + /// When set to true, .NET Core will use the managed SNI implementation instead of the native SNI implementation. + /// + /// + /// Non-Windows platforms will always use the managed networking implementation. Windows platforms will use the native SNI + /// implementation by default, but this can be overridden by setting the AppContext switch + /// + public static bool UseManagedNetworking + { + get + { + if (s_useManagedNetworkingOnWindows == Tristate.NotInitialized) + { + if (!OperatingSystem.IsWindows()) + { + s_useManagedNetworkingOnWindows = Tristate.True; + } + else if (AppContext.TryGetSwitch(UseManagedNetworkingOnWindowsString, out bool returnedValue) && returnedValue) + { + s_useManagedNetworkingOnWindows = Tristate.True; + } + else + { + s_useManagedNetworkingOnWindows = Tristate.False; + } + } + return s_useManagedNetworkingOnWindows == Tristate.True; + } + } #else /// /// .NET Framework does not support Globalization Invariant mode, so this will always be false. /// public const bool GlobalizationInvariantMode = false; + + /// + /// .NET Framework does not support the managed SNI, so this will always be false. + /// + public const bool UseManagedNetworking = false; #endif } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs index 06d679e22a..bcb7fc9005 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs @@ -3224,7 +3224,7 @@ internal void ReadSni(TaskCompletionSource completion) ReadAsyncCallback(IntPtr.Zero, readPacket, 0); // Only release packet for Managed SNI as for Native SNI packet is released in finally block. - if (TdsParserStateObjectFactory.UseManagedSNI && readFromNetwork && !IsPacketEmpty(readPacket)) + if (LocalAppContextSwitches.UseManagedNetworking && readFromNetwork && !IsPacketEmpty(readPacket)) { ReleasePacket(readPacket); } @@ -3260,7 +3260,7 @@ internal void ReadSni(TaskCompletionSource completion) } finally { - if (!TdsParserStateObjectFactory.UseManagedSNI) + if (!LocalAppContextSwitches.UseManagedNetworking) { if (readFromNetwork && !IsPacketEmpty(readPacket)) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Unix.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Unix.cs index f7cb55d451..1362e47b45 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Unix.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Unix.cs @@ -8,9 +8,6 @@ namespace Microsoft.Data.SqlClient { internal sealed class TdsParserStateObjectFactory { - - public static bool UseManagedSNI => true; - public static readonly TdsParserStateObjectFactory Singleton = new TdsParserStateObjectFactory(); public EncryptionOptions EncryptionOptions => ManagedSni.SniLoadHandle.SingletonInstance.Options; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Windows.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Windows.cs index 8b8fe9186b..2e642a81d2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Windows.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Windows.cs @@ -13,28 +13,16 @@ internal sealed class TdsParserStateObjectFactory { public static readonly TdsParserStateObjectFactory Singleton = new TdsParserStateObjectFactory(); - private const string UseManagedNetworkingOnWindows = "Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows"; - -#if NET - private static bool s_shouldUseManagedSNI; - - // If the appcontext switch is set then Use Managed SNI based on the value. Otherwise Native SNI.dll will be used by default. - public static bool UseManagedSNI => - AppContext.TryGetSwitch(UseManagedNetworkingOnWindows, out s_shouldUseManagedSNI) ? s_shouldUseManagedSNI : false; -#else - public const bool UseManagedSNI = false; -#endif - public EncryptionOptions EncryptionOptions => #if NET - UseManagedSNI ? ManagedSni.SniLoadHandle.SingletonInstance.Options : SNILoadHandle.SingletonInstance.Options; + LocalAppContextSwitches.UseManagedNetworking ? ManagedSni.SniLoadHandle.SingletonInstance.Options : SNILoadHandle.SingletonInstance.Options; #else SNILoadHandle.SingletonInstance.Options; #endif public uint SNIStatus => #if NET - UseManagedSNI ? ManagedSni.SniLoadHandle.SingletonInstance.Status : SNILoadHandle.SingletonInstance.Status; + LocalAppContextSwitches.UseManagedNetworking ? ManagedSni.SniLoadHandle.SingletonInstance.Status : SNILoadHandle.SingletonInstance.Status; #else SNILoadHandle.SingletonInstance.Status; #endif @@ -44,7 +32,7 @@ internal sealed class TdsParserStateObjectFactory /// public bool ClientOSEncryptionSupport => #if NET - UseManagedSNI ? ManagedSni.SniLoadHandle.SingletonInstance.ClientOSEncryptionSupport : SNILoadHandle.SingletonInstance.ClientOSEncryptionSupport; + LocalAppContextSwitches.UseManagedNetworking ? ManagedSni.SniLoadHandle.SingletonInstance.ClientOSEncryptionSupport : SNILoadHandle.SingletonInstance.ClientOSEncryptionSupport; #else SNILoadHandle.SingletonInstance.ClientOSEncryptionSupport; #endif @@ -52,16 +40,16 @@ internal sealed class TdsParserStateObjectFactory public TdsParserStateObject CreateTdsParserStateObject(TdsParser parser) { #if NET - if (UseManagedSNI) + if (LocalAppContextSwitches.UseManagedNetworking) { - SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObjectFactory.CreateTdsParserStateObject | Info | Found AppContext switch '{0}' enabled, managed networking implementation will be used." - , UseManagedNetworkingOnWindows); + SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObjectFactory.CreateTdsParserStateObject | Info | Found AppContext switch '{0}' enabled, managed networking implementation will be used.", + LocalAppContextSwitches.UseManagedNetworkingOnWindowsString); return new TdsParserStateObjectManaged(parser); } else { - SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObjectFactory.CreateTdsParserStateObject | Info | AppContext switch '{0}' not enabled, native networking implementation will be used." - , UseManagedNetworkingOnWindows); + SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObjectFactory.CreateTdsParserStateObject | Info | AppContext switch '{0}' not enabled, native networking implementation will be used.", + LocalAppContextSwitches.UseManagedNetworkingOnWindowsString); return new TdsParserStateObjectNative(parser); } #else @@ -72,7 +60,7 @@ public TdsParserStateObject CreateTdsParserStateObject(TdsParser parser) internal TdsParserStateObject CreateSessionObject(TdsParser tdsParser, TdsParserStateObject _pMarsPhysicalConObj, bool v) { #if NET - if (TdsParserStateObjectFactory.UseManagedSNI) + if (LocalAppContextSwitches.UseManagedNetworking) { return new TdsParserStateObjectManaged(tdsParser, _pMarsPhysicalConObj, true); } 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 45e9415865..ec60f0d3cd 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 @@ -2,6 +2,7 @@ // 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 Xunit; namespace Microsoft.Data.SqlClient.UnitTests; @@ -26,8 +27,11 @@ public void TestDefaultAppContextSwitchValues() Assert.False(LocalAppContextSwitches.UseCompatibilityAsyncBehaviour); Assert.False(LocalAppContextSwitches.UseConnectionPoolV2); Assert.False(LocalAppContextSwitches.TruncateScaledDecimal); - #if NETFRAMEWORK +#if NETFRAMEWORK Assert.False(LocalAppContextSwitches.DisableTnirByDefault); - #endif + Assert.False(LocalAppContextSwitches.UseManagedNetworking); +#else + Assert.Equal(!OperatingSystem.IsWindows(), LocalAppContextSwitches.UseManagedNetworking); +#endif } } From 4a15cf74bb1451cba71a310e80c9c4c98379616a Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 19 Jul 2025 10:56:06 +0100 Subject: [PATCH 4/9] Add ILLink.Substitutions.xml resources to trim unused SNI --- .../netcore/src/Microsoft.Data.SqlClient.csproj | 10 ++++++++++ .../Data/SqlClient/LocalAppContextSwitches.cs | 12 ++++++------ .../src/Resources/ILLink.Substitutions.Unix.xml | 8 ++++++++ .../src/Resources/ILLink.Substitutions.Windows.xml | 9 +++++++++ 4 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.Unix.xml create mode 100644 src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.Windows.xml diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 01f4de3352..4d456aa09c 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -963,6 +963,11 @@ + + + ILLink.Substitutions.xml + Resources\ILLink.Substitutions.Windows.xml + @@ -1005,6 +1010,11 @@ + + + ILLink.Substitutions.xml + Resources\ILLink.Substitutions.Unix.xml + 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 81846bff73..55f83a8397 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -327,7 +327,7 @@ public static bool TruncateScaledDecimal internal const string UseManagedNetworkingOnWindowsString = "Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows"; private static Tristate s_globalizationInvariantMode; - private static Tristate s_useManagedNetworkingOnWindows; + private static Tristate s_useManagedNetworking; /// /// .NET Core 2.0 and up supports Globalization Invariant mode, which reduces the size of the required libraries for @@ -389,22 +389,22 @@ public static bool UseManagedNetworking { get { - if (s_useManagedNetworkingOnWindows == Tristate.NotInitialized) + if (s_useManagedNetworking == Tristate.NotInitialized) { if (!OperatingSystem.IsWindows()) { - s_useManagedNetworkingOnWindows = Tristate.True; + s_useManagedNetworking = Tristate.True; } else if (AppContext.TryGetSwitch(UseManagedNetworkingOnWindowsString, out bool returnedValue) && returnedValue) { - s_useManagedNetworkingOnWindows = Tristate.True; + s_useManagedNetworking = Tristate.True; } else { - s_useManagedNetworkingOnWindows = Tristate.False; + s_useManagedNetworking = Tristate.False; } } - return s_useManagedNetworkingOnWindows == Tristate.True; + return s_useManagedNetworking == Tristate.True; } } #else diff --git a/src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.Unix.xml b/src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.Unix.xml new file mode 100644 index 0000000000..4e105d25f0 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.Unix.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.Windows.xml b/src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.Windows.xml new file mode 100644 index 0000000000..e8577c7ae0 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.Windows.xml @@ -0,0 +1,9 @@ + + + + + + + + + From 408fb25b444d466e865c9edeebba4b1c5f8e5431 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Sat, 19 Jul 2025 11:02:18 +0100 Subject: [PATCH 5/9] Remove now-unused constants --- .../netcore/src/Microsoft/Data/SqlClient/TdsParser.cs | 1 - .../netfx/src/Microsoft/Data/SqlClient/TdsParser.cs | 1 - 2 files changed, 2 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 856b94605f..1925fd756b 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 @@ -69,7 +69,6 @@ internal sealed partial class TdsParser // Constants private const int constBinBufferSize = 4096; // Size of the buffer used to read input parameter of type Stream private const int constTextBufferSize = 4096; // Size of the buffer (in chars) user to read input parameter of type TextReader - private const string enableTruncateSwitch = "Switch.Microsoft.Data.SqlClient.TruncateScaledDecimal"; // for applications that need to maintain backwards compatibility with the previous behavior // State variables internal TdsParserState _state = TdsParserState.Closed; // status flag for connection 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 382ec08f45..e8b054ba40 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 @@ -69,7 +69,6 @@ internal sealed partial class TdsParser // Constants private const int constBinBufferSize = 4096; // Size of the buffer used to read input parameter of type Stream private const int constTextBufferSize = 4096; // Size of the buffer (in chars) user to read input parameter of type TextReader - private const string enableTruncateSwitch = "Switch.Microsoft.Data.SqlClient.TruncateScaledDecimal"; // for applications that need to maintain backwards compatibility with the previous behavior // State variables internal TdsParserState _state = TdsParserState.Closed; // status flag for connection From c5182f3a1fa056b8e2dbd74882ffea7829cb4872 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Wed, 23 Jul 2025 07:49:12 +0100 Subject: [PATCH 6/9] Respond to code review on SqlClient Move string constants and variables together; add a comment to UseManagedNetworking explaining that it can be turned into a constant by ILLink. --- .../Data/SqlClient/LocalAppContextSwitches.cs | 101 ++++++++++-------- .../TdsParserStateObjectFactory.Windows.cs | 6 +- 2 files changed, 57 insertions(+), 50 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 55f83a8397..ee5d088dc4 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -24,6 +24,13 @@ private enum Tristate : byte private const string UseCompatibilityAsyncBehaviourString = @"Switch.Microsoft.Data.SqlClient.UseCompatibilityAsyncBehaviour"; private const string UseConnectionPoolV2String = @"Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2"; private const string TruncateScaledDecimalString = @"Switch.Microsoft.Data.SqlClient.TruncateScaledDecimal"; +#if NET + private const string GlobalizationInvariantModeString = @"System.Globalization.Invariant"; + private const string GlobalizationInvariantModeEnvironmentVariable = "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"; + private const string UseManagedNetworkingOnWindowsString = "Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows"; +#else + private const string DisableTnirByDefaultString = @"Switch.Microsoft.Data.SqlClient.DisableTNIRByDefaultInConnectionString"; +#endif // 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; @@ -36,6 +43,12 @@ private enum Tristate : byte private static Tristate s_useCompatibilityAsyncBehaviour; private static Tristate s_useConnectionPoolV2; private static Tristate s_truncateScaledDecimal; +#if NET + private static Tristate s_globalizationInvariantMode; + private static Tristate s_useManagedNetworking; +#else + private static Tristate s_disableTnirByDefault; +#endif #if NET static LocalAppContextSwitches() @@ -53,44 +66,6 @@ static LocalAppContextSwitches() } #endif -#if NETFRAMEWORK - internal const string DisableTnirByDefaultString = @"Switch.Microsoft.Data.SqlClient.DisableTNIRByDefaultInConnectionString"; - private static Tristate s_disableTnirByDefault; - - /// - /// Transparent Network IP Resolution (TNIR) is a revision of the existing MultiSubnetFailover feature. - /// TNIR affects the connection sequence of the driver in the case where the first resolved IP of the hostname - /// doesn't respond and there are multiple IPs associated with the hostname. - /// - /// TNIR interacts with MultiSubnetFailover to provide the following three connection sequences: - /// 0: One IP is attempted, followed by all IPs in parallel - /// 1: All IPs are attempted in parallel - /// 2: All IPs are attempted one after another - /// - /// TransparentNetworkIPResolution is enabled by default. MultiSubnetFailover is disabled by default. - /// To disable TNIR, you can enable the app context switch. - /// - /// This app context switch defaults to 'false'. - /// - public static bool DisableTnirByDefault - { - get - { - if (s_disableTnirByDefault == Tristate.NotInitialized) - { - if (AppContext.TryGetSwitch(DisableTnirByDefaultString, out bool returnedValue) && returnedValue) - { - s_disableTnirByDefault = Tristate.True; - } - else - { - s_disableTnirByDefault = Tristate.False; - } - } - return s_disableTnirByDefault == Tristate.True; - } - } -#endif /// /// In TdsParser the ProcessSni function changed significantly when the packet /// multiplexing code needed for high speed multi-packet column values was added. @@ -322,13 +297,6 @@ public static bool TruncateScaledDecimal } #if NET - private const string GlobalizationInvariantModeString = @"System.Globalization.Invariant"; - private const string GlobalizationInvariantModeEnvironmentVariable = "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"; - internal const string UseManagedNetworkingOnWindowsString = "Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows"; - - private static Tristate s_globalizationInvariantMode; - private static Tristate s_useManagedNetworking; - /// /// .NET Core 2.0 and up supports Globalization Invariant mode, which reduces the size of the required libraries for /// applications which don't need globalization support. SqlClient requires those libraries for core functionality, @@ -382,8 +350,15 @@ public static bool GlobalizationInvariantMode /// When set to true, .NET Core will use the managed SNI implementation instead of the native SNI implementation. /// /// + /// /// Non-Windows platforms will always use the managed networking implementation. Windows platforms will use the native SNI - /// implementation by default, but this can be overridden by setting the AppContext switch + /// implementation by default, but this can be overridden by setting the AppContext switch. + /// + /// + /// ILLink.Substitutions.xml allows the unused SNI implementation to be trimmed away when the corresponding AppContext + /// switch is set at compile time. In such cases, this property will return a constant value, even if the AppContext switch is + /// set or reset at runtime. See the ILLink.Substitutions.Windows.xml and ILLink.Substitutions.Unix.xml resource files for details. + /// /// public static bool UseManagedNetworking { @@ -417,6 +392,40 @@ public static bool UseManagedNetworking /// .NET Framework does not support the managed SNI, so this will always be false. /// public const bool UseManagedNetworking = false; + + /// + /// Transparent Network IP Resolution (TNIR) is a revision of the existing MultiSubnetFailover feature. + /// TNIR affects the connection sequence of the driver in the case where the first resolved IP of the hostname + /// doesn't respond and there are multiple IPs associated with the hostname. + /// + /// TNIR interacts with MultiSubnetFailover to provide the following three connection sequences: + /// 0: One IP is attempted, followed by all IPs in parallel + /// 1: All IPs are attempted in parallel + /// 2: All IPs are attempted one after another + /// + /// TransparentNetworkIPResolution is enabled by default. MultiSubnetFailover is disabled by default. + /// To disable TNIR, you can enable the app context switch. + /// + /// This app context switch defaults to 'false'. + /// + public static bool DisableTnirByDefault + { + get + { + if (s_disableTnirByDefault == Tristate.NotInitialized) + { + if (AppContext.TryGetSwitch(DisableTnirByDefaultString, out bool returnedValue) && returnedValue) + { + s_disableTnirByDefault = Tristate.True; + } + else + { + s_disableTnirByDefault = Tristate.False; + } + } + return s_disableTnirByDefault == Tristate.True; + } + } #endif } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Windows.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Windows.cs index 2e642a81d2..53cbc4decc 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Windows.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectFactory.Windows.cs @@ -42,14 +42,12 @@ public TdsParserStateObject CreateTdsParserStateObject(TdsParser parser) #if NET if (LocalAppContextSwitches.UseManagedNetworking) { - SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObjectFactory.CreateTdsParserStateObject | Info | Found AppContext switch '{0}' enabled, managed networking implementation will be used.", - LocalAppContextSwitches.UseManagedNetworkingOnWindowsString); + SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObjectFactory.CreateTdsParserStateObject | Info | Using managed networking implementation."); return new TdsParserStateObjectManaged(parser); } else { - SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObjectFactory.CreateTdsParserStateObject | Info | AppContext switch '{0}' not enabled, native networking implementation will be used.", - LocalAppContextSwitches.UseManagedNetworkingOnWindowsString); + SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObjectFactory.CreateTdsParserStateObject | Info | Using native networking implementation."); return new TdsParserStateObjectNative(parser); } #else From 5fe7dd035999206c891452e01f8b7b51e989212e Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Wed, 23 Jul 2025 20:46:58 +0100 Subject: [PATCH 7/9] Sync LocalAppContextSwitchesHelper --- .../Common/LocalAppContextSwitchesHelper.cs | 115 ++++++++++++++++-- 1 file changed, 108 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs index 2970b1f1ce..df68f86677 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs @@ -30,7 +30,11 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable private readonly PropertyInfo _useCompatibilityProcessSniProperty; private readonly PropertyInfo _useCompatibilityAsyncBehaviourProperty; private readonly PropertyInfo _useConnectionPoolV2Property; - #if NETFRAMEWORK + private readonly PropertyInfo _truncateScaledDecimalProperty; + #if NET + private readonly PropertyInfo _globalizationInvariantModeProperty; + private readonly PropertyInfo _useManagedNetworkingProperty; + #else private readonly PropertyInfo _disableTnirByDefaultProperty; #endif @@ -51,7 +55,14 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable private readonly Tristate _useCompatibilityAsyncBehaviourOriginal; private readonly FieldInfo _useConnectionPoolV2Field; private readonly Tristate _useConnectionPoolV2Original; - #if NETFRAMEWORK + private readonly FieldInfo _truncateScaledDecimalField; + private readonly Tristate _truncateScaledDecimalOriginal; + #if NET + private readonly FieldInfo _globalizationInvariantModeField; + private readonly Tristate _globalizationInvariantModeOriginal; + private readonly FieldInfo _useManagedNetworkingField; + private readonly Tristate _useManagedNetworkingOriginal; + #else private readonly FieldInfo _disableTnirByDefaultField; private readonly Tristate _disableTnirByDefaultOriginal; #endif @@ -140,7 +151,19 @@ void InitProperty(string name, out PropertyInfo property) "UseConnectionPoolV2", out _useConnectionPoolV2Property); - #if NETFRAMEWORK + InitProperty( + "TruncateScaledDecimal", + out _truncateScaledDecimalProperty); + + #if NET + InitProperty( + "GlobalizationInvariantMode", + out _globalizationInvariantModeProperty); + + InitProperty( + "UseManagedNetworking", + out _useManagedNetworkingProperty); + #else InitProperty( "DisableTnirByDefault", out _disableTnirByDefaultProperty); @@ -201,7 +224,22 @@ void InitField(string name, out FieldInfo field, out Tristate value) out _useConnectionPoolV2Field, out _useConnectionPoolV2Original); - #if NETFRAMEWORK + InitField( + "s_truncateScaledDecimal", + out _truncateScaledDecimalField, + out _truncateScaledDecimalOriginal); + + #if NET + InitField( + "s_globalizationInvariantMode", + out _globalizationInvariantModeField, + out _globalizationInvariantModeOriginal); + + InitField( + "s_useManagedNetworking", + out _useManagedNetworkingField, + out _useManagedNetworkingOriginal); + #else InitField( "s_disableTnirByDefault", out _disableTnirByDefaultField, @@ -265,7 +303,19 @@ void RestoreField(FieldInfo field, Tristate value) _useConnectionPoolV2Field, _useConnectionPoolV2Original); - #if NETFRAMEWORK + RestoreField( + _truncateScaledDecimalField, + _truncateScaledDecimalOriginal); + + #if NET + RestoreField( + _globalizationInvariantModeField, + _globalizationInvariantModeOriginal); + + RestoreField( + _useManagedNetworkingField, + _useManagedNetworkingOriginal); + #else RestoreField( _disableTnirByDefaultField, _disableTnirByDefaultOriginal); @@ -350,7 +400,31 @@ public bool UseConnectionPoolV2 get => (bool)_useConnectionPoolV2Property.GetValue(null); } - #if NETFRAMEWORK + /// + /// Access the LocalAppContextSwitches.TruncateScaledDecimal property. + /// + public bool TruncateScaledDecimal + { + get => (bool)_truncateScaledDecimalProperty.GetValue(null); + } + + #if NET + /// + /// Access the LocalAppContextSwitches.GlobalizationInvariantMode property. + /// + public bool GlobalizationInvariantMode + { + get => (bool)_globalizationInvariantModeProperty.GetValue(null); + } + + /// + /// Access the LocalAppContextSwitches.UseManagedNetworking property. + /// + public bool UseManagedNetworking + { + get => (bool)_useManagedNetworkingProperty.GetValue(null); + } + #else /// /// Access the LocalAppContextSwitches.DisableTnirByDefault property. /// @@ -443,7 +517,34 @@ public Tristate UseConnectionPoolV2Field set => SetValue(_useConnectionPoolV2Field, value); } - #if NETFRAMEWORK + /// + /// Get or set the LocalAppContextSwitches.TruncateScaledDecimal switch value. + /// + public Tristate TruncateScaledDecimalField + { + get => GetValue(_truncateScaledDecimalField); + set => SetValue(_truncateScaledDecimalField, value); + } + + #if NET + /// + /// Get or set the LocalAppContextSwitches.GlobalizationInvariantMode switch value. + /// + public Tristate GlobalizationInvariantModeField + { + get => GetValue(_globalizationInvariantModeField); + set => SetValue(_globalizationInvariantModeField, value); + } + + /// + /// Get or set the LocalAppContextSwitches.UseManagedNetworking switch value. + /// + public Tristate UseManagedNetworkingField + { + get => GetValue(_useManagedNetworkingField); + set => SetValue(_useManagedNetworkingField, value); + } + #else /// /// Get or set the LocalAppContextSwitches.DisableTnirByDefault switch /// value. From bae4604e17f8c7648506111afd612a3f32582dee Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Wed, 23 Jul 2025 20:56:15 +0100 Subject: [PATCH 8/9] Updated TestScaledDecimalParameter tests These tests no longer directly set the AppContext switches to round or truncate decimals, since these switches are cached when first queried. --- .../SQL/ParameterTest/ParametersTest.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs index bf99691907..d98fe6abd4 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs @@ -11,6 +11,8 @@ using System.Threading.Tasks; using Xunit; using System.Globalization; +using Microsoft.Data.SqlClient.Tests.Common; + #if !NETFRAMEWORK using Microsoft.SqlServer.Types; @@ -570,13 +572,16 @@ public static void SqlDecimalConvertToDecimal_TestOutOfRange(string sqlDecimalVa [ClassData(typeof(ConnectionStringsProvider))] public static void TestScaledDecimalParameter_CommandInsert(string connectionString, bool truncateScaledDecimal) { + using LocalAppContextSwitchesHelper appContextSwitchesHelper = new(); + string tableName = DataTestUtility.GetUniqueNameForSqlServer("TestDecimalParameterCMD"); using SqlConnection connection = InitialDatabaseTable(connectionString, tableName); try { using (SqlCommand cmd = connection.CreateCommand()) { - AppContext.SetSwitch(TruncateDecimalSwitch, truncateScaledDecimal); + appContextSwitchesHelper.TruncateScaledDecimalField = truncateScaledDecimal ? LocalAppContextSwitchesHelper.Tristate.True : LocalAppContextSwitchesHelper.Tristate.False; + var p = new SqlParameter("@Value", null) { Precision = 18, @@ -602,6 +607,8 @@ public static void TestScaledDecimalParameter_CommandInsert(string connectionStr [ClassData(typeof(ConnectionStringsProvider))] public static void TestScaledDecimalParameter_BulkCopy(string connectionString, bool truncateScaledDecimal) { + using LocalAppContextSwitchesHelper appContextSwitchesHelper = new(); + string tableName = DataTestUtility.GetUniqueNameForSqlServer("TestDecimalParameterBC"); using SqlConnection connection = InitialDatabaseTable(connectionString, tableName); try @@ -620,7 +627,7 @@ public static void TestScaledDecimalParameter_BulkCopy(string connectionString, } bulkCopy.DestinationTableName = tableName; - AppContext.SetSwitch(TruncateDecimalSwitch, truncateScaledDecimal); + appContextSwitchesHelper.TruncateScaledDecimalField = truncateScaledDecimal ? LocalAppContextSwitchesHelper.Tristate.True : LocalAppContextSwitchesHelper.Tristate.False; bulkCopy.WriteToServer(table); } Assert.True(ValidateInsertedValues(connection, tableName, truncateScaledDecimal), $"Invalid test happened with connection string [{connection.ConnectionString}]"); @@ -636,6 +643,8 @@ public static void TestScaledDecimalParameter_BulkCopy(string connectionString, [ClassData(typeof(ConnectionStringsProvider))] public static void TestScaledDecimalTVP_CommandSP(string connectionString, bool truncateScaledDecimal) { + using LocalAppContextSwitchesHelper appContextSwitchesHelper = new(); + string tableName = DataTestUtility.GetUniqueNameForSqlServer("TestDecimalParameterBC"); string tableTypeName = DataTestUtility.GetUniqueNameForSqlServer("UDTTTestDecimalParameterBC"); string spName = DataTestUtility.GetUniqueNameForSqlServer("spTestDecimalParameterBC"); @@ -663,7 +672,7 @@ public static void TestScaledDecimalTVP_CommandSP(string connectionString, bool table.Rows.Add(newRow); } p.Value = table; - AppContext.SetSwitch(TruncateDecimalSwitch, truncateScaledDecimal); + appContextSwitchesHelper.TruncateScaledDecimalField = truncateScaledDecimal ? LocalAppContextSwitchesHelper.Tristate.True : LocalAppContextSwitchesHelper.Tristate.False; cmd.ExecuteNonQuery(); } // TVP always rounds data without attention to the configuration. From 880396aff069fb3c5718e2507d3f499a5abf3895 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:02:08 +0100 Subject: [PATCH 9/9] Remove hardcoded reference to TruncateScaledDecimal switch --- .../SQL/SqlBulkCopyTest/AdjustPrecScaleForBulkCopy.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AdjustPrecScaleForBulkCopy.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AdjustPrecScaleForBulkCopy.cs index 72bab47869..117ab67c0d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AdjustPrecScaleForBulkCopy.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AdjustPrecScaleForBulkCopy.cs @@ -5,6 +5,7 @@ using System; using System.Data; using System.Data.SqlTypes; +using Microsoft.Data.SqlClient.Tests.Common; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests @@ -14,6 +15,7 @@ public static class AdjustPrecScaleForBulkCopy [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] public static void RunTest() { + using LocalAppContextSwitchesHelper appContextSwitches = new(); SqlDecimal value = BulkCopySqlDecimalToTable(new SqlDecimal(0), 1, 0, 2, 2); Assert.Equal("0.00", value.ToString()); @@ -27,7 +29,7 @@ public static void RunTest() Assert.Equal("12.3", value.ToString()); value = BulkCopySqlDecimalToTable(new SqlDecimal(123.45), 10, 2, 4, 1); - if (AppContext.TryGetSwitch("Switch.Microsoft.Data.SqlClient.TruncateScaledDecimal", out bool switchValue) && switchValue) + if (appContextSwitches.TruncateScaledDecimal) { Assert.Equal("123.4", value.ToString()); }