Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/instructions/features.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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
<ItemGroup>
<RuntimeHostConfigurationOption Include="Switch.Microsoft.Data.SqlClient.EnableAppConfig" Value="false" Trim="true" />
</ItemGroup>
```

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ internal static class LocalAppContextSwitches
"Switch.Microsoft.Data.SqlClient.DisableTNIRByDefaultInConnectionString";
#endif

/// <summary>
/// The name of the app context switch that controls whether SqlClient
/// reads app.config.
/// </summary>
private const string EnableAppConfigString =
"Switch.Microsoft.Data.SqlClient.EnableAppConfig";

/// <summary>
/// The name of the app context switch that controls whether
/// MultiSubnetFailover is enabled by default in the connection string.
Expand Down Expand Up @@ -186,6 +193,11 @@ private enum SwitchValue : byte
private static SwitchValue s_disableTnirByDefault = SwitchValue.None;
#endif

/// <summary>
/// The cached value of the EnableAppConfig switch.
/// </summary>
private static SwitchValue s_enableAppConfig = SwitchValue.None;

/// <summary>
/// The cached value of the EnableMultiSubnetFailoverByDefault switch.
/// </summary>
Expand Down Expand Up @@ -283,6 +295,13 @@ private enum SwitchValue : byte
/// </summary>
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>(AppContextSwitchOverridesSection.Name);

try
Expand Down Expand Up @@ -329,6 +348,25 @@ static LocalAppContextSwitches()
ref s_disableTnirByDefault);
#endif

/// <summary>
/// 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.
/// </summary>
public static bool EnableAppConfig =>
AcquireAndReturn(
EnableAppConfigString,
defaultValue: true,
ref s_enableAppConfig);

/// <summary>
/// When set to true, the default value for MultiSubnetFailover connection
/// string property will be true instead of false. This enables parallel IP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,9 @@ internal static void CreateLocalDbInstance(string instance)
{
Dictionary<string, InstanceInfo> tempConfigurableInstances =
new Dictionary<string, InstanceInfo>(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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>(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>(SqlAuthenticationProviderConfigurationSection.Name);
// New configuration section "SqlClientAuthenticationProviders" for Microsoft.Data.SqlClient accepted to avoid conflicts with older one.
configurationSection = FetchConfigurationSection<SqlClientAuthenticationProviderConfigurationSection>(SqlClientAuthenticationProviderConfigurationSection.Name);
Comment on lines -47 to +46

@edwardneal edwardneal Sep 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will remove the trim warning - does it allow ILLink to trim away every type within the System.Configuration assembly? We can use sizoscope to check this, it'll remove around 1.4MB if so.

@charlesroddie charlesroddie Sep 16, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not every type but most. System.Configuration.ConfigurationManager.dll goes from 443KB untrimmed, to 133KB trimmed before this PR (i.e. with the switch on), to 42KB trimmed with the switch off.

The residual is to do with SqlAuthenticationProviderManager where is ongoing work.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without loading data from app.config, the constructor for SqlAuthenticationProviderManager has no work to do. One way to remove this might be to have a private parameterless constructor which no-ops or logs; the class could be instantiated with that if the EnableAppConfig switch is disabled.

if (configurationSection == null)
{
// If configuration section is not yet found, try with old Configuration Section name for backwards compatibility
configurationSection = FetchConfigurationSection<SqlAuthenticationProviderConfigurationSection>(SqlAuthenticationProviderConfigurationSection.Name);
}
}
}
catch (ConfigurationErrorsException e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ public sealed partial class SqlCommand : DbCommand, ICloneable
/// </summary>
private static readonly SqlDiagnosticListener s_diagnosticListener = new();

// Shared by all commands when app.config is not read.
private static SqlRetryLogicBaseProvider s_noneRetryProvider;

/// <summary>
/// Connection that will be used to process the current instance.
/// </summary>
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ private static readonly ConcurrentDictionary<string, IList<string>> _ColumnEncry
private static readonly Action<object> s_openAsyncCancel = OpenAsyncCancel;
private static readonly Action<Task<object>, 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);

/// <include file='../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/RetryLogicProvider/*' />
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<!-- Enable the unused SNI to be trimmed based upon the publish-time value of the Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows switch -->
<method signature="System.Boolean get_UseManagedNetworking()" body="stub" value="false" feature="Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows" featurevalue="false" />
<method signature="System.Boolean get_UseManagedNetworking()" body="stub" value="true" feature="Switch.Microsoft.Data.SqlClient.UseManagedNetworkingOnWindows" featurevalue="true" />
<!-- Enable the app.config reading to be trimmed based upon the publish-time value of the Switch.Microsoft.Data.SqlClient.EnableAppConfig switch -->
<method signature="System.Boolean get_EnableAppConfig()" body="stub" value="false" feature="Switch.Microsoft.Data.SqlClient.EnableAppConfig" featurevalue="false" />
<method signature="System.Boolean get_EnableAppConfig()" body="stub" value="true" feature="Switch.Microsoft.Data.SqlClient.EnableAppConfig" featurevalue="true" />
</type>
</assembly>
</linker>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -97,6 +98,8 @@ public LocalAppContextSwitchesHelper()
_disableTnirByDefaultOriginal =
GetSwitchValue("s_disableTnirByDefault");
#endif
_enableAppConfigOriginal =
GetSwitchValue("s_enableAppConfig");
_enableMultiSubnetFailoverByDefaultOriginal =
GetSwitchValue("s_enableMultiSubnetFailoverByDefault");
#if NET
Expand Down Expand Up @@ -159,6 +162,9 @@ public void Dispose()
"s_disableTnirByDefault",
_disableTnirByDefaultOriginal);
#endif
SetSwitchValue(
"s_enableAppConfig",
_enableAppConfigOriginal);
SetSwitchValue(
"s_enableMultiSubnetFailoverByDefault",
_enableMultiSubnetFailoverByDefaultOriginal);
Expand Down Expand Up @@ -242,6 +248,15 @@ public bool? DisableTnirByDefault
}
#endif

/// <summary>
/// Get or set the EnableAppConfig switch value.
/// </summary>
public bool? EnableAppConfig
{
get => GetSwitchPropertyValue(nameof(EnableAppConfig));
set => SetSwitchValue("s_enableAppConfig", value);
}

/// <summary>
/// Get or set the EnableMultiSubnetFailoverByDefault switch value.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Tests that the EnableAppConfig switch gates the configurable retry logic
/// providers used by commands and connections.
/// </summary>
/// <remarks>
/// The authentication provider and switch override readers run once, in
/// static constructors, so they cannot be exercised in-process.
/// </remarks>
[Collection(AppContextSwitchTestCollection.Name)]
public class EnableAppConfigSwitchTest
{
/// <summary>
/// With the switch off, commands share one non-retriable provider that
/// does not come from the configurable retry logic manager.
/// </summary>
[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);
}

/// <summary>
/// With the switch off, connections share one non-retriable provider that
/// does not come from the configurable retry logic manager.
/// </summary>
[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);
}

/// <summary>
/// With the switch on, commands and connections use the manager's
/// providers, as they did before the switch existed.
/// </summary>
[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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,6 +70,7 @@ public void TestDefaultAppContextSwitchValues()
Assert.False(switchesHelper.IgnoreServerProvidedFailoverPartner);
Assert.False(switchesHelper.UseLegacyFailoverAlternationOnLoginSqlErrors);
Assert.False(switchesHelper.EnableMultiSubnetFailoverByDefault);
Assert.True(switchesHelper.EnableAppConfig);
Comment thread
Copilot marked this conversation as resolved.
#if NET
Assert.False(switchesHelper.GlobalizationInvariantMode);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
Expand Down