[Azure.Core] Phase 2a: AzureCredentialResolver + scope flip + ApiKey deletion#59256
[Azure.Core] Phase 2a: AzureCredentialResolver + scope flip + ApiKey deletion#59256m-nash wants to merge 3 commits into
Conversation
…deletion - Add experimental AzureCredentialResolver (SCME0002) that resolves Azure token credential sections via System.ClientModel.Primitives.CredentialResolver. ApiKeyCredential sections are intentionally not claimed; clients dispatch on Credential.CredentialSource themselves. - Add IConfiguration.GetAzureCredential, IConfiguration.GetAzureClientSettings<T> resolver-aware overloads, and AddAzureCredentialResolver on IServiceCollection and IHostApplicationBuilder. - Flip Azure OpenAI default-scope quirk to write Credential:Scope at the credential section root (canonical SCM 1.12.0+ location). SCM 1.12.0 reads both locations so existing configs continue to work. - Tests: 35 resolver tests + 6 scope-flip tests + DI/standalone coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Phase 2a of the Azure.Core CredentialResolver migration. Introduces a new experimental (SCME0002) Azure.Identity.AzureCredentialResolver plus convenience extensions on IConfiguration, IServiceCollection, and IHostApplicationBuilder for resolving Azure token credentials from configuration. Also flips the Azure OpenAI default-scope quirk to write Credential:Scope at the canonical SCM 1.12.0+ location, and stops claiming ApiKeyCredential sections so consuming clients dispatch on Credential.CredentialSource themselves.
Changes:
- New public type
AzureCredentialResolverand extension methodsGetAzureCredential, resolver-awareGetAzureClientSettings<T>, andAddAzureCredentialResolveron DI/host builders. - Azure OpenAI default-scope is now written at the credential-section root instead of
AdditionalProperties:Scope(legacyWithAzureCredentialpath updated to match). - New tests (resolver behavior, DI idempotency, default-scope quirk on both code paths) and compile-validated doc snippets backing updated
ConfigurationAndDependencyInjection.mdandExperimentalFeatures.md.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| sdk/core/Azure.Core/src/Identity/AzureCredentialResolver.cs | New resolver with ApiKey opt-out and Chained/single-source dispatch. |
| sdk/core/Azure.Core/src/Identity/ConfigurationExtensions.cs | New GetAzureCredential, resolver-aware GetAzureClientSettings<T>, AddAzureCredentialResolver; root-scope flip. |
| sdk/core/Azure.Core/api/Azure.Core.{net462,net472,netstandard2.0,net8.0,net10.0}.cs | Public API baselines updated additively. |
| sdk/core/Azure.Core/src/docs/ConfigurationAndDependencyInjection.md | Reworked sections + new Custom Credential Resolvers guidance. |
| sdk/core/Azure.Core/src/docs/ExperimentalFeatures.md | Updated extension list and equivalence snippet. |
| sdk/core/Azure.Core/CHANGELOG.md | Adds entries describing the new APIs and scope-flip. |
| sdk/core/Azure.Core/tests/Identity/CredentialResolvers/AzureCredentialResolverTests.cs | Resolver dispatch coverage including ApiKey opt-out and chained path. |
| sdk/core/Azure.Core/tests/Identity/CredentialResolvers/AzureCredentialResolverDITests.cs | DI registration idempotency. |
| sdk/core/Azure.Core/tests/Identity/CredentialResolvers/GetAzureCredentialTests.cs | Helper coverage including custom-vs-built-in precedence and overrides. |
| sdk/core/Azure.Core/tests/Identity/CredentialResolvers/AzureOpenAIDefaultScopeTests.cs | Default-scope quirk on resolver-aware and legacy paths. |
| sdk/core/Azure.Core/tests/Identity/samples/DocSnippets/*.cs | Compile-validated doc snippet types and snippet methods. |
| params CredentialResolver[] resolvers) | ||
| where T : ClientSettings, new() | ||
| { | ||
| CredentialResolver[] combined = [.. resolvers ?? [], AzureCredentialResolver.Instance]; |
There was a problem hiding this comment.
That's a nifty little splat pattern.
| using IHost host = builder.Build(); | ||
| CredentialResolver[] resolvers = host.Services.GetServices<CredentialResolver>().ToArray(); | ||
|
|
||
| Assert.AreEqual(1, resolvers.Length); |
There was a problem hiding this comment.
Please move to Assert.That format; it's required by NUnit 4+ and we should try to avoid accumulating more migration debt since we're trying to move the repo.
| { | ||
| IConfiguration config = BuildConfig(new Dictionary<string, string>()); | ||
|
|
||
| Assert.IsNull(config.GetAzureCredential("Nope")); |
…solver-azurecore-phase2
Modern AddAzureClient / AddKeyedAzureClient / GetAzureClientSettings no longer route through WithAzureCredential (which transitively uses ClientSettings.PostConfigure, IClientBuilder.PostConfigure, and the ClientSettings.CredentialProvider shim — all slated for SCM phase 5 removal). They register AzureCredentialResolver.Instance via TryAddEnumerable and apply the Azure OpenAI default-scope quirk by writing the scope directly to the original IConfiguration credential section (when it exists), matching the WithAzureCredential side-effect contract. AddAzureCredentialResolver now registers the static singleton instance instead of activating a new one so DI and standalone paths share the same SCM CredentialCache bucket (cross-path credential instance sharing preserved with the new API surface). Adopt the SCM 1.13.0 surface: rename GetAzureCredential -> GetAzureCredentialSettings (returns CredentialSettings? so ApiKey callers can dispatch on Credential.Key). API baselines regenerated for all 5 TFMs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| ```C# Snippet:Azure_Core_Samples_AzureClient_GetCredential | ||
| TokenCredential credential = configuration.GetAzureCredential("MyClient:Credential"); | ||
| ```C# Snippet:Azure_Core_Samples_AzureClient_GetCredentialSettings | ||
| CredentialSettings credential = configuration.GetAzureCredentialSettings("MyClient:Credential"); |
There was a problem hiding this comment.
Wasn't there also a case where a true token provider would be returned and you'd have to invoke the factory to create the credential?
| // the same section. Skips when the configured endpoint is not an | ||
| // AzureOpenAI endpoint or when the Credential section is absent — we | ||
| // never materialize a Credential section just to attach a scope. | ||
| private static void ApplyAzureOpenAIDefaultScopeIfNeeded(IConfiguration configuration, string sectionName) |
There was a problem hiding this comment.
This is suuuuper specific. What's the story?

Summary
Phase 2a of the Azure.Core CredentialResolver migration. Introduces
Azure.Identity.AzureCredentialResolver(experimental,SCME0002) — an Azure-flavored implementation ofSystem.ClientModel.Primitives.CredentialResolver— plus convenience extensions onIConfiguration,IServiceCollection, andIHostApplicationBuilder. Also flips the Azure OpenAI default-scope quirk to write at the canonical SCM 1.12.0+ location, and stops the resolver from claimingApiKeyCredentialsections.What's changed
New public API (
SCME0002)Azure.Identity.AzureCredentialResolver— aCredentialResolverthat resolves Azure token credential sections (AzureCliCredential,ManagedIdentityCredential,ChainedTokenCredential, etc.) intoTokenCredentialinstances.IConfiguration.GetAzureCredential(sectionName, ...)— returnsTokenCredential?;nullwhen no resolver claims the section. Never throws.IConfiguration.GetAzureClientSettings<T>(sectionName, params CredentialResolver[] resolvers)plusIEnumerable<CredentialResolver>+Action<IConfigurationSection>overloads.IServiceCollection.AddAzureCredentialResolver()andIHostApplicationBuilder.AddAzureCredentialResolver()— idempotent DI registration.ApiKeyCredentialownershipAzureCredentialResolverdoes not claimApiKeyCredentialsections. Service-specific Azure clients dispatch onCredential.CredentialSourcethemselves and readCredential.Keydirectly.ApiKeyTokenCredential(internal) was deleted.Azure OpenAI default-scope flip
The default-scope quirk now writes
Credential:Scopeat the credential-section root (canonical SCM 1.12.0+ location) instead ofCredential:AdditionalProperties:Scope. SCM 1.12.0'sAuthenticationPolicy.Createreads from both locations (root preferred, AdditionalProperties as fallback) so existing configurations continue to work.Tests
AzureCredentialResolverresolver tests (all token sources, chained, missing sections).GetAzureCredentialandGetAzureClientSettings<T>.ConfigurationAndDependencyInjection.mdandExperimentalFeatures.md.Build / test status
net462,net472,netstandard2.0,net8.0,net10.0).CredentialResolverstests pass onnet8.0.BrokerCredentialTests.GetToken_ExceptionType_MatchesExpectedfailures are unrelated to this PR.Notes
WithAzureCredentialandConfigurableCredentialare unchanged in this PR and will be removed in phase 2b.ApiKeyTokenCredentialwas internal).