AOT-safe auth provider with feature switch and trimmer support#4348
AOT-safe auth provider with feature switch and trimmer support#4348paulmedynski wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR exposes an AOT-friendly public authentication provider registration surface (SqlAuthenticationProviderManager) and introduces a feature switch to enable trimming away reflection-based Azure extension provider discovery for NativeAOT/trimming scenarios.
Changes:
- Made
SqlAuthenticationProviderManagerpublic withGetProvider/SetProviderAPIs and addedApplicationClientIdas part of the public surface. - Added
Microsoft.Data.SqlClient.EnableReflectionBasedAuthenticationProviderDiscoveryfeature switch with trimmer support ([FeatureSwitchDefinition]on .NET 9+ andILLink.Substitutions.xmlfor .NET 8). - Added an
tools/AotCompatibilitytest app plus new/updated unit & functional tests covering the new public API and config behavior.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/AotCompatibility/README.md | Documents the AOT compatibility test app and feature switch usage. |
| tools/AotCompatibility/Program.cs | Implements runtime checks and trimming verification via the ILC map file. |
| tools/AotCompatibility/Directory.Packages.props | Enables CPM for the tool (project-mode, no package versions). |
| tools/AotCompatibility/Directory.Build.props | Prevents inheriting repo-wide build props for the tool. |
| tools/AotCompatibility/AotCompatibility.csproj | Adds the NativeAOT/trimming test app and propagates the feature switch via RuntimeHostConfigurationOption. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlAuthenticationProviderManagerTests.cs | Adds unit tests for the new public manager API and interop with Abstractions. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/LocalAppContextSwitchesTest.cs | Extends default-switch tests to cover the new auth discovery switch. |
| src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlAuthenticationProviderManagerTests.cs | Adjusts tests (pragma) and adds validation for reading ApplicationClientId from config. |
| src/Microsoft.Data.SqlClient/tests/FunctionalTests/app.config | Adds applicationClientId attribute to the auth provider config section. |
| src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs | Adds pragma suppression around newly-obsoleted Abstractions API usage. |
| src/Microsoft.Data.SqlClient/tests/Common/LocalAppContextSwitchesHelper.cs | Adds helper plumbing to reset the new switch in tests. |
| src/Microsoft.Data.SqlClient/src/Resources/ILLink.Substitutions.xml | Adds substitutions for the new feature switch (and retains UseManagedNetworking stubs). |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs | Makes the type public and guards reflection-based discovery behind the new switch; adds AOT/trimming annotations. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs | Introduces the new switch property and [FeatureSwitchDefinition] for .NET 9+. |
| src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj | Embeds ILLink.Substitutions.xml for all non-net462 TFMs. |
| src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs | Adds the new public API to the reference assembly (without nullable annotations). |
| src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.Internal.cs | Updates reflection binding flags to target public manager methods. |
| src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationProvider.cs | Marks legacy Abstractions APIs obsolete in favor of the new manager APIs. |
| doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationProviderManager.xml | Adds public XML doc content for the new manager API. |
Comments suppressed due to low confidence (1)
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs:346
- SetProvider is now a public API but does not validate the provider argument; passing null currently results in a NullReferenceException. Public APIs should throw ArgumentNullException for null arguments.
/// <include file='../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationProviderManager.xml' path='docs/members[@name="SqlAuthenticationProviderManager"]/SetProvider/*'/>
public static bool SetProvider(SqlAuthenticationMethod authenticationMethod, SqlAuthenticationProvider provider)
{
if (!provider.IsSupported(authenticationMethod))
{
throw SQL.UnsupportedAuthenticationByProvider(authenticationMethod.ToString(), provider.GetType().Name);
- Fix SetProvider exception docs: SqlException → NotSupportedException - Clarify AOT remarks: static ctor uses reflection by default; describe how to disable it for full AOT compatibility - Return non-zero exit code on SqlConnection construction failure - Replace Directory.GetFiles with EnumerateFiles(IgnoreInaccessible) - Clarify README: 'the test app default' vs library default - Add XML summary docs to all new unit test methods - Remove [Obsolete] from SqlAuthenticationProvider.GetProvider/SetProvider; add remarks pointing to SqlAuthenticationProviderManager equivalents noting future deprecation and removal - Remove corresponding #pragma CS0618 suppressions from tests - Document applicationClientId in test app.config
paulmedynski
left a comment
There was a problem hiding this comment.
I decided against the obsoletion.
… doc fixes - Wrap `using System.Diagnostics.CodeAnalysis` in #if NET (unused on net462) - Gate ILLink substitution tests by TFM; add NETFRAMEWORK negative assertions - Fix remarks: "on all TFMs" -> "on .NET 8+" (net462 has no trimmer) - Add -f/-r flags to AotCompatibility README publish examples
|
Addressed all actionable review feedback in 11d75ab:
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4348 +/- ##
==========================================
- Coverage 65.39% 63.42% -1.98%
==========================================
Files 285 280 -5
Lines 43331 66217 +22886
==========================================
+ Hits 28337 41999 +13662
- Misses 14994 24218 +9224
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Pull request was converted to draft
|
We have another design approach that we would like to explore, so pausing this PR for the time being. |
- Fix SetProvider exception docs: SqlException → NotSupportedException - Clarify AOT remarks: static ctor uses reflection by default; describe how to disable it for full AOT compatibility - Return non-zero exit code on SqlConnection construction failure - Replace Directory.GetFiles with EnumerateFiles(IgnoreInaccessible) - Clarify README: 'the test app default' vs library default - Add XML summary docs to all new unit test methods - Remove [Obsolete] from SqlAuthenticationProvider.GetProvider/SetProvider; add remarks pointing to SqlAuthenticationProviderManager equivalents noting future deprecation and removal - Remove corresponding #pragma CS0618 suppressions from tests - Document applicationClientId in test app.config
… doc fixes - Wrap `using System.Diagnostics.CodeAnalysis` in #if NET (unused on net462) - Gate ILLink substitution tests by TFM; add NETFRAMEWORK negative assertions - Fix remarks: "on all TFMs" -> "on .NET 8+" (net462 has no trimmer) - Add -f/-r flags to AotCompatibility README publish examples
- Rename s_useManagedNetworking → s_useManagedNetworkingOnWindows in LocalAppContextSwitchesHelper to match field rename in LocalAppContextSwitches (3 sites: ctor, Dispose, property) - Use parameterless ActiveDirectoryAuthenticationProvider ctor when ApplicationClientId is null to preserve built-in default client ID - Add -f net9.0 -r linux-x64 to AOT README publish-with-reflection example
- Stream ILC map file line-by-line instead of File.ReadAllText to avoid memory pressure from large map files (tools/AotCompatibility) - Add warning when AppContext feature switch is absent (expected in JIT mode) - Fix stale app.config comment referencing removed public ApplicationClientId - Move app.config auth provider tests from FunctionalTests to UnitTests - Add separate token-acquisition test for config-registered provider - Remove duplicate IsDummySqlAuthenticationProviderSetByDefault test, its DummySqlAuthenticationProvider, and app.config from FunctionalTests - Add internal ApplicationClientId property for test verification
f1da5b1 to
99a0cb9
Compare
| /// <include file='../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationProviderManager.xml' path='docs/members[@name="SqlAuthenticationProviderManager"]/ApplicationClientId/*'/> | ||
| public static string? ApplicationClientId => Instance._applicationClientId; | ||
| /// <summary> | ||
| /// Gets the application client ID read from the app.config configuration section, | ||
| /// or <see langword="null"/> if none was configured. | ||
| /// </summary> | ||
| internal static string? ApplicationClientId => Instance._applicationClientId; | ||
|
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 64 out of 65 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.Designer.cs: Generated file
Comments suppressed due to low confidence (2)
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AuthenticationBootstrapper.cs:404
LoadAzureExtensionProvider()registers the Azure extension as the default provider by calling_registry.SetProvider(...)for each Active Directory method. Under the new lazy-bootstrap model, this can override a provider that was already registered viaSqlAuthenticationProvider.SetProvider()earlier in process startup (before the first federated/AD connection authenticates), changing the previous precedence (user registration should win over “default” providers).
To preserve expected behavior, the bootstrapper should only set these defaults when no provider is currently registered (or introduce an atomic TrySetProviderIfAbsent API on AuthenticationProviderRegistry and use it here).
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AuthenticationBootstrapper.cs:39
- PR description says it makes
SqlAuthenticationProviderManagerpublic and exposesGetProvider/SetProvider, but the implementation in this diff removesSqlAuthenticationProviderManagerentirely and instead introduces an internalAuthenticationBootstrapper+ an Abstractions-levelAuthenticationProviderRegistrybackingSqlAuthenticationProvider.GetProvider/SetProvider. Please update the PR description to reflect the actual public surface and architecture (registry moved into Abstractions; lazy bootstrap in core).
| if (_permanentProviders.Contains(authenticationMethod)) | ||
| { | ||
| SqlClientEventSource.Log.TryTraceEvent( | ||
| "AuthenticationProviderRegistry.SetProvider | Failed to add provider {0} because a " + | ||
| "permanent provider with type {1} already existed for authentication {2}.", |
Gate the reflection-based config/Azure-extension provider discovery behind a
feature switch so the trimmer can remove it, and relocate the provider registry
into the shared Abstractions assembly. The public surface stays
SqlAuthenticationProvider.GetProvider/SetProvider (no new public type).
- Registry: move into Abstractions as internal AuthenticationProviderRegistry
(one ConcurrentDictionary of a ProviderEntry record struct; IsExternalInit
polyfill for netstandard2.0); delete the Abstractions->core reflection bridge.
- Bootstrapper: replace SqlAuthenticationProviderManager with an internal,
core-side AuthenticationBootstrapper that lazily (Lazy<T>) discovers
config-driven and Azure-extension providers and seeds the registry, triggered
once from SqlConnectionInternal.GetFedAuthToken.
- AOT: feature switch
Microsoft.Data.SqlClient.EnableReflectionBasedAuthenticationProviderDiscovery
([FeatureSwitchDefinition] on .NET 9+, ILLink.Substitutions.xml on .NET 8)
trims LoadAzureExtensionProvider when disabled; AotCompatibility tool
validates Native AOT publish and trimming.
- Behavior: app-config ("permanent") providers take precedence and are
last-in-wins; BeforeLoad fires on first registration; lifecycle callbacks are
isolated from exceptions and documented as idempotent.
- Tests: registry and bootstrapper tests reorganized -- UnitTests covers the
Azure-absent and stub paths, Azure.Test covers the real provider (global vs
non-global split); serialized collection renamed to SqlAuthenticationProviderGlobal.
ca1cc20 to
936715e
Compare
| the AuthenticationProviderRegistry (internal to this assembly). --> | ||
| <ItemGroup Condition="'$(SigningKeyPath)' == ''"> | ||
| <InternalsVisibleTo Include="$(AssemblyName).Test" /> | ||
| <InternalsVisibleTo Include="UnitTests" /> |
There was a problem hiding this comment.
Give SqlClient access to Abstractions internals so it can call SetPermanentProvider().
Give UnitTests access to Abstractions internals so it can use non-global isloated provider registry instances.
| /// <see cref="SqlAuthenticationProvider.SetProvider"/> accessors. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Production code uses this shared instance. Tests can instead construct an isolated |
There was a problem hiding this comment.
I think we can apply this pattern to other globals and singletons (like LocalAppContextSwitches) to avoid tests clobbering each other.
| // addValueFactory: no provider is registered for this method yet. | ||
| (SqlAuthenticationMethod key) => | ||
| { | ||
| InvokeProviderCallback(provider, provider.BeforeLoad, key, nameof(SqlAuthenticationProvider.BeforeLoad)); |
There was a problem hiding this comment.
The old Manager SetProvider() wasn't calling BeforeLoad when a provider was being registered for the first time against a particular method. This was a bug - the docs say BeforeLoad is always called before registering a provider, regardless of whether or not it was new or a replacement.
The old code also wasn't protecting against BeforeLoad/Unload callbacks that threw, which would abort the AddOrUpdate(). Now we catch-and-ignore in these Invoke() helpers, preventing misbehaving providers from perturbing the registry. This was also a bug.
| /// Callers are responsible for verifying that the provider supports the authentication method | ||
| /// before registering it. | ||
| /// <para> | ||
| /// This is a last-in-wins operation: a later <see cref="SetPermanentProvider"/> call for the |
There was a problem hiding this comment.
This behaviour isn't documented anywhere public, but it is preserved from the original Manager implementation.
| /// (used by records and init-only properties). It is provided by the BCL on .NET, but not on | ||
| /// the netstandard2.0 / .NET Framework targets this assembly supports, so we define it here. | ||
| /// </summary> | ||
| internal static class IsExternalInit |
There was a problem hiding this comment.
This lets the C# 14 compiler write the ProviderEntry getters/setters properly when targeting .NET Standard 2.0.
| @@ -432,55 +509,10 @@ private SqlAuthenticationProviderManager(SqlAuthenticationProviderConfigurationS | |||
| return null; | |||
| } | |||
|
|
|||
| /// <summary> | |||
There was a problem hiding this comment.
Deleted entirely - now lives in Abstractions.
| @@ -57,8 +57,9 @@ public sealed class LocalAppContextSwitchesHelper : IDisposable | |||
| private readonly bool? _useOverallConnectTimeoutForPoolWaitOriginal; | |||
| #if NET && _WINDOWS | |||
There was a problem hiding this comment.
Note that these _WINDOWS conditions are never set in tests, so these branches are currently never compiled or tested. That is tracked elsewhere to be fixed.
| /// that configuration source. | ||
| /// </summary> | ||
| [ConditionalFact(typeof(TestUtility), nameof(TestUtility.IsNetFramework))] | ||
| public async Task IsDummySqlAuthenticationProviderSetByDefault() |
There was a problem hiding this comment.
Replaced by Bootstrapper tests.
There was a problem hiding this comment.
@paulmedynski - Remove this before we merge the PR.
… tests - AuthenticationBootstrapper: replace backing fields with internal auto-properties (Registry, ApplicationClientId, UseWamBroker). - AuthenticationProviderRegistry: remove PermanentProviderException sentinel; document SetProvider null behavior. - UnitTests: assert Registry/ApplicationClientId/UseWamBroker; add UseWamBroker config tests (app.config sets useWamBroker=true).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 53 out of 54 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Strings.Designer.cs: Generated file
Comments suppressed due to low confidence (1)
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AuthenticationBootstrapper.cs:392
- This comment is now misleading:
AuthenticationProviderRegistry.SetProviderwill override any existing non-permanent provider, and only refuses to replace permanent (app.config) providers. As written, it suggests defaults apply only when no provider exists, which is not accurate.
| if (!reflectionEnabled && hasLoadAzure) | ||
| { | ||
| Console.ForegroundColor = ConsoleColor.Red; | ||
| Console.WriteLine(" FAIL: Reflection code was NOT trimmed!"); | ||
| Console.ResetColor(); | ||
| return 1; | ||
| } | ||
| else if (!reflectionEnabled && !hasLoadAzure) | ||
| { | ||
| Console.ForegroundColor = ConsoleColor.Green; | ||
| Console.WriteLine(" PASS: Reflection code was successfully trimmed."); | ||
| Console.ResetColor(); | ||
| } | ||
| else if (reflectionEnabled && hasLoadAzure) | ||
| { | ||
| Console.ForegroundColor = ConsoleColor.Green; | ||
| Console.WriteLine(" PASS: Reflection code is present (as expected with discovery enabled)."); | ||
| Console.ResetColor(); | ||
| } | ||
| else | ||
| { | ||
| Console.ForegroundColor = ConsoleColor.Yellow; | ||
| Console.WriteLine(" WARN: Reflection code absent despite discovery being enabled."); | ||
| Console.ResetColor(); | ||
| } |
Summary
Make authentication-provider discovery AOT-/trimmer-safe and restructure the provider registry so the reflection-based config/Azure-extension discovery can be trimmed away behind a feature switch.
The reflection that loaded the Azure extension and read
app.configis now isolated in a single core-side component, gated by a feature switch, and the provider registry itself was relocated into the shared Abstractions assembly. The public entry point remainsSqlAuthenticationProvider.GetProvider()/SetProvider()— no new public type is introduced.🔗 PR Stack
Part of a 6-PR stack — current PR marked 👉. Indentation shows the branch base.
mainSTRONG_NAME_SIGNING→ASSEMBLY_SIGNINGMicrosoft.SqlServer.Serverassembly & CIflowchart TD main([main]) PR1["🏗️ #4382<br/>signing-core"] AOT["✂️ #4348<br/>aot"] PR2["🏷️ #4383<br/>rename"] PR3["🪵 #4384<br/>logging-tests"] PR4["☁️ #4385<br/>azure-signing"] PR5["🧩 #4386<br/>sqlserver.server"] main --> PR1 PR1 --> AOT PR1 --> PR2 PR2 --> PR3 PR2 --> PR4 PR2 --> PR5 click PR1 "https://github.com/dotnet/SqlClient/pull/4382" _blank click AOT "https://github.com/dotnet/SqlClient/pull/4348" _blank click PR2 "https://github.com/dotnet/SqlClient/pull/4383" _blank click PR3 "https://github.com/dotnet/SqlClient/pull/4384" _blank click PR4 "https://github.com/dotnet/SqlClient/pull/4385" _blank click PR5 "https://github.com/dotnet/SqlClient/pull/4386" _blank classDef current fill:#1f6feb,stroke:#1f6feb,color:#fff; class AOT current;Architecture
AuthenticationProviderRegistry(internal) now lives inMicrosoft.Data.SqlClient.Extensions.Abstractionsand backs the public staticSqlAuthenticationProvider.GetProvider/SetProvider. The old Abstractions→core reflection bridge (SqlAuthenticationProvider.Internal.cs) is deleted.SqlAuthenticationProviderManagerremoved. It is replaced by an internal, core-sideAuthenticationBootstrapperthat performs the (reflection-based) config-driven and Azure-extension provider discovery and seeds the shared registry. It is lazy:AuthenticationBootstrapper.Bootstrap()— invoked once fromSqlConnectionInternal.GetFedAuthToken— triggers a run-onceLazy<T>initialization. Constructing a bootstrapper instance directly (used by tests) has no global side effect.AuthenticationProviderRegistryandAuthenticationBootstrappertake an injected registry so tests can exercise them against isolated instances;UnitTestsis grantedInternalsVisibleTofrom Abstractions.AOT / trimming
Microsoft.Data.SqlClient.EnableReflectionBasedAuthenticationProviderDiscovery[FeatureSwitchDefinition]on .NET 9+ for linker-integrated trimming.ILLink.Substitutions.xmlfor .NET 8 trimmer support.false, the reflectiveLoadAzureExtensionProvider()(and itsAssembly.Load/Activator.CreateInstancepaths) is trimmed away.ILLink.Substitutions.xml(embedded on all non-net462TFMs) carries both the auth-provider feature switch and theUseManagedNetworkingOnWindowsentries; the managed-networking property uses a runtime platform guard so the substitution is safe on all platforms.tools/AotCompatibility/): validates Native AOT publish succeeds and that the trimmer removes the reflection code when the switch is disabled.Registry behavior
ConcurrentDictionary<SqlAuthenticationMethod, ProviderEntry>(areadonly record structcarrying the provider + anIsPermanentflag), replacing the previous two-map design. AnIsExternalInitpolyfill is added so the record compiles onnetstandard2.0.SetProviderrefuses to overwrite a permanent provider (and the failure path is driven throughAddOrUpdate);SetPermanentProvideris last-in-wins. This matches the previousSqlAuthenticationProviderManagerbehavior.SetProvideruses add/update factories soBeforeLoadfires on the first registration too (matching the documented contract), and theBeforeLoad/BeforeUnloadlifecycle callbacks are routed through a single helper that logs and swallows provider exceptions. Their docs now state they must be idempotent (the concurrentAddOrUpdatefactory may run more than once).Tests
AuthenticationProviderRegistryTest.cs, with added coverage for permanent last-in-wins, lifecycle-callback invocation/isolation, and per-method keying.UnitTestsasserts (in its ctor) that the Azure extension is absent and covers the Azure-absent + stub-basedCreateAzureAuthenticationProviderpaths.CreateAzureAuthenticationProviderconstructor selection (no global state) and the end-to-end bootstrap (global). The serialized collection is renamed toSqlAuthenticationProviderGlobal, and tests that don't mutate global state were freed from it.Verification
dotnet build build.projsucceeds (Project mode) and-p:ReferenceType=Package.net9.0core build has no trimmer/IL warnings; the feature-switch substitution keepsLoadAzureExtensionProvidertrimmable.SqlAuthenticationProviderManageror the Abstractions→core reflection bridge.