diff --git a/.github/instructions/features.instructions.md b/.github/instructions/features.instructions.md index 34262b8db6..0fda359a15 100644 --- a/.github/instructions/features.instructions.md +++ b/.github/instructions/features.instructions.md @@ -243,6 +243,7 @@ AppContext switches allow runtime behavior changes without modifying connection | Switch Name | Default | Description | |-------------|---------|-------------| | `Switch.Microsoft.Data.SqlClient.DisableTNIRByDefaultInConnectionString` | `false` | Disables Transparent Network IP Resolution by default | +| `Switch.Microsoft.Data.SqlClient.EnableAppConfig` | `true` | Controls whether SqlClient reads app.config: configurable retry logic, authentication providers, switch overrides and, on .NET Framework, `system.data.localdb`. See [Trimming](#trimming) | | `Switch.Microsoft.Data.SqlClient.EnableMultiSubnetFailoverByDefault` | `false` | Sets `MultiSubnetFailover=true` as the default for all connections | | `Switch.Microsoft.Data.SqlClient.EnableUserAgent` | varies | Controls sending user agent information to SQL Server | | `Switch.Microsoft.Data.SqlClient.IgnoreServerProvidedFailoverPartner` | `false` | Ignores failover partner information sent by the server | @@ -273,6 +274,18 @@ AppContext.SetSwitch("Switch.Microsoft.Data.SqlClient.EnableMultiSubnetFailoverB // } ``` +### Trimming + +`EnableAppConfig` removes the configuration reading from a trimmed or Native AOT application only when set at publish time: + +```xml + + + +``` + +Setting it only at run time, with `AppContext.SetSwitch` or `runtimeconfig.json`, stops the reading but leaves the trim warnings. + ### Guidelines for Adding New Switches 1. Define the switch name constant in `LocalAppContextSwitches.cs` 2. Add a cached property with lazy evaluation pattern (see existing switches) 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 06bf6c4f0e..9fd97a33f3 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs @@ -29,6 +29,13 @@ internal static class LocalAppContextSwitches "Switch.Microsoft.Data.SqlClient.DisableTNIRByDefaultInConnectionString"; #endif + /// + /// The name of the app context switch that controls whether SqlClient + /// reads app.config. + /// + private const string EnableAppConfigString = + "Switch.Microsoft.Data.SqlClient.EnableAppConfig"; + /// /// The name of the app context switch that controls whether /// MultiSubnetFailover is enabled by default in the connection string. @@ -186,6 +193,11 @@ private enum SwitchValue : byte private static SwitchValue s_disableTnirByDefault = SwitchValue.None; #endif + /// + /// The cached value of the EnableAppConfig switch. + /// + private static SwitchValue s_enableAppConfig = SwitchValue.None; + /// /// The cached value of the EnableMultiSubnetFailoverByDefault switch. /// @@ -283,6 +295,13 @@ private enum SwitchValue : byte /// static LocalAppContextSwitches() { + // Read before any override is applied, so this switch itself cannot be + // set from the config file it gates. + if (!EnableAppConfig) + { + return; + } + IAppContextSwitchOverridesSection appContextSwitch = AppConfigManager.FetchConfigurationSection(AppContextSwitchOverridesSection.Name); try @@ -329,6 +348,25 @@ static LocalAppContextSwitches() ref s_disableTnirByDefault); #endif + /// + /// When set to false, SqlClient does not read app.config. Configurable + /// retry logic, authentication providers and switch overrides are then not + /// taken from the configuration file. + /// + /// ILLink.Substitutions.xml allows the configuration reading, and the type + /// resolution it drives, 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. + /// + /// The default value of this switch is true. + /// + public static bool EnableAppConfig => + AcquireAndReturn( + EnableAppConfigString, + defaultValue: true, + ref s_enableAppConfig); + /// /// When set to true, the default value for MultiSubnetFailover connection /// string property will be true instead of false. This enables parallel IP diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalDb/LocalDbApi.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalDb/LocalDbApi.cs index da1e613c44..529952e8aa 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalDb/LocalDbApi.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalDb/LocalDbApi.cs @@ -182,7 +182,9 @@ internal static void CreateLocalDbInstance(string instance) { Dictionary tempConfigurableInstances = new Dictionary(StringComparer.OrdinalIgnoreCase); - object section = ConfigurationManager.GetSection("system.data.localdb"); + object section = LocalAppContextSwitches.EnableAppConfig + ? ConfigurationManager.GetSection("system.data.localdb") + : null; if (section is not null) { // Validate section type diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs index 082a5599ce..5a37060285 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs @@ -40,12 +40,15 @@ static SqlAuthenticationProviderManager() try { - // New configuration section "SqlClientAuthenticationProviders" for Microsoft.Data.SqlClient accepted to avoid conflicts with older one. - configurationSection = FetchConfigurationSection(SqlClientAuthenticationProviderConfigurationSection.Name); - if (configurationSection == null) + if (LocalAppContextSwitches.EnableAppConfig) { - // If configuration section is not yet found, try with old Configuration Section name for backwards compatibility - configurationSection = FetchConfigurationSection(SqlAuthenticationProviderConfigurationSection.Name); + // New configuration section "SqlClientAuthenticationProviders" for Microsoft.Data.SqlClient accepted to avoid conflicts with older one. + configurationSection = FetchConfigurationSection(SqlClientAuthenticationProviderConfigurationSection.Name); + if (configurationSection == null) + { + // If configuration section is not yet found, try with old Configuration Section name for backwards compatibility + configurationSection = FetchConfigurationSection(SqlAuthenticationProviderConfigurationSection.Name); + } } } catch (ConfigurationErrorsException e) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs index c90ac4520d..f12ac8b49d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -166,6 +166,9 @@ public sealed partial class SqlCommand : DbCommand, ICloneable /// private static readonly SqlDiagnosticListener s_diagnosticListener = new(); + // Shared by all commands when app.config is not read. + private static SqlRetryLogicBaseProvider s_noneRetryProvider; + /// /// Connection that will be used to process the current instance. /// @@ -763,7 +766,9 @@ public SqlRetryLogicBaseProvider RetryLogicProvider { get { - _retryLogicProvider ??= SqlConfigurableRetryLogicManager.CommandProvider; + _retryLogicProvider ??= LocalAppContextSwitches.EnableAppConfig + ? SqlConfigurableRetryLogicManager.CommandProvider + : LazyInitializer.EnsureInitialized(ref s_noneRetryProvider, SqlConfigurableRetryFactory.CreateNoneRetryProvider); return _retryLogicProvider; } set => _retryLogicProvider = value; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs index 66b8df4564..f433e0c78f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -142,6 +142,9 @@ private static readonly ConcurrentDictionary> _ColumnEncry private static readonly Action s_openAsyncCancel = OpenAsyncCancel; private static readonly Action, object> s_openAsyncComplete = OpenAsyncComplete; + // Shared by all connections when app.config is not read. + private static SqlRetryLogicBaseProvider s_noneRetryProvider; + private bool IsProviderRetriable => SqlConfigurableRetryFactory.IsRetriable(RetryLogicProvider); /// @@ -153,7 +156,9 @@ public SqlRetryLogicBaseProvider RetryLogicProvider { if (_retryLogicProvider == null) { - _retryLogicProvider = SqlConfigurableRetryLogicManager.ConnectionProvider; + _retryLogicProvider = LocalAppContextSwitches.EnableAppConfig + ? SqlConfigurableRetryLogicManager.ConnectionProvider + : LazyInitializer.EnsureInitialized(ref s_noneRetryProvider, SqlConfigurableRetryFactory.CreateNoneRetryProvider); } return _retryLogicProvider; } diff --git a/src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.xml b/src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.xml index e8577c7ae0..9f30adbbb4 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.xml +++ b/src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.xml @@ -4,6 +4,9 @@ + + + diff --git a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs index 49ad2712ec..d3a6030b4e 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs @@ -43,6 +43,7 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable #if NETFRAMEWORK private readonly bool? _disableTnirByDefaultOriginal; #endif + private readonly bool? _enableAppConfigOriginal; private readonly bool? _enableMultiSubnetFailoverByDefaultOriginal; #if NET private readonly bool? _globalizationInvariantModeOriginal; @@ -97,6 +98,8 @@ public LocalAppContextSwitchesHelper() _disableTnirByDefaultOriginal = GetSwitchValue("s_disableTnirByDefault"); #endif + _enableAppConfigOriginal = + GetSwitchValue("s_enableAppConfig"); _enableMultiSubnetFailoverByDefaultOriginal = GetSwitchValue("s_enableMultiSubnetFailoverByDefault"); #if NET @@ -159,6 +162,9 @@ public void Dispose() "s_disableTnirByDefault", _disableTnirByDefaultOriginal); #endif + SetSwitchValue( + "s_enableAppConfig", + _enableAppConfigOriginal); SetSwitchValue( "s_enableMultiSubnetFailoverByDefault", _enableMultiSubnetFailoverByDefaultOriginal); @@ -242,6 +248,15 @@ public bool? DisableTnirByDefault } #endif + /// + /// Get or set the EnableAppConfig switch value. + /// + public bool? EnableAppConfig + { + get => GetSwitchPropertyValue(nameof(EnableAppConfig)); + set => SetSwitchValue("s_enableAppConfig", value); + } + /// /// Get or set the EnableMultiSubnetFailoverByDefault switch value. /// diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/EnableAppConfigSwitchTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/EnableAppConfigSwitchTest.cs new file mode 100644 index 0000000000..c58434172f --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/EnableAppConfigSwitchTest.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Data.SqlClient.Tests.Common; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests; + +/// +/// Tests that the EnableAppConfig switch gates the configurable retry logic +/// providers used by commands and connections. +/// +/// +/// The authentication provider and switch override readers run once, in +/// static constructors, so they cannot be exercised in-process. +/// +[Collection(AppContextSwitchTestCollection.Name)] +public class EnableAppConfigSwitchTest +{ + /// + /// With the switch off, commands share one non-retriable provider that + /// does not come from the configurable retry logic manager. + /// + [Fact] + public void Disabled_CommandsShareNonRetriableProvider() + { + using LocalAppContextSwitchesHelper switchesHelper = new(); + switchesHelper.EnableAppConfig = false; + + using SqlCommand first = new(); + using SqlCommand second = new(); + + Assert.Same(first.RetryLogicProvider, second.RetryLogicProvider); + Assert.False(SqlConfigurableRetryFactory.IsRetriable(first.RetryLogicProvider)); + Assert.NotSame(SqlConfigurableRetryLogicManager.CommandProvider, first.RetryLogicProvider); + } + + /// + /// With the switch off, connections share one non-retriable provider that + /// does not come from the configurable retry logic manager. + /// + [Fact] + public void Disabled_ConnectionsShareNonRetriableProvider() + { + using LocalAppContextSwitchesHelper switchesHelper = new(); + switchesHelper.EnableAppConfig = false; + + using SqlConnection first = new(); + using SqlConnection second = new(); + + Assert.Same(first.RetryLogicProvider, second.RetryLogicProvider); + Assert.False(SqlConfigurableRetryFactory.IsRetriable(first.RetryLogicProvider)); + Assert.NotSame(SqlConfigurableRetryLogicManager.ConnectionProvider, first.RetryLogicProvider); + } + + /// + /// With the switch on, commands and connections use the manager's + /// providers, as they did before the switch existed. + /// + [Fact] + public void Enabled_UsesManagerProviders() + { + using LocalAppContextSwitchesHelper switchesHelper = new(); + switchesHelper.EnableAppConfig = true; + + using SqlCommand command = new(); + using SqlConnection connection = new(); + + Assert.Same(SqlConfigurableRetryLogicManager.CommandProvider, command.RetryLogicProvider); + Assert.Same(SqlConfigurableRetryLogicManager.ConnectionProvider, connection.RetryLogicProvider); + } +} 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 ff70c17f4b..801f965034 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 @@ -31,6 +31,7 @@ public void TestDefaultAppContextSwitchValues() // cached field to None so the properties re-read from AppContext. using LocalAppContextSwitchesHelper switchesHelper = new(); + switchesHelper.EnableAppConfig = null; switchesHelper.EnableMultiSubnetFailoverByDefault = null; switchesHelper.IgnoreServerProvidedFailoverPartner = null; switchesHelper.UseLegacyFailoverAlternationOnLoginSqlErrors = null; @@ -69,6 +70,7 @@ public void TestDefaultAppContextSwitchValues() Assert.False(switchesHelper.IgnoreServerProvidedFailoverPartner); Assert.False(switchesHelper.UseLegacyFailoverAlternationOnLoginSqlErrors); Assert.False(switchesHelper.EnableMultiSubnetFailoverByDefault); + Assert.True(switchesHelper.EnableAppConfig); #if NET Assert.False(switchesHelper.GlobalizationInvariantMode); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))