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 09d84e0ea7..74fd226728 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -4,540 +4,673 @@ using System; -namespace Microsoft.Data.SqlClient +#nullable enable + +namespace Microsoft.Data.SqlClient; + +/// +/// This class provides immutable access to the app context switches used +/// throughout the codebase. Each switch value is read once (on first access) +/// and cached for future access. Most switch values are obtained solely from +/// the AppContext, but some switches may use other mechanisms to determine +/// their values as well (e.g. environment variables). +/// +internal static class LocalAppContextSwitches { - internal static partial class LocalAppContextSwitches + #region Switch Names + + #if NETFRAMEWORK + /// + /// The name of the app context switch that controls whether TNIR is + /// disabled by default in the connection string. + /// + private const string DisableTnirByDefaultString = + "Switch.Microsoft.Data.SqlClient.DisableTNIRByDefaultInConnectionString"; + #endif + + /// + /// The name of the app context switch that controls whether + /// MultiSubnetFailover is enabled by default in the connection string. + /// + private const string EnableMultiSubnetFailoverByDefaultString = + "Switch.Microsoft.Data.SqlClient.EnableMultiSubnetFailoverByDefault"; + + /// + /// The name of the app context switch that controls whether + /// the user agent feature is enabled. + /// + private const string EnableUserAgentString = + "Switch.Microsoft.Data.SqlClient.EnableUserAgent"; + + #if NET + /// + /// The name of the app context switch that controls whether + /// Globalization Invariant mode is enabled. + /// + private const string GlobalizationInvariantModeString = + "System.Globalization.Invariant"; + /// + /// The name of the environment variable that controls whether + /// Globalization Invariant mode is enabled. + /// + private const string GlobalizationInvariantModeEnvironmentVariable = + "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"; + #endif + + /// + /// The name of the app context switch that controls whether the failover + /// partner provided by the server during connection will be ignored. + /// + private const string IgnoreServerProvidedFailoverPartnerString = + "Switch.Microsoft.Data.SqlClient.IgnoreServerProvidedFailoverPartner"; + + /// + /// The name of the app context switch that controls whether to preserve + /// legacy behavior where Timestamp/RowVersion fields return empty byte + /// arrays instead of null. + /// + private const string LegacyRowVersionNullString = + "Switch.Microsoft.Data.SqlClient.LegacyRowVersionNullBehavior"; + + /// + /// The name of the app context switch that controls whether to preserve + /// legacy VarTime zero scale behavior for datetime2, datetimeoffset, and + /// time data types. + /// + private const string LegacyVarTimeZeroScaleBehaviourString = + "Switch.Microsoft.Data.SqlClient.LegacyVarTimeZeroScaleBehaviour"; + + /// + /// The name of the app context switch that controls whether + /// ReadAsync operations run synchronously and block the calling thread. + /// + private const string MakeReadAsyncBlockingString = + "Switch.Microsoft.Data.SqlClient.MakeReadAsyncBlocking"; + + /// + /// The name of the app context switch that controls whether to suppress + /// the security warning when using Encrypt=false with TLS 1.2 or lower. + /// + private const string SuppressInsecureTlsWarningString = + "Switch.Microsoft.Data.SqlClient.SuppressInsecureTLSWarning"; + + /// + /// The name of the app context switch that controls whether TdsParser + /// truncates (rather than rounds) decimal and SqlDecimal values when + /// scaling them. + /// + private const string TruncateScaledDecimalString = + "Switch.Microsoft.Data.SqlClient.TruncateScaledDecimal"; + + /// + /// The name of the app context switch that controls whether to use legacy + /// async multi-packet column value fetch behavior without continue snapshot + /// state. + /// + private const string UseCompatibilityAsyncBehaviourString = + "Switch.Microsoft.Data.SqlClient.UseCompatibilityAsyncBehaviour"; + + /// + /// The name of the app context switch that controls whether to use the old + /// ProcessSni design instead of the packet multiplexing implementation. + /// + private const string UseCompatibilityProcessSniString = + "Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni"; + + /// + /// The name of the app context switch that controls whether to use the new + /// V2 connection pool implementation or the legacy V1 implementation. + /// + private const string UseConnectionPoolV2String = + "Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2"; + + #if NET && _WINDOWS + /// + /// The name of the app context switch that controls whether to use the + /// managed SNI implementation instead of the native SNI implementation on + /// Windows. + /// + private const string UseManagedNetworkingOnWindowsString = + "Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows"; + #endif + + /// + /// The name of the app context switch that controls whether to enforce + /// a minimum login timeout of 1 second instead of 0 seconds. + /// + private const string UseMinimumLoginTimeoutString = + "Switch.Microsoft.Data.SqlClient.UseOneSecFloorInTimeoutCalculationDuringLogin"; + + #endregion + + #region Switch Values + + /// + /// We use a byte-based enum to track the value of each switch. This plays + /// nicely with threaded access. A nullable bool would seem to be the + /// obvious choice, but the way nullable bools are implemented in the CLR + /// makes them not thread-safe without using locks (the HasValue and Value + /// properties can get out of sync if one thread is writing while another is + /// reading). + /// + private enum SwitchValue : byte { - // @TODO: Replace with `bool?` since that's exactly how this is being used - private enum Tristate : byte - { - NotInitialized = 0, - False = 1, - True = 2 - } + None = 0, + True = 1, + False = 2 + } - 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"; - 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 EnableMultiSubnetFailoverByDefaultString = @"Switch.Microsoft.Data.SqlClient.EnableMultiSubnetFailoverByDefault"; - - #if NET - private const string GlobalizationInvariantModeString = @"System.Globalization.Invariant"; - private const string GlobalizationInvariantModeEnvironmentVariable = "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"; - - #if _WINDOWS - private const string UseManagedNetworkingOnWindowsString = "Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows"; - #endif - #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; - private static Tristate s_suppressInsecureTlsWarning; - private static Tristate s_makeReadAsyncBlocking; - private static Tristate s_useMinimumLoginTimeout; - // this field is accessed through reflection in Microsoft.Data.SqlClient.Tests.SqlParameterTests and should not be renamed or have the type changed without refactoring related tests - private static Tristate s_legacyVarTimeZeroScaleBehaviour; - private static Tristate s_useCompatibilityProcessSni; - private static Tristate s_useCompatibilityAsyncBehaviour; - private static Tristate s_useConnectionPoolV2; - 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; - - #if _WINDOWS - private static Tristate s_useManagedNetworking; - #endif - #else - private static Tristate s_disableTnirByDefault; - #endif - - #if NET - static LocalAppContextSwitches() - { - IAppContextSwitchOverridesSection appContextSwitch = AppConfigManager.FetchConfigurationSection(AppContextSwitchOverridesSection.Name); + // GOTCHA: These fields are accessed via reflection by the + // LocalAppContextSwitchesHelper test helper class. If you rename them, be + // sure to update the test helper as well. + + #if NETFRAMEWORK + /// + /// The cached value of the DisableTnirByDefault switch. + /// + private static SwitchValue s_disableTnirByDefault = SwitchValue.None; + #endif - try - { - SqlAppContextSwitchManager.ApplyContextSwitches(appContextSwitch); - } - catch (Exception e) - { - // @TODO: Adopt netcore style of trace logs - // Don't throw an exception for an invalid config file - SqlClientEventSource.Log.TryTraceEvent(": {1}", nameof(LocalAppContextSwitches), e); - } - } - #endif - - // @TODO: Sort by name - - /// - /// In TdsParser, the ProcessSni function changed significantly when the packet - /// multiplexing code needed for high speed multi-packet column values was added. - /// When this switch is set to true (the default), the old ProcessSni design is used. - /// When this switch is set to false, the new experimental ProcessSni behavior using - /// the packet multiplexer is enabled. - /// - public static bool UseCompatibilityProcessSni - { - get - { - if (s_useCompatibilityProcessSni == Tristate.NotInitialized) - { - // Check if the switch has been set by the AppContext switch directly - // If it has not been set, we default to true. - if (!AppContext.TryGetSwitch(UseCompatibilityProcessSniString, out bool returnedValue) || returnedValue) - { - s_useCompatibilityProcessSni = Tristate.True; - } - else - { - s_useCompatibilityProcessSni = Tristate.False; - } - } - return s_useCompatibilityProcessSni == Tristate.True; - } - } + /// + /// The cached value of the EnableMultiSubnetFailoverByDefault switch. + /// + private static SwitchValue s_enableMultiSubnetFailoverByDefault = SwitchValue.None; - /// - /// In TdsParser, the async multi-packet column value fetch behavior can use a continue snapshot state - /// for improved efficiency. When this switch is enabled (the default), the driver preserves the legacy - /// compatibility behavior, which does not use the continue snapshot state. When disabled, the new behavior - /// using the continue snapshot state is enabled. This switch will always return true if - /// is enabled, because the continue state is not stable without - /// the multiplexer. - /// - public static bool UseCompatibilityAsyncBehaviour - { - get - { - if (UseCompatibilityProcessSni) - { - // If ProcessSni compatibility mode has been enabled then the packet - // multiplexer has been disabled. The new async behaviour using continue - // point capture is only stable if the multiplexer is enabled so we must - // return true to enable compatibility async behaviour using only restarts. - return true; - } - - if (s_useCompatibilityAsyncBehaviour == Tristate.NotInitialized) - { - if (!AppContext.TryGetSwitch(UseCompatibilityAsyncBehaviourString, out bool returnedValue) || returnedValue) - { - s_useCompatibilityAsyncBehaviour = Tristate.True; - } - else - { - s_useCompatibilityAsyncBehaviour = Tristate.False; - } - } - return s_useCompatibilityAsyncBehaviour == Tristate.True; - } - } + /// + /// The cached value of the EnableUserAgent switch. + /// + private static SwitchValue s_enableUserAgent = SwitchValue.None; + + #if NET + /// + /// The cached value of the GlobalizationInvariantMode switch. + /// + private static SwitchValue s_globalizationInvariantMode = SwitchValue.None; + #endif + + /// + /// The cached value of the IgnoreServerProvidedFailoverPartner switch. + /// + private static SwitchValue s_ignoreServerProvidedFailoverPartner = SwitchValue.None; + + /// + /// The cached value of the LegacyRowVersionNullBehavior switch. + /// + private static SwitchValue s_legacyRowVersionNullBehavior = SwitchValue.None; + + /// + /// The cached value of the LegacyVarTimeZeroScaleBehaviour switch. + /// + private static SwitchValue s_legacyVarTimeZeroScaleBehaviour = SwitchValue.None; + + /// + /// The cached value of the MakeReadAsyncBlocking switch. + /// + private static SwitchValue s_makeReadAsyncBlocking = SwitchValue.None; - /// - /// When using Encrypt=false in the connection string, a security warning is output to the console if the TLS version is 1.2 or lower. - /// This warning can be suppressed by enabling this AppContext switch. - /// This app context switch defaults to 'false'. - /// - public static bool SuppressInsecureTlsWarning + /// + /// The cached value of the SuppressInsecureTlsWarning switch. + /// + private static SwitchValue s_suppressInsecureTlsWarning = SwitchValue.None; + + /// + /// The cached value of the TruncateScaledDecimal switch. + /// + private static SwitchValue s_truncateScaledDecimal = SwitchValue.None; + + /// + /// The cached value of the UseCompatibilityAsyncBehaviour switch. + /// + private static SwitchValue s_useCompatibilityAsyncBehaviour = SwitchValue.None; + + /// + /// The cached value of the UseCompatibilityProcessSni switch. + /// + private static SwitchValue s_useCompatibilityProcessSni = SwitchValue.None; + + /// + /// The cached value of the UseConnectionPoolV2 switch. + /// + private static SwitchValue s_useConnectionPoolV2 = SwitchValue.None; + + #if NET && _WINDOWS + /// + /// The cached value of the UseManagedNetworking switch. + /// + private static SwitchValue s_useManagedNetworking = SwitchValue.None; + #endif + + /// + /// The cached value of the UseMinimumLoginTimeout switch. + /// + private static SwitchValue s_useMinimumLoginTimeout = SwitchValue.None; + + #endregion + + #region Static Initialization + + #if NET + /// + /// Static construction for .NET reads the AppContextSwitchOverridesSection + /// from the default app config file and applies any switch values found + /// there to the AppContext. These values are then later read and cached by + /// the individual switch properties on first access. + /// + static LocalAppContextSwitches() + { + IAppContextSwitchOverridesSection appContextSwitch = AppConfigManager.FetchConfigurationSection(AppContextSwitchOverridesSection.Name); + + try { - get - { - if (s_suppressInsecureTlsWarning == Tristate.NotInitialized) - { - if (AppContext.TryGetSwitch(SuppressInsecureTlsWarningString, out bool returnedValue) && returnedValue) - { - s_suppressInsecureTlsWarning = Tristate.True; - } - else - { - s_suppressInsecureTlsWarning = Tristate.False; - } - } - return s_suppressInsecureTlsWarning == Tristate.True; - } + SqlAppContextSwitchManager.ApplyContextSwitches(appContextSwitch); } - - /// - /// In System.Data.SqlClient and Microsoft.Data.SqlClient prior to 3.0.0 a field with type Timestamp/RowVersion - /// would return an empty byte array. This switch controls whether to preserve that behaviour on newer versions - /// of Microsoft.Data.SqlClient, if this switch returns false an appropriate null value will be returned. - /// This app context switch defaults to 'false'. - /// - public static bool LegacyRowVersionNullBehavior + catch (Exception e) { - get - { - if (s_legacyRowVersionNullBehavior == Tristate.NotInitialized) - { - if (AppContext.TryGetSwitch(LegacyRowVersionNullString, out bool returnedValue) && returnedValue) - { - s_legacyRowVersionNullBehavior = Tristate.True; - } - else - { - s_legacyRowVersionNullBehavior = Tristate.False; - } - } - return s_legacyRowVersionNullBehavior == Tristate.True; - } + // @TODO: Adopt netcore style of trace logs + // Don't throw an exception for an invalid config file + SqlClientEventSource.Log.TryTraceEvent(": {1}", nameof(LocalAppContextSwitches), e); } + } + #endif + + #endregion + + #region Switch Properties + + #if NETFRAMEWORK + /// + /// 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. + /// + /// The default value of this switch is false. + /// + public static bool DisableTnirByDefault => + AcquireAndReturn( + DisableTnirByDefaultString, + defaultValue: false, + ref s_disableTnirByDefault); + #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. + /// + /// The default value of this switch is false. + /// + public static bool EnableMultiSubnetFailoverByDefault => + AcquireAndReturn( + EnableMultiSubnetFailoverByDefaultString, + defaultValue: false, + ref s_enableMultiSubnetFailoverByDefault); - /// - /// When enabled, ReadAsync runs asynchronously and does not block the calling thread. - /// This app context switch defaults to 'false'. - /// - public static bool MakeReadAsyncBlocking + /// + /// When set to true, the user agent feature is enabled and the driver will + /// send the user agent string to the server. + /// + /// The default value of this switch is false. + /// + public static bool EnableUserAgent => + AcquireAndReturn( + EnableUserAgentString, + defaultValue: false, + ref s_enableUserAgent); + + #if NET + /// + /// .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. + /// + /// The value of this switch is determined first by checking the AppContext. + /// If not set there, it falls back to checking the environment variable + /// DOTNET_SYSTEM_GLOBALIZATION_INVARIANT. If neither is set, it attempts to + /// create the "en-US" culture and infers invariant mode from whether that + /// operation throws an exception. + /// + /// The default value of this switch is false. + /// + public static bool GlobalizationInvariantMode + { + get { - get + if (s_globalizationInvariantMode != SwitchValue.None) { - if (s_makeReadAsyncBlocking == Tristate.NotInitialized) - { - if (AppContext.TryGetSwitch(MakeReadAsyncBlockingString, out bool returnedValue) && returnedValue) - { - s_makeReadAsyncBlocking = Tristate.True; - } - else - { - s_makeReadAsyncBlocking = Tristate.False; - } - } - return s_makeReadAsyncBlocking == Tristate.True; + return s_globalizationInvariantMode == SwitchValue.True; } - } - /// - /// Specifies minimum login timeout to be set to 1 second instead of 0 seconds, - /// to prevent a login attempt from waiting indefinitely. - /// This app context switch defaults to 'true'. - /// - public static bool UseMinimumLoginTimeout - { - get + // Check if invariant mode has been set by the AppContext switch directly + if (AppContext.TryGetSwitch(GlobalizationInvariantModeString, out bool returnedValue) && returnedValue) { - if (s_useMinimumLoginTimeout == Tristate.NotInitialized) - { - if (!AppContext.TryGetSwitch(UseMinimumLoginTimeoutString, out bool returnedValue) || returnedValue) - { - s_useMinimumLoginTimeout = Tristate.True; - } - else - { - s_useMinimumLoginTimeout = Tristate.False; - } - } - return s_useMinimumLoginTimeout == Tristate.True; + s_globalizationInvariantMode = SwitchValue.True; + return true; } - } + // TODO(https://github.com/dotnet/SqlClient/pull/3853): + // + // The intention of the comment below doesn't match the code. + // + // The comment claims to fallback to the environment variable if the + // switch is not set. However, it actually falls-back if the switch + // is not set _OR_ it is set to false. + // + // Should we update the comment or fix the code to match? - /// - /// When set to 'true' this will output a scale value of 7 (DEFAULT_VARTIME_SCALE) when the scale - /// is explicitly set to zero for VarTime data types ('datetime2', 'datetimeoffset' and 'time') - /// If no scale is set explicitly it will continue to output scale of 7 (DEFAULT_VARTIME_SCALE) - /// regardless of switch value. - /// This app context switch defaults to 'true'. - /// - public static bool LegacyVarTimeZeroScaleBehaviour - { - get + // 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)) { - if (s_legacyVarTimeZeroScaleBehaviour == Tristate.NotInitialized) - { - if (!AppContext.TryGetSwitch(LegacyVarTimeZeroScaleBehaviourString, out bool returnedValue)) - { - s_legacyVarTimeZeroScaleBehaviour = Tristate.True; - } - else - { - s_legacyVarTimeZeroScaleBehaviour = returnedValue ? Tristate.True : Tristate.False; - } - } - return s_legacyVarTimeZeroScaleBehaviour == Tristate.True; + s_globalizationInvariantMode = SwitchValue.True; + return true; } - } - /// - /// When set to true, the connection pool will use the new V2 connection pool implementation. - /// When set to false, the connection pool will use the legacy V1 implementation. - /// This app context switch defaults to 'false'. - /// - public static bool UseConnectionPoolV2 - { - get + // TODO(https://github.com/dotnet/SqlClient/pull/3853): + // + // What if the environment variable is set to false? Why are we + // ignoring that case? + + // 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 { - if (s_useConnectionPoolV2 == Tristate.NotInitialized) - { - if (AppContext.TryGetSwitch(UseConnectionPoolV2String, out bool returnedValue) && returnedValue) - { - s_useConnectionPoolV2 = Tristate.True; - } - else - { - s_useConnectionPoolV2 = Tristate.False; - } - } - return s_useConnectionPoolV2 == Tristate.True; + s_globalizationInvariantMode = System.Globalization.CultureInfo.GetCultureInfo("en-US").EnglishName.Contains("Invariant") + ? SwitchValue.True + : SwitchValue.False; } - } - - /// - /// When set to true, TdsParser will truncate (rather than round) decimal and SqlDecimal values when scaling them. - /// - public static bool TruncateScaledDecimal - { - get + catch (System.Globalization.CultureNotFoundException) { - 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 the culture is not found, it means we are in invariant mode + s_globalizationInvariantMode = SwitchValue.True; } + + return s_globalizationInvariantMode == SwitchValue.True; } + } + #else + /// + /// .NET Framework does not support Globalization Invariant mode, so this + /// will always be false. + /// + public static bool GlobalizationInvariantMode => false; + #endif + + /// + /// When set to true, the failover partner provided by the server during + /// connection will be ignored. This is useful in scenarios where the + /// application wants to control the failover behavior explicitly (e.g. + /// using a custom port). The application must be kept up to date with the + /// failover configuration of the server. The application will not + /// automatically discover a newly configured failover partner. + /// + /// The default value of this switch is false. + /// + public static bool IgnoreServerProvidedFailoverPartner => + AcquireAndReturn( + IgnoreServerProvidedFailoverPartnerString, + defaultValue: false, + ref s_ignoreServerProvidedFailoverPartner); + + /// + /// In System.Data.SqlClient and Microsoft.Data.SqlClient prior to 3.0.0 a + /// field with type Timestamp/RowVersion would return an empty byte array. + /// This switch controls whether to preserve that behaviour on newer + /// versions of Microsoft.Data.SqlClient, if this switch returns false an + /// appropriate null value will be returned. + /// + /// The default value of this switch is false. + /// + public static bool LegacyRowVersionNullBehavior => + AcquireAndReturn( + LegacyRowVersionNullString, + defaultValue: false, + ref s_legacyRowVersionNullBehavior); + + /// + /// When set to 'true' this will output a scale value of 7 + /// (DEFAULT_VARTIME_SCALE) when the scale is explicitly set to zero for + /// VarTime data types ('datetime2', 'datetimeoffset' and 'time') If no + /// scale is set explicitly it will continue to output scale of 7 + /// (DEFAULT_VARTIME_SCALE) regardless of switch value. + /// + /// The default value of this switch is true. + /// + public static bool LegacyVarTimeZeroScaleBehaviour => + AcquireAndReturn( + LegacyVarTimeZeroScaleBehaviourString, + defaultValue: true, + ref s_legacyVarTimeZeroScaleBehaviour); + + /// + /// When enabled, ReadAsync runs asynchronously and does not block the + /// calling thread. + /// + /// The default value of this switch is false. + /// + public static bool MakeReadAsyncBlocking => + AcquireAndReturn( + MakeReadAsyncBlockingString, + defaultValue: false, + ref s_makeReadAsyncBlocking); + + /// + /// When using Encrypt=false in the connection string, a security warning is + /// output to the console if the TLS version is 1.2 or lower. This warning + /// can be suppressed by enabling this AppContext switch. + /// + /// The default value of this switch is false. + /// + public static bool SuppressInsecureTlsWarning => + AcquireAndReturn( + SuppressInsecureTlsWarningString, + defaultValue: false, + ref s_suppressInsecureTlsWarning); - /// - /// When set to true, the failover partner provided by the server during connection - /// will be ignored. This is useful in scenarios where the application wants to - /// control the failover behavior explicitly (e.g. using a custom port). The application - /// must be kept up to date with the failover configuration of the server. - /// The application will not automatically discover a newly configured failover partner. - /// - /// This app context switch defaults to 'false'. - /// - public static bool IgnoreServerProvidedFailoverPartner + /// + /// When set to true, TdsParser will truncate (rather than round) decimal + /// and SqlDecimal values when scaling them. + /// + /// The default value of this switch is false. + /// + public static bool TruncateScaledDecimal => + AcquireAndReturn( + TruncateScaledDecimalString, + defaultValue: false, + ref s_truncateScaledDecimal); + + /// + /// In TdsParser, the async multi-packet column value fetch behavior can use + /// a continue snapshot state for improved efficiency. When this switch is + /// enabled (the default), the driver preserves the legacy compatibility + /// behavior, which does not use the continue snapshot state. When disabled, + /// the new behavior using the continue snapshot state is enabled. + /// + /// This switch will always return true if + /// is enabled, because the + /// continue state is not stable without the multiplexer. + /// + /// The default value of this switch is true. + /// + public static bool UseCompatibilityAsyncBehaviour + { + get { - get + if (UseCompatibilityProcessSni) { - if (s_ignoreServerProvidedFailoverPartner == Tristate.NotInitialized) - { - if (AppContext.TryGetSwitch(IgnoreServerProvidedFailoverPartnerString, out bool returnedValue) && returnedValue) - { - s_ignoreServerProvidedFailoverPartner = Tristate.True; - } - else - { - s_ignoreServerProvidedFailoverPartner = Tristate.False; - } - } - return s_ignoreServerProvidedFailoverPartner == Tristate.True; + // If ProcessSni compatibility mode has been enabled then the + // packet multiplexer has been disabled. The new async behaviour + // using continue point capture is only stable if the + // multiplexer is enabled so we must return true to enable + // compatibility async behaviour using only restarts. + return true; } + + return AcquireAndReturn( + UseCompatibilityAsyncBehaviourString, + defaultValue: true, + ref s_useCompatibilityAsyncBehaviour); } - /// - /// When set to true, the user agent feature is enabled and the driver will send the user agent string to the server. - /// - public static bool EnableUserAgent + } + + /// + /// In TdsParser, the ProcessSni function changed significantly when the + /// packet multiplexing code needed for high speed multi-packet column + /// values was added. When this switch is set to true (the default), the + /// old ProcessSni design is used. When this switch is set to false, the + /// new experimental ProcessSni behavior using the packet multiplexer is + /// enabled. + /// + /// The default value of this switch is true. + /// + public static bool UseCompatibilityProcessSni => + AcquireAndReturn( + UseCompatibilityProcessSniString, + defaultValue: true, + ref s_useCompatibilityProcessSni); + + /// + /// When set to true, the connection pool will use the new V2 connection + /// pool implementation. When set to false, the connection pool will use + /// the legacy V1 implementation. + /// + /// The default value of this switch is false. + /// + public static bool UseConnectionPoolV2 => + AcquireAndReturn( + UseConnectionPoolV2String, + defaultValue: false, + ref s_useConnectionPoolV2); + + #if NET && _WINDOWS + /// + /// When set to true, .NET on Windows will use the managed SNI + /// implementation instead of the native SNI implementation. + /// + /// 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. + /// + /// The default value of this switch is false. + /// + public static bool UseManagedNetworking + { + get { - get + if (s_useManagedNetworking != SwitchValue.None) { - if (s_enableUserAgent == Tristate.NotInitialized) - { - if (AppContext.TryGetSwitch(EnableUserAgentString, out bool returnedValue) && returnedValue) - { - s_enableUserAgent = Tristate.True; - } - else - { - s_enableUserAgent = Tristate.False; - } - } - return s_enableUserAgent == Tristate.True; + return s_useManagedNetworking == SwitchValue.True; } - } - #if NET - /// - /// .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 (!OperatingSystem.IsWindows()) { - if (s_globalizationInvariantMode == Tristate.NotInitialized) - { - // Check if invariant mode 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; + s_useManagedNetworking = SwitchValue.True; + return true; } - } - #else - /// - /// .NET Framework does not support Globalization Invariant mode, so this will always be false. - /// - public static bool GlobalizationInvariantMode - { - get => false; - } - #endif - - #if NET - - #if _WINDOWS - /// - /// 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. - /// - /// - /// 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 - { - get + + if (AppContext.TryGetSwitch(UseManagedNetworkingOnWindowsString, out bool returnedValue) && returnedValue) { - if (s_useManagedNetworking == Tristate.NotInitialized) - { - if (!OperatingSystem.IsWindows()) - { - s_useManagedNetworking = Tristate.True; - } - else if (AppContext.TryGetSwitch(UseManagedNetworkingOnWindowsString, out bool returnedValue) && returnedValue) - { - s_useManagedNetworking = Tristate.True; - } - else - { - s_useManagedNetworking = Tristate.False; - } - } - return s_useManagedNetworking == Tristate.True; + s_useManagedNetworking = SwitchValue.True; + return true; } + + s_useManagedNetworking = SwitchValue.False; + return false; } - #else - /// - /// .NET Core on Unix does not support the native SNI, so this will always be true. - /// - public static bool UseManagedNetworking => true; - #endif - - #else - /// - /// .NET Framework does not support the managed SNI, so this will always be false. - /// - public static bool UseManagedNetworking => false; - #endif - - #if NETFRAMEWORK - /// - /// 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 + } + #elif NET + /// + /// .NET Core on Unix does not support native SNI, so this will always be + /// true. + /// + public static bool UseManagedNetworking => true; + #else + /// + /// .NET Framework does not support the managed SNI, so this will always be + /// false. + /// + public static bool UseManagedNetworking => false; + #endif + + /// + /// Specifies minimum login timeout to be set to 1 second instead of 0 + /// seconds, to prevent a login attempt from waiting indefinitely. + /// + /// The default value of this switch is true. + /// + public static bool UseMinimumLoginTimeout => + AcquireAndReturn( + UseMinimumLoginTimeoutString, + defaultValue: true, + ref s_useMinimumLoginTimeout); + + #endregion + + #region Helpers + + /// + /// Acquires the value of the specified app context switch and stores it + /// in the given reference. Applies the default value if the switch isn't + /// set. + /// + /// If the cached value is already set, it is returned immediately without + /// attempting to re-acquire it. + /// + /// No attempt is made to prevent multiple threads from acquiring the same + /// switch value simultaneously. The worst that can happen is that the + /// switch is acquired more than once, and the last acquired value wins. + /// + /// The name of the app context switch. + /// The default value to use if the switch is not set. + /// A reference to variable to store the switch value in. + /// Returns the acquired value as a bool. + private static bool AcquireAndReturn( + string switchName, + bool defaultValue, + ref SwitchValue switchValue) + { + // Refuse to re-acquire a switch value. Simply return whatever value + // was previously acquired. + if (switchValue != SwitchValue.None) { - 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; - } + return switchValue == SwitchValue.True; } -#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 + + // Attempt to acquire the switch value from AppContext. + if (! AppContext.TryGetSwitch(switchName, out bool acquiredValue)) { - 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; - } + // The switch has no value, so use the given default. + switchValue = defaultValue ? SwitchValue.True : SwitchValue.False; + return defaultValue; } + + // Assign the appropriate SwitchValue based on the acquired value. + switchValue = acquiredValue ? SwitchValue.True : SwitchValue.False; + return acquiredValue; } + + #endregion } diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Common.csproj b/src/Microsoft.Data.SqlClient/tests/Common/Common.csproj index ffcd2869f9..ec728d6a98 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Common.csproj +++ b/src/Microsoft.Data.SqlClient/tests/Common/Common.csproj @@ -8,6 +8,7 @@ $(ObjFolder)$(Configuration).$(Platform).$(AssemblyName) $(BinFolder)$(Configuration).$(Platform).$(AssemblyName) true + enable diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CertificateFixtureBase.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CertificateFixtureBase.cs index f97aedfe4e..e3dd3902f0 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CertificateFixtureBase.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CertificateFixtureBase.cs @@ -67,7 +67,7 @@ protected X509Certificate2 CreateCertificate(string subjectName, IEnumerable csc.Location == storeLocation && csc.Name == storeName); + CertificateStoreContext? storeContext = _certificateStoreModifications.Find(csc => csc.Location == storeLocation && csc.Name == storeName); if (storeContext == null) { diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/ColumnEncryptionCertificateFixture.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/ColumnEncryptionCertificateFixture.cs index a4aa84842f..0c51a7e17b 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/ColumnEncryptionCertificateFixture.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/ColumnEncryptionCertificateFixture.cs @@ -25,7 +25,7 @@ public sealed class ColumnEncryptionCertificateFixture : CertificateFixtureBase public X509Certificate2 CertificateWithoutPrivateKey { get; } private readonly X509Certificate2 _currentUserCertificate; - private readonly X509Certificate2 _localMachineCertificate; + private readonly X509Certificate2? _localMachineCertificate; public ColumnEncryptionCertificateFixture() { @@ -57,11 +57,19 @@ public ColumnEncryptionCertificateFixture() public X509Certificate2 GetCertificate(StoreLocation storeLocation) { - return storeLocation == StoreLocation.CurrentUser - ? _currentUserCertificate - : storeLocation == StoreLocation.LocalMachine && IsAdmin - ? _localMachineCertificate - : throw new InvalidOperationException("Attempted to retrieve the certificate added to the local machine store; this requires administrator rights."); + if (storeLocation == StoreLocation.CurrentUser) + { + return _currentUserCertificate; + } + + if (storeLocation == StoreLocation.LocalMachine && + IsAdmin && + _localMachineCertificate is not null) + { + return _localMachineCertificate; + } + + throw new InvalidOperationException("Attempted to retrieve the certificate added to the local machine store; this requires administrator rights."); } public static bool IsAdmin diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/ColumnMasterKeyCertificateFixture.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/ColumnMasterKeyCertificateFixture.cs index a91ca7a0e0..7b431782af 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/ColumnMasterKeyCertificateFixture.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/ColumnMasterKeyCertificateFixture.cs @@ -21,7 +21,7 @@ public ColumnMasterKeyCertificateFixture() { } - public X509Certificate2 ColumnMasterKeyCertificate { get; } + public X509Certificate2? ColumnMasterKeyCertificate { get; } protected ColumnMasterKeyCertificateFixture(bool createCertificate) { diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CspCertificateFixture.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CspCertificateFixture.cs index 74c4ca0325..56463e91b8 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CspCertificateFixture.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CspCertificateFixture.cs @@ -32,17 +32,17 @@ public CspCertificateFixture() public string CspCertificatePath { get; } - public string CspKeyPath { get; } + public string? CspKeyPath { get; } - private string GetCspPathFromCertificate() + private string? GetCspPathFromCertificate() { - RSA privateKey = CspCertificate.GetRSAPrivateKey(); + RSA? privateKey = CspCertificate.GetRSAPrivateKey(); if (privateKey is RSACryptoServiceProvider csp) { return string.Concat(csp.CspKeyContainerInfo.ProviderName, @"/", csp.CspKeyContainerInfo.KeyContainerName); } - else if (privateKey is RSACng cng) + else if (privateKey is RSACng cng && cng.Key.Provider is not null) { return string.Concat(cng.Key.Provider.Provider, @"/", cng.Key.KeyName); } diff --git a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs index 7d90df85fe..e71d82b398 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs @@ -1,106 +1,61 @@ using System; -using System.Collections.Generic; using System.Reflection; +using System.Threading; namespace Microsoft.Data.SqlClient.Tests.Common; /// -/// This class provides read/write access to LocalAppContextSwitches values -/// for the duration of a test. It is intended to be constructed at the start -/// of a test and disposed of at the end. It captures the original values of -/// the switches and restores them when disposed. +/// This class provides read/write access to LocalAppContextSwitches values for +/// the duration of a test. It is intended to be constructed at the start of a +/// test and disposed of at the end. It captures the original values of the +/// switches and restores them when disposed. /// /// This follows the RAII pattern to ensure that the switches are always /// restored, which is important for global state like LocalAppContextSwitches. /// /// https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization -/// -/// This class is not thread-aware and should not be used concurrently. +/// +/// As with all global state, care must be taken when using this class in tests +/// that may run in parallel. This class enforces a single instance policy +/// using a semaphore. Overlapping constructor calls will wait up to 5 seconds +/// for the previous instance to be disposed. Any tests that use this class +/// should not keep an instance alive for longer than 5 seconds, or they risk +/// causing failures in other tests. /// public sealed class LocalAppContextSwitchesHelper : IDisposable { #region Private Fields - // These fields are used to expose LocalAppContextSwitches's properties. - private readonly PropertyInfo _legacyRowVersionNullBehaviorProperty; - private readonly PropertyInfo _suppressInsecureTlsWarningProperty; - private readonly PropertyInfo _makeReadAsyncBlockingProperty; - private readonly PropertyInfo _useMinimumLoginTimeoutProperty; - private readonly PropertyInfo _legacyVarTimeZeroScaleBehaviourProperty; - private readonly PropertyInfo _useCompatibilityProcessSniProperty; - private readonly PropertyInfo _useCompatibilityAsyncBehaviourProperty; - private readonly PropertyInfo _useConnectionPoolV2Property; - private readonly PropertyInfo _truncateScaledDecimalProperty; - private readonly PropertyInfo _ignoreServerProvidedFailoverPartner; - private readonly PropertyInfo _enableUserAgent; - private readonly PropertyInfo _enableMultiSubnetFailoverByDefaultProperty; -#if NET - private readonly PropertyInfo _globalizationInvariantModeProperty; - #endif - - #if NET && _WINDOWS - private readonly PropertyInfo _useManagedNetworkingProperty; - #endif - + /// + /// This semaphore ensures that only one instance of this class may exist at + /// a time. + /// + private static readonly SemaphoreSlim s_instanceLock = new(1, 1); + + /// + /// These fields are used to capture the original switch values. + /// #if NETFRAMEWORK - private readonly PropertyInfo _disableTnirByDefaultProperty; + private readonly bool? _disableTnirByDefaultOriginal; #endif - - // These fields are used to capture the original switch values. - private readonly FieldInfo _legacyRowVersionNullBehaviorField; - private readonly Tristate _legacyRowVersionNullBehaviorOriginal; - private readonly FieldInfo _suppressInsecureTlsWarningField; - private readonly Tristate _suppressInsecureTlsWarningOriginal; - private readonly FieldInfo _makeReadAsyncBlockingField; - private readonly Tristate _makeReadAsyncBlockingOriginal; - private readonly FieldInfo _useMinimumLoginTimeoutField; - private readonly Tristate _useMinimumLoginTimeoutOriginal; - private readonly FieldInfo _legacyVarTimeZeroScaleBehaviourField; - private readonly Tristate _legacyVarTimeZeroScaleBehaviourOriginal; - private readonly FieldInfo _useCompatibilityProcessSniField; - private readonly Tristate _useCompatibilityProcessSniOriginal; - private readonly FieldInfo _useCompatibilityAsyncBehaviourField; - private readonly Tristate _useCompatibilityAsyncBehaviourOriginal; - private readonly FieldInfo _useConnectionPoolV2Field; - private readonly Tristate _useConnectionPoolV2Original; - private readonly FieldInfo _truncateScaledDecimalField; - private readonly Tristate _truncateScaledDecimalOriginal; - private readonly FieldInfo _ignoreServerProvidedFailoverPartnerField; - private readonly Tristate _ignoreServerProvidedFailoverPartnerOriginal; - private readonly FieldInfo _enableUserAgentField; - private readonly Tristate _enableUserAgentOriginal; - private readonly FieldInfo _multiSubnetFailoverByDefaultField; - private readonly Tristate _multiSubnetFailoverByDefaultOriginal; -#if NET - private readonly FieldInfo _globalizationInvariantModeField; - private readonly Tristate _globalizationInvariantModeOriginal; + private readonly bool? _enableMultiSubnetFailoverByDefaultOriginal; + private readonly bool? _enableUserAgentOriginal; + #if NET + private readonly bool? _globalizationInvariantModeOriginal; #endif - + private readonly bool? _ignoreServerProvidedFailoverPartnerOriginal; + private readonly bool? _legacyRowVersionNullBehaviorOriginal; + private readonly bool? _legacyVarTimeZeroScaleBehaviourOriginal; + private readonly bool? _makeReadAsyncBlockingOriginal; + private readonly bool? _suppressInsecureTlsWarningOriginal; + private readonly bool? _truncateScaledDecimalOriginal; + private readonly bool? _useCompatibilityAsyncBehaviourOriginal; + private readonly bool? _useCompatibilityProcessSniOriginal; + private readonly bool? _useConnectionPoolV2Original; #if NET && _WINDOWS - private readonly FieldInfo _useManagedNetworkingField; - private readonly Tristate _useManagedNetworkingOriginal; + private readonly bool? _useManagedNetworkingOriginal; #endif - - #if NETFRAMEWORK - private readonly FieldInfo _disableTnirByDefaultField; - private readonly Tristate _disableTnirByDefaultOriginal; - #endif - - #endregion - - #region Public Types - - /// - /// This enum is used to represent the state of a switch. - /// - /// It is a copy of the Tristate enum from LocalAppContextSwitches. - /// - public enum Tristate : byte - { - NotInitialized = 0, - False = 1, - True = 2 - } + private readonly bool? _useMinimumLoginTimeoutOriginal; #endregion @@ -108,585 +63,379 @@ public enum Tristate : byte /// /// Construct to capture all existing switch values. + /// + /// This call will block for at most 5 seconds, waiting for any previous + /// instance to be disposed before completing construction. Failure to + /// acquire the lock in that time will result in an exception being thrown. /// - /// - /// - /// Throws if any values cannot be captured. - /// public LocalAppContextSwitchesHelper() { - // Acquire a handle to the LocalAppContextSwitches type. - var assembly = typeof(SqlCommandBuilder).Assembly; - var switchesType = assembly.GetType( - "Microsoft.Data.SqlClient.LocalAppContextSwitches"); - if (switchesType == null) + // Wait for any previous instance to be disposed. + // + // We are only willing to wait a short time to avoid deadlocks. + // + if (! s_instanceLock.Wait(TimeSpan.FromSeconds(5))) { - throw new Exception("Unable to find LocalAppContextSwitches type."); + throw new InvalidOperationException( + "Timeout waiting for previous LocalAppContextSwitchesHelper " + + "instance to be disposed."); } - // A local helper to acquire a handle to a property. - void InitProperty(string name, out PropertyInfo property) + try { - var prop = switchesType.GetProperty( - name, BindingFlags.Public | BindingFlags.Static); - if (prop == null) - { - throw new Exception($"Unable to find {name} property."); - } - property = prop; + #if NETFRAMEWORK + _disableTnirByDefaultOriginal = + GetSwitchValue("s_disableTnirByDefault"); + #endif + _enableMultiSubnetFailoverByDefaultOriginal = + GetSwitchValue("s_enableMultiSubnetFailoverByDefault"); + _enableUserAgentOriginal = + GetSwitchValue("s_enableUserAgent"); + #if NET + _globalizationInvariantModeOriginal = + GetSwitchValue("s_globalizationInvariantMode"); + #endif + _ignoreServerProvidedFailoverPartnerOriginal = + GetSwitchValue("s_ignoreServerProvidedFailoverPartner"); + _legacyRowVersionNullBehaviorOriginal = + GetSwitchValue("s_legacyRowVersionNullBehavior"); + _legacyVarTimeZeroScaleBehaviourOriginal = + GetSwitchValue("s_legacyVarTimeZeroScaleBehaviour"); + _makeReadAsyncBlockingOriginal = + GetSwitchValue("s_makeReadAsyncBlocking"); + _suppressInsecureTlsWarningOriginal = + GetSwitchValue("s_suppressInsecureTlsWarning"); + _truncateScaledDecimalOriginal = + GetSwitchValue("s_truncateScaledDecimal"); + _useCompatibilityAsyncBehaviourOriginal = + GetSwitchValue("s_useCompatibilityAsyncBehaviour"); + _useCompatibilityProcessSniOriginal = + GetSwitchValue("s_useCompatibilityProcessSni"); + _useConnectionPoolV2Original = + GetSwitchValue("s_useConnectionPoolV2"); + #if NET && _WINDOWS + _useManagedNetworkingOriginal = + GetSwitchValue("s_useManagedNetworking"); + #endif + _useMinimumLoginTimeoutOriginal = + GetSwitchValue("s_useMinimumLoginTimeout"); } - - // Acquire handles to all of the public properties of - // LocalAppContextSwitches. - InitProperty( - "LegacyRowVersionNullBehavior", - out _legacyRowVersionNullBehaviorProperty); - - InitProperty( - "SuppressInsecureTlsWarning", - out _suppressInsecureTlsWarningProperty); - - InitProperty( - "MakeReadAsyncBlocking", - out _makeReadAsyncBlockingProperty); - - InitProperty( - "UseMinimumLoginTimeout", - out _useMinimumLoginTimeoutProperty); - - InitProperty( - "LegacyVarTimeZeroScaleBehaviour", - out _legacyVarTimeZeroScaleBehaviourProperty); - - InitProperty( - "UseCompatibilityProcessSni", - out _useCompatibilityProcessSniProperty); - - InitProperty( - "UseCompatibilityAsyncBehaviour", - out _useCompatibilityAsyncBehaviourProperty); - - InitProperty( - "UseConnectionPoolV2", - out _useConnectionPoolV2Property); - - InitProperty( - "TruncateScaledDecimal", - out _truncateScaledDecimalProperty); - - InitProperty( - "IgnoreServerProvidedFailoverPartner", - out _ignoreServerProvidedFailoverPartner); - - InitProperty( - "EnableUserAgent", - out _enableUserAgent); - - InitProperty( - "EnableMultiSubnetFailoverByDefault", - out _enableMultiSubnetFailoverByDefaultProperty); - -#if NET - InitProperty( - "GlobalizationInvariantMode", - out _globalizationInvariantModeProperty); - #endif - - #if NET && _WINDOWS - InitProperty( - "UseManagedNetworking", - out _useManagedNetworkingProperty); - #endif - - #if NETFRAMEWORK - InitProperty( - "DisableTnirByDefault", - out _disableTnirByDefaultProperty); - #endif - - // A local helper to capture the original value of a switch. - void InitField(string name, out FieldInfo field, out Tristate value) + catch { - var fieldInfo = - switchesType.GetField( - name, BindingFlags.NonPublic | BindingFlags.Static); - if (fieldInfo == null) - { - throw new Exception($"Unable to find {name} field."); - } - field = fieldInfo; - value = GetValue(field); + // If we fail to capture the original values, release the lock + // immediately to avoid deadlocks. + s_instanceLock.Release(); + throw; } - - // Capture the original value of each switch. - InitField( - "s_legacyRowVersionNullBehavior", - out _legacyRowVersionNullBehaviorField, - out _legacyRowVersionNullBehaviorOriginal); - - InitField( - "s_suppressInsecureTlsWarning", - out _suppressInsecureTlsWarningField, - out _suppressInsecureTlsWarningOriginal); - - InitField( - "s_makeReadAsyncBlocking", - out _makeReadAsyncBlockingField, - out _makeReadAsyncBlockingOriginal); - - InitField( - "s_useMinimumLoginTimeout", - out _useMinimumLoginTimeoutField, - out _useMinimumLoginTimeoutOriginal); - - InitField( - "s_legacyVarTimeZeroScaleBehaviour", - out _legacyVarTimeZeroScaleBehaviourField, - out _legacyVarTimeZeroScaleBehaviourOriginal); - - InitField( - "s_useCompatibilityProcessSni", - out _useCompatibilityProcessSniField, - out _useCompatibilityProcessSniOriginal); - - InitField( - "s_useCompatibilityAsyncBehaviour", - out _useCompatibilityAsyncBehaviourField, - out _useCompatibilityAsyncBehaviourOriginal); - - InitField( - "s_useConnectionPoolV2", - out _useConnectionPoolV2Field, - out _useConnectionPoolV2Original); - - InitField( - "s_truncateScaledDecimal", - out _truncateScaledDecimalField, - out _truncateScaledDecimalOriginal); - - InitField( - "s_ignoreServerProvidedFailoverPartner", - out _ignoreServerProvidedFailoverPartnerField, - out _ignoreServerProvidedFailoverPartnerOriginal); - - InitField( - "s_enableUserAgent", - out _enableUserAgentField, - out _enableUserAgentOriginal); - - InitField( - "s_multiSubnetFailoverByDefault", - out _multiSubnetFailoverByDefaultField, - out _multiSubnetFailoverByDefaultOriginal); - -#if NET - InitField( - "s_globalizationInvariantMode", - out _globalizationInvariantModeField, - out _globalizationInvariantModeOriginal); - #endif - - #if NET && _WINDOWS - InitField( - "s_useManagedNetworking", - out _useManagedNetworkingField, - out _useManagedNetworkingOriginal); -#endif - - #if NETFRAMEWORK - InitField( - "s_disableTnirByDefault", - out _disableTnirByDefaultField, - out _disableTnirByDefaultOriginal); - #endif } /// - /// Disposal restores all original switch values as a best effort. + /// Disposal restores all original switch values and releases the instance + /// lock. /// - /// - /// - /// Throws if any values could not be restored after trying to restore all - /// values. - /// public void Dispose() { - List failedFields = new(); - - void RestoreField(FieldInfo field, Tristate value) + try { - try - { - SetValue(field, value); - } - catch (Exception) - { - failedFields.Add(field.Name); - } + #if NETFRAMEWORK + SetSwitchValue( + "s_disableTnirByDefault", + _disableTnirByDefaultOriginal); + #endif + SetSwitchValue( + "s_enableMultiSubnetFailoverByDefault", + _enableMultiSubnetFailoverByDefaultOriginal); + SetSwitchValue( + "s_enableUserAgent", + _enableUserAgentOriginal); + #if NET + SetSwitchValue( + "s_globalizationInvariantMode", + _globalizationInvariantModeOriginal); + #endif + SetSwitchValue( + "s_ignoreServerProvidedFailoverPartner", + _ignoreServerProvidedFailoverPartnerOriginal); + SetSwitchValue( + "s_legacyRowVersionNullBehavior", + _legacyRowVersionNullBehaviorOriginal); + SetSwitchValue( + "s_legacyVarTimeZeroScaleBehaviour", + _legacyVarTimeZeroScaleBehaviourOriginal); + SetSwitchValue( + "s_makeReadAsyncBlocking", + _makeReadAsyncBlockingOriginal); + SetSwitchValue( + "s_suppressInsecureTlsWarning", + _suppressInsecureTlsWarningOriginal); + SetSwitchValue( + "s_truncateScaledDecimal", + _truncateScaledDecimalOriginal); + SetSwitchValue( + "s_useCompatibilityAsyncBehaviour", + _useCompatibilityAsyncBehaviourOriginal); + SetSwitchValue( + "s_useCompatibilityProcessSni", + _useCompatibilityProcessSniOriginal); + SetSwitchValue( + "s_useConnectionPoolV2", + _useConnectionPoolV2Original); + #if NET && _WINDOWS + SetSwitchValue( + "s_useManagedNetworking", + _useManagedNetworkingOriginal); + #endif + SetSwitchValue( + "s_useMinimumLoginTimeout", + _useMinimumLoginTimeoutOriginal); } - - RestoreField( - _legacyRowVersionNullBehaviorField, - _legacyRowVersionNullBehaviorOriginal); - - RestoreField( - _suppressInsecureTlsWarningField, - _suppressInsecureTlsWarningOriginal); - - RestoreField( - _makeReadAsyncBlockingField, - _makeReadAsyncBlockingOriginal); - - RestoreField( - _useMinimumLoginTimeoutField, - _useMinimumLoginTimeoutOriginal); - - RestoreField( - _legacyVarTimeZeroScaleBehaviourField, - _legacyVarTimeZeroScaleBehaviourOriginal); - - RestoreField( - _useCompatibilityProcessSniField, - _useCompatibilityProcessSniOriginal); - - RestoreField( - _useCompatibilityAsyncBehaviourField, - _useCompatibilityAsyncBehaviourOriginal); - - RestoreField( - _useConnectionPoolV2Field, - _useConnectionPoolV2Original); - - RestoreField( - _truncateScaledDecimalField, - _truncateScaledDecimalOriginal); - - RestoreField( - _ignoreServerProvidedFailoverPartnerField, - _ignoreServerProvidedFailoverPartnerOriginal); - - RestoreField( - _enableUserAgentField, - _enableUserAgentOriginal); - - RestoreField( - _multiSubnetFailoverByDefaultField, - _multiSubnetFailoverByDefaultOriginal); - - #if NET - RestoreField( - _globalizationInvariantModeField, - _globalizationInvariantModeOriginal); - #endif - - #if NET && _WINDOWS - RestoreField( - _useManagedNetworkingField, - _useManagedNetworkingOriginal); - #endif - - #if NETFRAMEWORK - RestoreField( - _disableTnirByDefaultField, - _disableTnirByDefaultOriginal); - #endif - - if (failedFields.Count > 0) + finally { - throw new Exception( - "Failed to restore the following fields: " + - string.Join(", ", failedFields)); + // Release the lock to allow another instance to be created. + s_instanceLock.Release(); } } #endregion - #region Public Properties - - /// - /// Access the LocalAppContextSwitches.LegacyRowVersionNullBehavior - /// property. - /// - public bool LegacyRowVersionNullBehavior - { - get => (bool)_legacyRowVersionNullBehaviorProperty.GetValue(null); - } + #region Switch Value Getters and Setters - /// - /// Access the LocalAppContextSwitches.SuppressInsecureTlsWarning property. - /// - public bool SuppressInsecureTlsWarning - { - get => (bool)_suppressInsecureTlsWarningProperty.GetValue(null); - } + // These properties get or set the like-named underlying switch field value. + // + // They all throw if the value cannot be retrieved or set. + #if NETFRAMEWORK /// - /// Access the LocalAppContextSwitches.MakeReadAsyncBlocking property. + /// Get or set the DisableTnirByDefault switch value. /// - public bool MakeReadAsyncBlocking + public bool? DisableTnirByDefault { - get => (bool)_makeReadAsyncBlockingProperty.GetValue(null); + get => GetSwitchValue("s_disableTnirByDefault"); + set => SetSwitchValue("s_disableTnirByDefault", value); } + #endif /// - /// Access the LocalAppContextSwitches.UseMinimumLoginTimeout property. + /// Get or set the EnableMultiSubnetFailoverByDefault switch value. /// - public bool UseMinimumLoginTimeout + public bool? EnableMultiSubnetFailoverByDefault { - get => (bool)_useMinimumLoginTimeoutProperty.GetValue(null); + get => GetSwitchValue("s_enableMultiSubnetFailoverByDefault"); + set => SetSwitchValue("s_enableMultiSubnetFailoverByDefault", value); } /// - /// Access the LocalAppContextSwitches.LegacyVarTimeZeroScaleBehaviour - /// property. + /// Get or set the EnableUserAgent switch value. /// - public bool LegacyVarTimeZeroScaleBehaviour + public bool? EnableUserAgent { - get => (bool)_legacyVarTimeZeroScaleBehaviourProperty.GetValue(null); + get => GetSwitchValue("s_enableUserAgent"); + set => SetSwitchValue("s_enableUserAgent", value); } + #if NET /// - /// Access the LocalAppContextSwitches.UseCompatibilityProcessSni property. + /// Get or set the GlobalizationInvariantMode switch value. /// - public bool UseCompatibilityProcessSni + public bool? GlobalizationInvariantMode { - get => (bool)_useCompatibilityProcessSniProperty.GetValue(null); + get => GetSwitchValue("s_globalizationInvariantMode"); + set => SetSwitchValue("s_globalizationInvariantMode", value); } + #endif /// - /// Access the LocalAppContextSwitches.UseCompatibilityAsyncBehaviour - /// property. + /// Get or set the IgnoreServerProvidedFailoverPartner switch value. /// - public bool UseCompatibilityAsyncBehaviour + public bool? IgnoreServerProvidedFailoverPartner { - get => (bool)_useCompatibilityAsyncBehaviourProperty.GetValue(null); + get => GetSwitchValue("s_ignoreServerProvidedFailoverPartner"); + set => SetSwitchValue("s_ignoreServerProvidedFailoverPartner", value); } /// - /// Access the LocalAppContextSwitches.UseConnectionPoolV2 property. + /// Get or set the LegacyRowVersionNullBehavior switch value. /// - public bool UseConnectionPoolV2 + public bool? LegacyRowVersionNullBehavior { - get => (bool)_useConnectionPoolV2Property.GetValue(null); + get => GetSwitchValue("s_legacyRowVersionNullBehavior"); + set => SetSwitchValue("s_legacyRowVersionNullBehavior", value); } /// - /// Access the LocalAppContextSwitches.TruncateScaledDecimal property. + /// Get or set the LegacyVarTimeZeroScaleBehaviour switch value. /// - public bool TruncateScaledDecimal - { - get => (bool)_truncateScaledDecimalProperty.GetValue(null); - } - - public bool IgnoreServerProvidedFailoverPartner - { - get => (bool)_ignoreServerProvidedFailoverPartner.GetValue(null); - } - - public bool EnableUserAgent - { - get => (bool)_enableUserAgent.GetValue(null); - } - - public bool EnableMultiSubnetFailoverByDefault + public bool? LegacyVarTimeZeroScaleBehaviour { - get => (bool)_enableMultiSubnetFailoverByDefaultProperty.GetValue(null); + get => GetSwitchValue("s_legacyVarTimeZeroScaleBehaviour"); + set => SetSwitchValue("s_legacyVarTimeZeroScaleBehaviour", value); } - #if NET /// - /// Access the LocalAppContextSwitches.GlobalizationInvariantMode property. + /// Get or set the MakeReadAsyncBlocking switch value. /// - public bool GlobalizationInvariantMode + public bool? MakeReadAsyncBlocking { - get => (bool)_globalizationInvariantModeProperty.GetValue(null); + get => GetSwitchValue("s_makeReadAsyncBlocking"); + set => SetSwitchValue("s_makeReadAsyncBlocking", value); } - #endif - #if NET && _WINDOWS - /// - /// Access the LocalAppContextSwitches.UseManagedNetworking property. - /// - public bool UseManagedNetworking - { - get => (bool)_useManagedNetworkingProperty.GetValue(null); - } - #endif - - #if NETFRAMEWORK /// - /// Access the LocalAppContextSwitches.DisableTnirByDefault property. + /// Get or set the SuppressInsecureTlsWarning switch value. /// - public bool DisableTnirByDefault + public bool? SuppressInsecureTlsWarning { - get => (bool)_disableTnirByDefaultProperty.GetValue(null); + get => GetSwitchValue("s_suppressInsecureTlsWarning"); + set => SetSwitchValue("s_suppressInsecureTlsWarning", value); } - #endif - - // 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. /// - /// Get or set the LocalAppContextSwitches.LegacyRowVersionNullBehavior - /// switch value. + /// Get or set the TruncateScaledDecimal switch value. /// - public Tristate LegacyRowVersionNullBehaviorField + public bool? TruncateScaledDecimal { - get => GetValue(_legacyRowVersionNullBehaviorField); - set => SetValue(_legacyRowVersionNullBehaviorField, value); + get => GetSwitchValue("s_truncateScaledDecimal"); + set => SetSwitchValue("s_truncateScaledDecimal", value); } /// - /// Get or set the LocalAppContextSwitches.SuppressInsecureTlsWarning - /// switch value. + /// Get or set the UseCompatibilityAsyncBehaviour switch value. /// - public Tristate SuppressInsecureTlsWarningField + public bool? UseCompatibilityAsyncBehaviour { - get => GetValue(_suppressInsecureTlsWarningField); - set => SetValue(_suppressInsecureTlsWarningField, value); + get => GetSwitchValue("s_useCompatibilityAsyncBehaviour"); + set => SetSwitchValue("s_useCompatibilityAsyncBehaviour", value); } /// - /// Get or set the LocalAppContextSwitches.MakeReadAsyncBlocking switch - /// value. + /// Get or set the UseCompatibilityProcessSni switch value. /// - public Tristate MakeReadAsyncBlockingField + public bool? UseCompatibilityProcessSni { - get => GetValue(_makeReadAsyncBlockingField); - set => SetValue(_makeReadAsyncBlockingField, value); + get => GetSwitchValue("s_useCompatibilityProcessSni"); + set => SetSwitchValue("s_useCompatibilityProcessSni", value); } /// - /// Get or set the LocalAppContextSwitches.UseMinimumLoginTimeout switch - /// value. + /// Get or set the UseConnectionPoolV2 switch value. /// - public Tristate UseMinimumLoginTimeoutField + public bool? UseConnectionPoolV2 { - get => GetValue(_useMinimumLoginTimeoutField); - set => SetValue(_useMinimumLoginTimeoutField, value); + get => GetSwitchValue("s_useConnectionPoolV2"); + set => SetSwitchValue("s_useConnectionPoolV2", value); } + #if NET && _WINDOWS /// - /// Get or set the LocalAppContextSwitches.LegacyVarTimeZeroScaleBehaviour - /// switch value. + /// Get or set the UseManagedNetworking switch value. /// - public Tristate LegacyVarTimeZeroScaleBehaviourField + public bool? UseManagedNetworking { - get => GetValue(_legacyVarTimeZeroScaleBehaviourField); - set => SetValue(_legacyVarTimeZeroScaleBehaviourField, value); + get => GetSwitchValue("s_useManagedNetworking"); + set => SetSwitchValue("s_useManagedNetworking", value); } + #endif /// - /// Get or set the LocalAppContextSwitches.UseCompatibilityProcessSni switch - /// value. + /// Get or set the UseMinimumLoginTimeout switch value. /// - public Tristate UseCompatibilityProcessSniField + public bool? UseMinimumLoginTimeout { - get => GetValue(_useCompatibilityProcessSniField); - set => SetValue(_useCompatibilityProcessSniField, value); + get => GetSwitchValue("s_useMinimumLoginTimeout"); + set => SetSwitchValue("s_useMinimumLoginTimeout", value); } - /// - /// Get or set the LocalAppContextSwitches.UseCompatibilityAsyncBehaviour - /// switch value. - /// - public Tristate UseCompatibilityAsyncBehaviourField - { - get => GetValue(_useCompatibilityAsyncBehaviourField); - set => SetValue(_useCompatibilityAsyncBehaviourField, value); - } + #endregion - /// - /// Get or set the LocalAppContextSwitches.UseConnectionPoolV2 switch value. - /// - public Tristate UseConnectionPoolV2Field - { - get => GetValue(_useConnectionPoolV2Field); - set => SetValue(_useConnectionPoolV2Field, value); - } + #region Helpers /// - /// Get or set the LocalAppContextSwitches.TruncateScaledDecimal switch value. + /// Use reflection to get a switch field value from LocalAppContextSwitches. /// - public Tristate TruncateScaledDecimalField + private static bool? GetSwitchValue(string fieldName) { - get => GetValue(_truncateScaledDecimalField); - set => SetValue(_truncateScaledDecimalField, value); - } - - public Tristate IgnoreServerProvidedFailoverPartnerField - { - get => GetValue(_ignoreServerProvidedFailoverPartnerField); - set => SetValue(_ignoreServerProvidedFailoverPartnerField, value); - } + var assembly = Assembly.GetAssembly(typeof(SqlConnection)); + if (assembly is null) + { + throw new InvalidOperationException( + "Could not get assembly for Microsoft.Data.SqlClient"); + } + + var type = assembly.GetType("Microsoft.Data.SqlClient.LocalAppContextSwitches"); + if (type is null) + { + throw new InvalidOperationException( + "Could not get type LocalAppContextSwitches"); + } - public Tristate EnableUserAgentField - { - get => GetValue(_enableUserAgentField); - set => SetValue(_enableUserAgentField, value); - } + var field = type.GetField( + fieldName, + BindingFlags.Static | BindingFlags.NonPublic); + if (field == null) + { + throw new InvalidOperationException( + $"Field '{fieldName}' not found in LocalAppContextSwitches"); + } - public Tristate EnableMultiSubnetFailoverByDefaultField - { - get => GetValue(_multiSubnetFailoverByDefaultField); - set => SetValue(_multiSubnetFailoverByDefaultField, value); - } + var value = field.GetValue(null); + if (value is not null) + { + // GOTCHA: This assumes that switch values map to bytes as: + // + // None = 0 + // True = 1 + // False = 2 + // + // See the LocalAppContextSwitches.SwitchValue enum definition. + // + byte underlyingValue = (byte)value; + return underlyingValue == 0 ? null : underlyingValue == 1; + } -#if NET - /// - /// Get or set the LocalAppContextSwitches.GlobalizationInvariantMode switch value. - /// - public Tristate GlobalizationInvariantModeField - { - get => GetValue(_globalizationInvariantModeField); - set => SetValue(_globalizationInvariantModeField, value); + throw new InvalidOperationException( + $"Field '{fieldName}' is not of type byte"); } - #endif - #if NET && _WINDOWS - /// - /// Get or set the LocalAppContextSwitches.UseManagedNetworking switch value. - /// - public Tristate UseManagedNetworkingField - { - get => GetValue(_useManagedNetworkingField); - set => SetValue(_useManagedNetworkingField, value); - } - #endif - - #if NETFRAMEWORK /// - /// Get or set the LocalAppContextSwitches.DisableTnirByDefault switch - /// value. + /// Use reflection to set a switch field value in LocalAppContextSwitches. /// - public Tristate DisableTnirByDefaultField - { - get => GetValue(_disableTnirByDefaultField); - set => SetValue(_disableTnirByDefaultField, value); - } - #endif - - #endregion - - #region Private Helpers - - // Get the value of the given field, or throw if it is null. - private static Tristate GetValue(FieldInfo field) + private static void SetSwitchValue(string fieldName, bool? value) { - var value = field.GetValue(null); - if (value is null) + var assembly = Assembly.GetAssembly(typeof(SqlConnection)); + if (assembly is null) + { + throw new InvalidOperationException( + "Could not get assembly for Microsoft.Data.SqlClient"); + } + + var type = assembly.GetType("Microsoft.Data.SqlClient.LocalAppContextSwitches"); + if (type is null) { - throw new Exception($"Field {field.Name} has a null value."); + throw new InvalidOperationException( + "Could not get type LocalAppContextSwitches"); } - return (Tristate)value; - } + var field = type.GetField( + fieldName, + BindingFlags.Static | BindingFlags.NonPublic); + if (field == null) + { + throw new InvalidOperationException( + $"Field '{fieldName}' not found in LocalAppContextSwitches"); + } - // Set the value of the given field. - private static void SetValue(FieldInfo field, Tristate value) - { - field.SetValue(null, (byte)value); + // GOTCHA: This assumes that switch values map to bytes as: + // + // None = 0 + // True = 1 + // False = 2 + // + // See the LocalAppContextSwitches.SwitchValue enum definition. + // + byte byteValue = + (byte)(!value.HasValue ? 0 : value.Value ? 1 : 2); + + field.SetValue(null, Enum.ToObject(field.FieldType, byteValue)); } #endregion diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlParameterTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlParameterTest.cs index f62c5a70c8..8465d894a9 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlParameterTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlParameterTest.cs @@ -1952,10 +1952,8 @@ public void SqlDateTime2Scale_Legacy(int? setScale, byte outputScale, bool legac lock (_parameterLegacyScaleLock) { using SwitchesHelper switches = new SwitchesHelper(); - switches.LegacyVarTimeZeroScaleBehaviourField = - legacyVarTimeZeroScaleSwitchValue - ? SwitchesHelper.Tristate.True - : SwitchesHelper.Tristate.False; + switches.LegacyVarTimeZeroScaleBehaviour = + legacyVarTimeZeroScaleSwitchValue; var parameter = new SqlParameter { diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/TdsParserStateObject.TestHarness.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/TdsParserStateObject.TestHarness.cs index 6e8f64fcc8..4e70eaae57 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/TdsParserStateObject.TestHarness.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/TdsParserStateObject.TestHarness.cs @@ -5,10 +5,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Reflection; using Microsoft.Data.SqlClient.Tests; - -using SwitchesHelper = Microsoft.Data.SqlClient.Tests.Common.LocalAppContextSwitchesHelper; +using Microsoft.Data.SqlClient.Tests.Common; namespace Microsoft.Data.SqlClient { @@ -163,7 +161,25 @@ private void AssertValidState() { } [DebuggerStepThrough] private void AddError(object value) => throw new Exception(value as string ?? "AddError"); - private SwitchesHelper LocalAppContextSwitches = new(); + private class SwitchesHelper : IDisposable + { + private readonly LocalAppContextSwitchesHelper _helper = new(); + + public void Dispose() + { + _helper.Dispose(); + } + + public bool UseCompatibilityProcessSni + { + get + { + var value = _helper.UseCompatibilityProcessSni; + return value.HasValue && value.Value; + } + } + } + private readonly SwitchesHelper LocalAppContextSwitches = new SwitchesHelper(); #if NETFRAMEWORK private SniNativeWrapperImpl _native; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs index b071eeaf5a..040119616e 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs @@ -268,7 +268,7 @@ public static void CheckNullRowVersionIsDBNull() lock (s_rowVersionLock) { using SwitchesHelper helper = new(); - helper.LegacyRowVersionNullBehaviorField = SwitchesHelper.Tristate.False; + helper.LegacyRowVersionNullBehavior = false; using SqlConnection con = new(DataTestUtility.TCPConnectionString); con.Open(); @@ -869,7 +869,7 @@ public static void CheckLegacyNullRowVersionIsEmptyArray() lock (s_rowVersionLock) { using SwitchesHelper helper = new(); - helper.LegacyRowVersionNullBehaviorField = SwitchesHelper.Tristate.True; + helper.LegacyRowVersionNullBehavior = true; using SqlConnection con = new(DataTestUtility.TCPConnectionString); con.Open(); 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 7ae4a82fb9..4574f81a93 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs @@ -589,7 +589,7 @@ public static void TestScaledDecimalParameter_CommandInsert(string connectionStr { using (SqlCommand cmd = connection.CreateCommand()) { - appContextSwitchesHelper.TruncateScaledDecimalField = truncateScaledDecimal ? LocalAppContextSwitchesHelper.Tristate.True : LocalAppContextSwitchesHelper.Tristate.False; + appContextSwitchesHelper.TruncateScaledDecimal = truncateScaledDecimal; var p = new SqlParameter("@Value", null) { @@ -636,7 +636,7 @@ public static void TestScaledDecimalParameter_BulkCopy(string connectionString, } bulkCopy.DestinationTableName = tableName; - appContextSwitchesHelper.TruncateScaledDecimalField = truncateScaledDecimal ? LocalAppContextSwitchesHelper.Tristate.True : LocalAppContextSwitchesHelper.Tristate.False; + appContextSwitchesHelper.TruncateScaledDecimal = truncateScaledDecimal; bulkCopy.WriteToServer(table); } Assert.True(ValidateInsertedValues(connection, tableName, truncateScaledDecimal), $"Invalid test happened with connection string [{connection.ConnectionString}]"); @@ -681,7 +681,7 @@ public static void TestScaledDecimalTVP_CommandSP(string connectionString, bool table.Rows.Add(newRow); } p.Value = table; - appContextSwitchesHelper.TruncateScaledDecimalField = truncateScaledDecimal ? LocalAppContextSwitchesHelper.Tristate.True : LocalAppContextSwitchesHelper.Tristate.False; + appContextSwitchesHelper.TruncateScaledDecimal = truncateScaledDecimal; cmd.ExecuteNonQuery(); } // TVP always rounds data without attention to the configuration. 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 5a335e957e..49912202d6 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AdjustPrecScaleForBulkCopy.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/AdjustPrecScaleForBulkCopy.cs @@ -29,7 +29,9 @@ public static void RunTest() Assert.Equal("12.3", value.ToString()); value = BulkCopySqlDecimalToTable(new SqlDecimal(123.45), 10, 2, 4, 1); - if (appContextSwitches.TruncateScaledDecimal) + + bool? truncate = appContextSwitches.TruncateScaledDecimal; + if (truncate.HasValue && truncate.Value) { Assert.Equal("123.4", value.ToString()); } 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 c92402af41..59445a26b6 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,12 +27,17 @@ public void TestDefaultAppContextSwitchValues() Assert.True(LocalAppContextSwitches.UseCompatibilityAsyncBehaviour); Assert.False(LocalAppContextSwitches.UseConnectionPoolV2); Assert.False(LocalAppContextSwitches.TruncateScaledDecimal); + Assert.False(LocalAppContextSwitches.IgnoreServerProvidedFailoverPartner); + Assert.False(LocalAppContextSwitches.EnableUserAgent); Assert.False(LocalAppContextSwitches.EnableMultiSubnetFailoverByDefault); -#if NETFRAMEWORK - Assert.False(LocalAppContextSwitches.DisableTnirByDefault); + #if NET + Assert.False(LocalAppContextSwitches.GlobalizationInvariantMode); + #endif + #if NET && _WINDOWS Assert.False(LocalAppContextSwitches.UseManagedNetworking); -#else - Assert.Equal(!OperatingSystem.IsWindows(), LocalAppContextSwitches.UseManagedNetworking); -#endif + #endif + #if NETFRAMEWORK + Assert.False(LocalAppContextSwitches.DisableTnirByDefault); + #endif } } 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 e413821932..844506e92f 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 @@ -1,40 +1,41 @@ using System; using Microsoft.Data.SqlClient.Tests.Common; using Xunit; -using static Microsoft.Data.SqlClient.Tests.Common.LocalAppContextSwitchesHelper; namespace Microsoft.Data.SqlClient.UnitTests.Microsoft.Data.SqlClient { public class SqlConnectionStringTest : IDisposable { - private LocalAppContextSwitchesHelper _appContextSwitchHelper; - public SqlConnectionStringTest() + // Ensure we restore the original app context switch values after each + // test. + private readonly LocalAppContextSwitchesHelper _appContextSwitchHelper = new(); + + public void Dispose() { - // Ensure that the app context switch is set to the default value - _appContextSwitchHelper = new LocalAppContextSwitchesHelper(); + _appContextSwitchHelper.Dispose(); } #if NETFRAMEWORK [Theory] - [InlineData("test.database.windows.net", true, Tristate.True, true)] - [InlineData("test.database.windows.net", false, Tristate.True, false)] - [InlineData("test.database.windows.net", null, Tristate.True, false)] - [InlineData("test.database.windows.net", true, Tristate.False, true)] - [InlineData("test.database.windows.net", false, Tristate.False, false)] - [InlineData("test.database.windows.net", null, Tristate.False, true)] - [InlineData("test.database.windows.net", true, Tristate.NotInitialized, true)] - [InlineData("test.database.windows.net", false, Tristate.NotInitialized, false)] - [InlineData("test.database.windows.net", null, Tristate.NotInitialized, true)] - [InlineData("my.test.server", true, Tristate.True, true)] - [InlineData("my.test.server", false, Tristate.True, false)] - [InlineData("my.test.server", null, Tristate.True, false)] - [InlineData("my.test.server", true, Tristate.False, true)] - [InlineData("my.test.server", false, Tristate.False, false)] - [InlineData("my.test.server", null, Tristate.False, true)] - [InlineData("my.test.server", true, Tristate.NotInitialized, true)] - [InlineData("my.test.server", false, Tristate.NotInitialized, false)] - [InlineData("my.test.server", null, Tristate.NotInitialized, true)] - public void TestDefaultTnir(string dataSource, bool? tnirEnabledInConnString, Tristate tnirDisabledAppContext, bool expectedValue) + [InlineData("test.database.windows.net", true, true, true)] + [InlineData("test.database.windows.net", false, true, false)] + [InlineData("test.database.windows.net", null, true, false)] + [InlineData("test.database.windows.net", true, false, true)] + [InlineData("test.database.windows.net", false, false, false)] + [InlineData("test.database.windows.net", null, false, true)] + [InlineData("test.database.windows.net", true, null, true)] + [InlineData("test.database.windows.net", false, null, false)] + [InlineData("test.database.windows.net", null, null, true)] + [InlineData("my.test.server", true, true, true)] + [InlineData("my.test.server", false, true, false)] + [InlineData("my.test.server", null, true, false)] + [InlineData("my.test.server", true, false, true)] + [InlineData("my.test.server", false, false, false)] + [InlineData("my.test.server", null, false, true)] + [InlineData("my.test.server", true, null, true)] + [InlineData("my.test.server", false, null, false)] + [InlineData("my.test.server", null, null, true)] + public void TestDefaultTnir(string dataSource, bool? tnirEnabledInConnString, bool? tnirDisabledAppContext, bool expectedValue) { // Note: TNIR is only supported on .NET Framework. // Note: TNIR is disabled by default for Azure SQL Database servers (i.e. *.database.windows.net) @@ -43,7 +44,7 @@ public void TestDefaultTnir(string dataSource, bool? tnirEnabledInConnString, Tr // the value of TransparentNetworkIPResolution property in SqlConnectionString. // Arrange - _appContextSwitchHelper.DisableTnirByDefaultField = tnirDisabledAppContext; + _appContextSwitchHelper.DisableTnirByDefault = tnirDisabledAppContext; // Act SqlConnectionStringBuilder builder = new(); @@ -62,16 +63,16 @@ public void TestDefaultTnir(string dataSource, bool? tnirEnabledInConnString, Tr /// 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) + [InlineData(true, true, true)] + [InlineData(false, true, false)] + [InlineData(null, true, true)] + [InlineData(true, false, true)] + [InlineData(false, false, false)] + [InlineData(null, false, false)] + [InlineData(null, null, false)] + public void TestDefaultMultiSubnetFailover(bool? msfInConnString, bool? msfEnabledAppContext, bool expectedValue) { - _appContextSwitchHelper.EnableMultiSubnetFailoverByDefaultField = msfEnabledAppContext; + _appContextSwitchHelper.EnableMultiSubnetFailoverByDefault = msfEnabledAppContext; SqlConnectionStringBuilder builder = new(); if (msfInConnString.HasValue) @@ -89,7 +90,7 @@ public void TestDefaultMultiSubnetFailover(bool? msfInConnString, Tristate msfEn [Fact] public void TestMultiSubnetFailoverWithFailoverPartnerThrows() { - _appContextSwitchHelper.EnableMultiSubnetFailoverByDefaultField = Tristate.True; + _appContextSwitchHelper.EnableMultiSubnetFailoverByDefault = true; SqlConnectionStringBuilder builder = new() { @@ -100,11 +101,5 @@ public void TestMultiSubnetFailoverWithFailoverPartnerThrows() Assert.Throws(() => new SqlConnectionString(builder.ConnectionString)); } - - public void Dispose() - { - // Clean up any resources if necessary - _appContextSwitchHelper.Dispose(); - } } } diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs index dfc37d2720..5b294ff20e 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionFailoverTests.cs @@ -529,7 +529,7 @@ public void TransientFault_IgnoreServerProvidedFailoverPartner_ShouldConnectToUs { // Arrange using LocalAppContextSwitchesHelper switchesHelper = new(); - switchesHelper.IgnoreServerProvidedFailoverPartnerField = LocalAppContextSwitchesHelper.Tristate.True; + switchesHelper.IgnoreServerProvidedFailoverPartner = true; using TdsServer failoverServer = new( new TdsServerArguments diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs index 9dad33aa1d..a6d2cac9fd 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs @@ -839,7 +839,7 @@ public void TestConnWithUserAgentFeatureExtension(bool forceAck) { // Make sure needed switch is enabled using LocalAppContextSwitchesHelper switchesHelper = new(); - switchesHelper.EnableUserAgentField = LocalAppContextSwitchesHelper.Tristate.True; + switchesHelper.EnableUserAgent = true; using var server = new TdsServer(); server.Start(); @@ -935,7 +935,7 @@ public void TestConnWithoutUserAgentFeatureExtension() { // Disable the client-side UserAgent field entirely using LocalAppContextSwitchesHelper switchesHelper = new(); - switchesHelper.EnableUserAgentField = LocalAppContextSwitchesHelper.Tristate.False; + switchesHelper.EnableUserAgent = false; using var server = new TdsServer(); server.Start();