You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On .NET, SqlClient reads app.config from three places:
SqlConfigurableRetryLogicManager, for the retry sections. Reached from SqlConnection.Open and every SqlCommand.Execute*, which read RetryLogicProvider unconditionally.
LocalAppContextSwitches' static constructor, for the AppContextSwitchOverrides section.
SqlAuthenticationProviderManager's static constructor, for the auth provider sections.
ConfigurationManager.GetSection resolves section handler types named as strings in the config file, so reaching it from anywhere leaves five trim warnings inside System.Configuration.TypeUtil (IL2026, two IL2057, IL2067, IL2070). They are in the dependency rather than in SqlClient, so no annotation here can remove them, and they are a fixed cost rather than a per-call-site one: a single ungated caller keeps all five. dotnet/runtime#49062 is closed with no fix planned, and upstream's guidance is to use Microsoft.Extensions.Configuration instead, which is what the @TODO comments on both FetchConfigurationSection copies already anticipate.
Annotating public entry points with RequiresUnreferencedCode is not an option for the retry path, because it is reached from Open and Execute* rather than from retry-specific API, so the annotation would propagate onto every caller of those methods.
Change
A new Switch.Microsoft.Data.SqlClient.EnableAppConfig, defaulting to true, with a guard at each app.config reader. It is .NET only: trimming does not apply to .NET Framework, where the switch is a constant true. When the switch is fixed at publish time, ILLink.Substitutions.xml makes the trimmer treat the property as a constant, as it already does for UseManagedNetworking, so the configuration reading becomes dead code and is removed.
The retry guards sit at the SqlCommand and SqlConnection call sites rather than inside SqlConfigurableRetryLogicManager, because that class builds its loader in a static field initializer, so touching the type at all would run the configuration reading regardless of the guard.
Measurement
Native AOT publish of a small ASP.NET Core application that opens a connection, runs a stored procedure with a DataTable-valued parameter inside a transaction, reads results, and uses Azure Monitor OpenTelemetry, with TrimmerSingleWarn=false. Measured on top of main plus #4683, #4684 and #4688, counting only warnings the application can reach:
SqlClient
System.Configuration
total
Before
24
5
29
Publishing with the switch set to false
19
0
19
The ten removed are the five in System.Configuration.TypeUtil and the five in SqlConfigurableRetryLogicLoader.
Behaviour
Default true keeps today's behaviour exactly. Set to false, app.config is ignored: no configurable retry logic from config, no config-declared authentication providers, and no AppContextSwitchOverrides. This is a real behaviour change rather than a pure trimming knob, so it is opt-in per application.
The switch is read before any override is applied, so it cannot itself be set from the app.config that it gates. It has to be set through AppContext or runtimeconfig.
Open question
features.instructions.md says new switches should default to false and opt in to new behaviour. This one gates behaviour that already exists, and the default has to mean "the feature is present" for the substitution to trim the disabled path, so it defaults to true. Happy to rename it to DisableAppConfig if you would rather follow the guideline literally, though that makes the substitution entries read backwards.
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Also list the .NET Framework system.data.localdb section among what the
switch gates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
🟢 Approval recommended
The switch consistently gates all identified configuration readers, preserves default behavior, and includes focused documentation and tests.
Review details
Files reviewed: 10/10 changed files
Comments generated: 0 new
Review effort level: Balanced
charlesroddie
changed the title
Add EnableAppConfig switch to remove System.Configuration trim warnings
Add an EnableAppConfig switch to remove trim warnings
Sep 16, 2026
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.
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.
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.
The reason will be displayed to describe this comment to others. Learn more.
That's a useful observation but leaving this out of this PR as the switch currenly does what it says literally and there are other PRs touching this area, including #4670 and #4573 . Ideally, when this goes in, the switch in #4573 can be updated to honour EnableAppConfig and likely that will allow all this to be trimmed out. I'll keep track of it.
The reason will be displayed to describe this comment to others. Learn more.
The approach looks sound: the substitution follows UseManagedNetworking, and EnableAppConfig with a default of true is the right naming choice.
The main gap is coverage: the retry tests initialize the config-reading manager, so they verify provider selection, not that config reads are skipped. Please cover that behavior and the auth-provider/LocalDB gates, and document that the .NET Framework LocalDB gate changes runtime behavior without a trimming benefit.
The reason will be displayed to describe this comment to others. Learn more.
This NotSame forces SqlConfigurableRetryLogicManager to initialize, which runs the config read the switch is meant to avoid — so the test can't distinguish "config not read" from "different provider returned". Compare against SqlConfigurableRetryFactory.CreateNoneRetryProvider's behaviour instead, or assert in a separate process/AppDomain.
The reason will be displayed to describe this comment to others. Learn more.
Fixed in ea407c1. The disabled tests now compare against the factory's shared no-retry provider instead of the manager.
"Config not read" is now asserted directly: a new test collects SqlClient's trace events while the providers are obtained. Since every path through AppConfigManager.FetchConfigurationSection traces the section name, the absence of such a trace shows app.config wasn't read. The test reads a section deliberately and asserts the trace appears, so it can't pass merely because tracing is unavailable. That made a separate process unnecessary.
The reason will be displayed to describe this comment to others. Learn more.
SqlCommand and SqlConnection each keep their own s_noneRetryProvider, so the disabled path allocates two equivalent none-providers. Hoisting one static onto SqlConfigurableRetryFactory would avoid the duplication.
SqlCommand and SqlConnection each kept their own, so the disabled path
built two equivalent providers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🟢 Approval recommended
The switch consistently gates all identified configuration readers, preserves compatibility by default, supports trimming, and has focused retry-path coverage.
Trimming does not apply to .NET Framework, so gating app.config there only
changed behaviour with no benefit. The switch is now a constant true on
.NET Framework, and the LocalDB gate, which only existed in that build, is
gone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous test compared against SqlConfigurableRetryLogicManager, which
initialized it and so performed the config read the switch avoids. Compare
against the factory's shared provider instead, and add a test that collects
SqlClient's trace events while the providers are obtained: every path
through AppConfigManager traces the section name, so the absence of such a
trace shows app.config was not read. A deliberate read at the end of the
test proves the check cannot pass merely because tracing is unavailable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This switch violates the repository rule that new AppContext switches default to false and opt in to new behavior (.github/instructions/features.instructions.md:289-294). Since ignoring app.config is the opt-in behavior and existing behavior must remain the default, please name this DisableAppConfig, default it to false, and invert the guards and publish-time setting. ILLink supports substituting the true feature value, so trimming does not require an enabled-by-default switch.
The main gap is coverage: the retry tests initialize the config-reading manager, so they verify provider selection, not that config reads are skipped. Please cover that behavior
Retry is covered in ea407c1. The disabled tests no longer touch the manager, and a new one asserts that no config-read trace is emitted while the providers are obtained.
the auth-provider/LocalDB gates, and document that the .NET Framework LocalDB gate changes runtime behavior without a trimming benefit.
The netframework behaviour is modified in 259bad2, so that netframework is unaffected by the switch. This is documented in the switch table in features.instructions.md. That resolves the localdb part of this which is scoped to netframework: the only read of the system.data.localdb section is LocalDbApi.cs:185, inside a #if NETFRAMEWORK block around CreateLocalDbInstance.
Auth providers are not covered: that reader runs once in a static constructor, so it needs a separate process, and the repo doesn't have a harness for that.
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of the work toward #1947.
Problem
On .NET, SqlClient reads app.config from three places:
SqlConfigurableRetryLogicManager, for the retry sections. Reached fromSqlConnection.Openand everySqlCommand.Execute*, which readRetryLogicProviderunconditionally.LocalAppContextSwitches' static constructor, for theAppContextSwitchOverridessection.SqlAuthenticationProviderManager's static constructor, for the auth provider sections.ConfigurationManager.GetSectionresolves section handler types named as strings in the config file, so reaching it from anywhere leaves five trim warnings insideSystem.Configuration.TypeUtil(IL2026, two IL2057, IL2067, IL2070). They are in the dependency rather than in SqlClient, so no annotation here can remove them, and they are a fixed cost rather than a per-call-site one: a single ungated caller keeps all five. dotnet/runtime#49062 is closed with no fix planned, and upstream's guidance is to useMicrosoft.Extensions.Configurationinstead, which is what the@TODOcomments on bothFetchConfigurationSectioncopies already anticipate.Annotating public entry points with
RequiresUnreferencedCodeis not an option for the retry path, because it is reached fromOpenandExecute*rather than from retry-specific API, so the annotation would propagate onto every caller of those methods.Change
A new
Switch.Microsoft.Data.SqlClient.EnableAppConfig, defaulting totrue, with a guard at each app.config reader. It is .NET only: trimming does not apply to .NET Framework, where the switch is a constanttrue. When the switch is fixed at publish time,ILLink.Substitutions.xmlmakes the trimmer treat the property as a constant, as it already does forUseManagedNetworking, so the configuration reading becomes dead code and is removed.The retry guards sit at the
SqlCommandandSqlConnectioncall sites rather than insideSqlConfigurableRetryLogicManager, because that class builds its loader in a static field initializer, so touching the type at all would run the configuration reading regardless of the guard.Measurement
Native AOT publish of a small ASP.NET Core application that opens a connection, runs a stored procedure with a
DataTable-valued parameter inside a transaction, reads results, and uses Azure Monitor OpenTelemetry, withTrimmerSingleWarn=false. Measured on top of main plus #4683, #4684 and #4688, counting only warnings the application can reach:falseThe ten removed are the five in
System.Configuration.TypeUtiland the five inSqlConfigurableRetryLogicLoader.Behaviour
Default
truekeeps today's behaviour exactly. Set tofalse, app.config is ignored: no configurable retry logic from config, no config-declared authentication providers, and noAppContextSwitchOverrides. This is a real behaviour change rather than a pure trimming knob, so it is opt-in per application.The switch is read before any override is applied, so it cannot itself be set from the app.config that it gates. It has to be set through
AppContextorruntimeconfig.Open question
features.instructions.mdsays new switches should default tofalseand opt in to new behaviour. This one gates behaviour that already exists, and the default has to mean "the feature is present" for the substitution to trim the disabled path, so it defaults totrue. Happy to rename it toDisableAppConfigif you would rather follow the guideline literally, though that makes the substitution entries read backwards.🤖 Generated with Claude Code