From cdd04507ce1850bdbed9a6f549ebeb7267c44bdf Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Wed, 16 Sep 2026 10:03:32 +0100 Subject: [PATCH 1/4] Allow app.config reading to be trimmed away On .NET, SqlClient reads app.config from three places: the configurable retry logic manager, LocalAppContextSwitches' own static constructor, and SqlAuthenticationProviderManager. ConfigurationManager.GetSection resolves section handler types named as strings in the config file, so reaching it from anywhere produces five trim warnings inside System.Configuration's TypeUtil that no annotation in SqlClient can remove. dotnet/runtime#49062 is closed with no fix planned. Gate all three readers behind a new EnableAppConfig switch, defaulting to true, and stub it through ILLink.Substitutions.xml as UseManagedNetworking already is. Publishing with the switch set to false removes those five warnings and the five in SqlConfigurableRetryLogicLoader, measured on a Native AOT application. The retry guards sit at the SqlCommand and SqlConnection call sites rather than inside SqlConfigurableRetryLogicManager, whose static field initializer would otherwise still run and keep the configuration reading reachable. Co-Authored-By: Claude Opus 5 (1M context) --- .github/instructions/features.instructions.md | 1 + .../Data/SqlClient/LocalAppContextSwitches.cs | 38 +++++++++++++++++++ .../SqlAuthenticationProviderManager.cs | 13 ++++--- .../Microsoft/Data/SqlClient/SqlCommand.cs | 4 +- .../Microsoft/Data/SqlClient/SqlConnection.cs | 4 +- .../src/Resources/ILLink.Substitutions.xml | 3 ++ .../Common/LocalAppContextSwitchesHelper.cs | 15 ++++++++ .../SqlClient/LocalAppContextSwitchesTest.cs | 2 + 8 files changed, 73 insertions(+), 7 deletions(-) diff --git a/.github/instructions/features.instructions.md b/.github/instructions/features.instructions.md index 34262b8db6..e0faa69354 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, for configurable retry logic, authentication providers and switch overrides. Set to `false` to trim the configuration reading out of a trimmed or Native AOT application | | `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 | 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/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..5cae9b6cf6 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -763,7 +763,9 @@ public SqlRetryLogicBaseProvider RetryLogicProvider { get { - _retryLogicProvider ??= SqlConfigurableRetryLogicManager.CommandProvider; + _retryLogicProvider ??= LocalAppContextSwitches.EnableAppConfig + ? SqlConfigurableRetryLogicManager.CommandProvider + : 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..14f6d7aca3 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -153,7 +153,9 @@ public SqlRetryLogicBaseProvider RetryLogicProvider { if (_retryLogicProvider == null) { - _retryLogicProvider = SqlConfigurableRetryLogicManager.ConnectionProvider; + _retryLogicProvider = LocalAppContextSwitches.EnableAppConfig + ? SqlConfigurableRetryLogicManager.ConnectionProvider + : 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/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)) From fecd9532505409716e2e92875ae3d1bc3ff6a25b Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Wed, 16 Sep 2026 10:29:36 +0100 Subject: [PATCH 2/4] Share the no-retry providers when app.config is not read With EnableAppConfig off, each command and connection created its own no-retry provider, where the manager shares one of each process-wide. Keep a lazily created shared provider for each instead, without touching SqlConfigurableRetryLogicManager. Add tests that the switch selects between the shared providers and the manager's. The authentication provider and switch override readers run in static constructors, so they cannot be tested in-process. Co-Authored-By: Claude Opus 5 (1M context) --- .../Microsoft/Data/SqlClient/SqlCommand.cs | 5 +- .../Microsoft/Data/SqlClient/SqlConnection.cs | 5 +- .../SqlClient/EnableAppConfigSwitchTest.cs | 73 +++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/EnableAppConfigSwitchTest.cs 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 5cae9b6cf6..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. /// @@ -765,7 +768,7 @@ public SqlRetryLogicBaseProvider RetryLogicProvider { _retryLogicProvider ??= LocalAppContextSwitches.EnableAppConfig ? SqlConfigurableRetryLogicManager.CommandProvider - : SqlConfigurableRetryFactory.CreateNoneRetryProvider(); + : 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 14f6d7aca3..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); /// @@ -155,7 +158,7 @@ public SqlRetryLogicBaseProvider RetryLogicProvider { _retryLogicProvider = LocalAppContextSwitches.EnableAppConfig ? SqlConfigurableRetryLogicManager.ConnectionProvider - : SqlConfigurableRetryFactory.CreateNoneRetryProvider(); + : LazyInitializer.EnsureInitialized(ref s_noneRetryProvider, SqlConfigurableRetryFactory.CreateNoneRetryProvider); } return _retryLogicProvider; } 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); + } +} From 6d0013c5f15a00182fd22220b3bd9257191835e3 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Wed, 16 Sep 2026 12:16:21 +0100 Subject: [PATCH 3/4] Gate the LocalDB config section on EnableAppConfig On .NET Framework, LocalDbApi still read system.data.localdb from app.config with the switch off, so the switch did not mean the same thing on every platform. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/Microsoft/Data/SqlClient/LocalDb/LocalDbApi.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 From 6f5acf9cdbad267f7906eba277412f056e1fe516 Mon Sep 17 00:00:00 2001 From: Charles Roddie Date: Wed, 16 Sep 2026 12:49:10 +0100 Subject: [PATCH 4/4] Document that EnableAppConfig trims only when set at publish time Also list the .NET Framework system.data.localdb section among what the switch gates. Co-Authored-By: Claude Opus 5 (1M context) --- .github/instructions/features.instructions.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/instructions/features.instructions.md b/.github/instructions/features.instructions.md index e0faa69354..0fda359a15 100644 --- a/.github/instructions/features.instructions.md +++ b/.github/instructions/features.instructions.md @@ -243,7 +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, for configurable retry logic, authentication providers and switch overrides. Set to `false` to trim the configuration reading out of a trimmed or Native AOT application | +| `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 | @@ -274,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)