Configuration Schema Differences: Microsoft.Extensions.Azure (Old) vs New Config System
Configuration Schema Differences: Microsoft.Extensions.Azure (Old) vs New Config System
This document catalogs every difference between the configuration schema used by the
old Microsoft.Extensions.Azure client registration system and the new
IConfigurationSection-based system being built. Each finding includes links to the
relevant source code in the
azure-sdk-for-net repository.
Permalink base: All links point to commit
7c1e032on themainbranch.
- Background: How Each System Works
- Options Layer Flattening
- Credential Configuration Restructured
- Credential Property Renames
additionallyAllowedTenantsFormat Change- Managed Identity ID Consolidation
- Missing Credential Types in Old System
- Properties Settable in New System but Not in Old
- LoggedHeaderNames / LoggedQueryParameters Semantic Difference
- Connection String Bare-Value Magic
- Configuration Reference Syntax
- X509 Certificate Store Credentials Removed
- Service Version Handling
- Summary
- Feasibility Analysis: Accepting Old Config Format in New System
The old system uses two separate binding mechanisms on the same IConfiguration section:
-
Constructor parameters ΓÇö
ClientFactory.CreateClient()iterates client constructors via reflection and matches config keys to parameter names usingTryConvertArgument. -
Options properties ΓÇö
AzureClientBuilderExtensions.ConfigureOptions()callsconfiguration.Bind(options)(the standardMicrosoft.Extensions.Configuration.Binder) to set properties on the pre-instantiated options object.
Both operate on the same flat config section, so constructor params, credential keys, and options properties all live at the same configuration path with no structural separation between them.
Old JSON schema:
{
"MyClient": {
"uri": "https://myservice.azure.com",
"clientId": "00000000-0000-0000-0000-000000000000",
"clientSecret": "secret",
"tenantId": "00000000-0000-0000-0000-000000000000",
"Diagnostics": {
"ApplicationId": "my-app",
"IsLoggingEnabled": true,
"LoggedHeaderNames": [ "x-ms-request-id" ]
},
"Retry": {
"MaxRetries": 5,
"Mode": "Exponential"
}
}
}The new system uses a Settings POCO whose structure matches the constructor parameters exactly. Configuration hierarchy mirrors the object graph 1:1.
ClientOptions(IConfigurationSection section)ΓÇö readssection.GetSection("Diagnostics")andsection.GetSection("Retry")DiagnosticsOptions(IConfigurationSection section)ΓÇö reads each property by name from the sectionRetryOptions(IConfigurationSection section)ΓÇö reads each property by name from the section- Configuration and DI documentation ΓÇö shows the full JSON schema
New JSON schema:
{
"MyClient": {
"Endpoint": "https://myservice.azure.com",
"Credential": {
"CredentialSource": "AzureCliCredential"
},
"Options": {
"Diagnostics": {
"ApplicationId": "my-app",
"IsLoggingEnabled": true,
"LoggedHeaderNames": [ "x-ms-request-id" ]
},
"Retry": {
"MaxRetries": 5,
"Mode": "Exponential"
}
}
}
}Finding: In the old system, the Options layer in the object hierarchy is
eliminated. Properties like Retry and Diagnostics sit at the same config level
as constructor parameters. In the new system, they are nested under Options: which
matches the actual constructor signature ΓÇö the client constructor takes an options
parameter, and Retry and Diagnostics are properties on that options object.
AzureClientBuilderExtensions.cs:86 ΓÇö the config section is bound
directly to the options instance, so Diagnostics:ApplicationId sits at the top
level with no Options: prefix
(test example).
Old schema:
{
"Retry": {
"MaxRetries": 3
},
"Diagnostics": {
"ApplicationId": "my-app"
}
}ClientOptions.cs:89-90 ΓÇö reads from nested sections matching the object
hierarchy. New config docs (lines 179-205) show the schema:
{
"Options": {
"Retry": {
"MaxRetries": 3
},
"Diagnostics": {
"ApplicationId": "my-app"
}
}
}| Old Config Path | New Config Path | Object Path |
|---|---|---|
Retry:MaxRetries |
Options:Retry:MaxRetries |
settings.Options.Retry.MaxRetries |
Retry:Mode |
Options:Retry:Mode |
settings.Options.Retry.Mode |
Diagnostics:ApplicationId |
Options:Diagnostics:ApplicationId |
settings.Options.Diagnostics.ApplicationId |
Diagnostics:IsLoggingEnabled |
Options:Diagnostics:IsLoggingEnabled |
settings.Options.Diagnostics.IsLoggingEnabled |
Finding: The old system reads all credential properties as flat camelCase keys
from the same config section used for constructor params and options. The new system
nests them under a Credential section with PascalCase names and an explicit
CredentialSource discriminator.
ClientFactory.cs:95-111 ΓÇö reads 15 flat camelCase keys from the
config section. Credential keys like clientId, tenantId, and clientSecret sit at
the same level as constructor params like uri
(test example).
Old schema:
{
"uri": "https://myservice.azure.com",
"clientId": "00000000-0000-0000-0000-000000000000",
"clientSecret": "secret",
"tenantId": "00000000-0000-0000-0000-000000000000"
}The credential type is determinedimplicitly by which keys are present. For
example, tenantId + clientId + clientSecret → ClientSecretCredential
(ClientFactory.cs:239-252).
Config docs (lines 228-233) ΓÇö nested section with explicit type:
{
"Credential": {
"CredentialSource": "AzureCliCredential",
"TenantId": "00000000-0000-0000-0000-000000000000",
"AdditionallyAllowedTenants": [ "*" ]
}
}| Aspect | Old | New |
|---|---|---|
| Nesting | Flat at root | Nested under Credential: |
| Type selection | Implicit (which keys present) | Explicit CredentialSource discriminator |
| Key collision | Credential keys share config path with ctor params | Separate Credential section |
Note: Configuration keys are case-insensitive in
Microsoft.Extensions.Configuration, soclientIdandClientIdresolve identically. Casing differences between the two systems are cosmetic, not functional.
Finding: Several credential config keys were genuinely renamed (not just casing ΓÇö
config keys are case-insensitive, so clientId and ClientId are the same key).
Only actual name changes are listed here.
| Old Config Key | Old Source | New Config Key | New Source | Notes |
|---|---|---|---|---|
credential |
ClientFactory.cs:97 |
CredentialSource |
ConfigDI.md:230 | Different word |
serviceConnectionId |
ClientFactory.cs:100 |
AzurePipelinesServiceConnectionId |
ConfigDI.md:279 | Prefixed with AzurePipelines |
systemAccessToken |
ClientFactory.cs:107 |
AzurePipelinesSystemAccessToken |
ConfigDI.md:280 | Prefixed with AzurePipelines |
managedIdentityResourceId |
ClientFactory.cs:101 |
ManagedIdentityId + ManagedIdentityIdKind: ResourceId |
ConfigDI.md:357-359 | Consolidated (see §6) |
managedIdentityObjectId |
ClientFactory.cs:102 |
ManagedIdentityId + ManagedIdentityIdKind: ObjectId |
ConfigDI.md:357-359 | Consolidated (see §6) |
managedIdentityClientId |
ClientFactory.cs:111 |
ManagedIdentityId + ManagedIdentityIdKind: ClientId |
ConfigDI.md:357-359 | Consolidated (see §6) |
clientSecret |
ClientFactory.cs:103 |
Credential:ClientSecret |
PR #56721 | When CredentialSource=EnvironmentCredential; config takes priority over AZURE_CLIENT_SECRET env var |
tokenFilePath |
ClientFactory.cs:109 |
Credential:TokenFilePath |
PR #56721 | When CredentialSource=WorkloadIdentityCredential; config takes priority over AZURE_FEDERATED_TOKEN_FILE env var |
clientCertificate |
ClientFactory.cs:104 |
(removed) | — | See §13 |
clientCertificateStoreName |
ClientFactory.cs:105 |
(removed) | — | See §13 |
clientCertificateStoreLocation |
ClientFactory.cs:106 |
(removed) | — | See §13 |
Keys that are unchanged (case-insensitive equivalent):
clientId/ClientId, tenantId/TenantId, azureCloud/AzureCloud,
additionallyAllowedTenants/AdditionallyAllowedTenants (though format changed — see §5).
Note (post-fix):
ClientIdwas always read from config in theDefaultAzureCredentialOptionsconstructor, but prior to PR #56721 it was not propagated toEnvironmentCredentialOptionsbecauseClone()only copied DAC-specific properties. This is now fixed ΓÇöClientIdflows correctly toEnvironmentCredentialfrom config.
Finding: The old system encodes a list as a semicolon-delimited string. The new
system uses a proper JSON array. The object is IList<string> in both cases, so the
new format matches the object structure; the old does not.
ClientFactory.cs:108,115-121 ΓÇö reads as a semicolon-delimited string
and manually splits it (test example).
Old schema:
{
"additionallyAllowedTenants": "tenant1;tenant2;tenant3"
}Config docs (line 231):
{
"AdditionallyAllowedTenants": [ "tenant1", "tenant2", "tenant3" ]
}Finding: The old system uses three separate config keys to identify a managed
identity. The new system uses two fields (ManagedIdentityId +
ManagedIdentityIdKind).
ClientFactory.cs:101-102,111 reads three separate keys
(managedIdentityResourceId, managedIdentityObjectId, managedIdentityClientId)
and uses a chain of if/else to determine which factory method to call
(ClientFactory.cs:123-147,
test example).
Old schema:
{
"credential": "managedidentity",
"managedIdentityResourceId": "00000000-0000-0000-0000-000000000000"
}Config docs (lines 353-364) ΓÇö unified fields:
{
"Credential": {
"CredentialSource": "ManagedIdentityCredential",
"ManagedIdentityIdKind": "ResourceId",
"ManagedIdentityId": "00000000-0000-0000-0000-000000000000"
}
}ManagedIdentityIdKind can be SystemAssigned, ClientId, ResourceId, or ObjectId.
Finding: The old system supports 7 credential types (some implicit). The new
system supports 14+ with explicit CredentialSource values.
ClientFactory.cs:123-340 handles only:
| Credential | Old Discriminator | Old Code |
|---|---|---|
| ManagedIdentityCredential | "managedidentity" |
ClientFactory.cs:123 |
| WorkloadIdentityCredential | "workloadidentity" |
ClientFactory.cs:150 |
| ManagedFederatedIdentityCredential | "managedidentityasfederatedidentity" |
ClientFactory.cs:189 |
| AzurePipelinesCredential | "azurepipelines" |
ClientFactory.cs:216 |
| ClientSecretCredential | (implicit: tenantId+clientId+clientSecret) | ClientFactory.cs:239 |
| ClientCertificateCredential | (implicit: tenantId+clientId+clientCertificate) | ClientFactory.cs:255 |
| DefaultAzureCredential | (fallback) | ClientFactory.cs:301 |
All documented in ConfigurationAndDependencyInjection.md:
| Credential | New Doc Location |
|---|---|
| AzureCliCredential | lines 249-258 |
| AzureDeveloperCliCredential | lines 261-269 |
| AzurePowerShellCredential | lines 290-298 |
| BrokerCredential | lines 301-316 |
| EnvironmentCredential | lines 319-327 |
| InteractiveBrowserCredential | lines 330-350 |
| VisualStudioCodeCredential | lines 387-394 |
| VisualStudioCredential | lines 397-405 |
| ApiKeyCredential | lines 238-246 |
Update (post-PR #56721): The EnvironmentCredential and WorkloadIdentityCredential schemas below now include properties that were previously only settable via environment variables. Config values take priority over env vars.
EnvironmentCredential full schema (post-fix):
{
"Credential": {
"CredentialSource": "EnvironmentCredential",
"TenantId": "00000000-0000-0000-0000-000000000000",
"ClientId": "00000000-0000-0000-0000-000000000000",
"ClientSecret": "...",
"ClientCertificatePath": "/path/to/cert.pem",
"ClientCertificatePassword": "...",
"SendCertificateChain": false,
"Username": "...",
"Password": "...",
"DisableInstanceDiscovery": false
}
}WorkloadIdentityCredential full schema (post-fix):
{
"Credential": {
"CredentialSource": "WorkloadIdentityCredential",
"TenantId": "00000000-0000-0000-0000-000000000000",
"ClientId": "00000000-0000-0000-0000-000000000000",
"TokenFilePath": "/path/to/token",
"IsAzureProxyEnabled": false,
"DisableInstanceDiscovery": false
}
}Finding: The new system supports significantly more configurable properties than the old system. This section catalogs every property that is settable via configuration in the new system but has no equivalent in the old.
The old ClientFactory.CreateCredential hard-codes a small set of config keys and
passes them to credential constructors. It never sets option properties like
DisableInstanceDiscovery or TokenCachePersistenceOptions on the credential
options objects it creates.
| Property | Credential Types | New Doc Location |
|---|---|---|
DisableInstanceDiscovery |
AzurePipelines, Broker, Environment, InteractiveBrowser, ManagedFederated, WorkloadIdentity | ConfigDI.md various |
TokenCachePersistenceOptions (nested: Name, UnsafeAllowUnencryptedStorage) |
AzurePipelines, Broker, InteractiveBrowser, ManagedFederated | ConfigDI.md:282-285 |
IsAzureProxyEnabled |
WorkloadIdentity | ConfigDI.md:415 |
ClientSecret |
EnvironmentCredential | PR #56721 ΓÇö config overrides AZURE_CLIENT_SECRET env var |
ClientCertificatePath |
EnvironmentCredential | PR #56721 ΓÇö config overrides AZURE_CLIENT_CERTIFICATE_PATH env var |
ClientCertificatePassword |
EnvironmentCredential | PR #56721 ΓÇö config overrides AZURE_CLIENT_CERTIFICATE_PASSWORD env var |
SendCertificateChain |
EnvironmentCredential | PR #56721 ΓÇö config overrides AZURE_CLIENT_SEND_CERTIFICATE_CHAIN env var |
Username |
EnvironmentCredential | PR #56721 ΓÇö config overrides AZURE_USERNAME env var |
Password |
EnvironmentCredential | PR #56721 ΓÇö config overrides AZURE_PASSWORD env var |
TokenFilePath |
WorkloadIdentityCredential | PR #56721 ΓÇö config overrides AZURE_FEDERATED_TOKEN_FILE env var |
For comparison, the old system's only configurable credential sub-property is
AdditionallyAllowedTenants (and even that uses a non-standard semicolon format — see §5).
Note: The
ClientSecret,ClientCertificatePath,ClientCertificatePassword,SendCertificateChain,Username, andPasswordproperties on EnvironmentCredential andTokenFilePathon WorkloadIdentityCredential were added in PR #56721 (fixing #56715). Prior to this fix, these properties defaulted to environment variables with no config override.
These credential types don't exist in the old system at all (see §7), so all of their properties are new:
| Property | Credential Type | New Doc Location |
|---|---|---|
Subscription |
AzureCliCredential | ConfigDI.md:255 |
CredentialProcessTimeout |
AzureCli, AzureDeveloperCli, AzurePowerShell, VisualStudio | ConfigDI.md:256 |
UseDefaultBrokerAccount |
BrokerCredential | ConfigDI.md:307 |
IsLegacyMsaPassthroughEnabled |
BrokerCredential | ConfigDI.md:308 |
DisableAutomaticAuthentication |
Broker, InteractiveBrowser | ConfigDI.md:310 |
LoginHint |
InteractiveBrowserCredential | ConfigDI.md:337 |
RedirectUri |
InteractiveBrowserCredential | ConfigDI.md:338 |
BrowserCustomization (nested: SuccessMessage, ErrorMessage) |
InteractiveBrowserCredential | ConfigDI.md:345-348 |
Key (via env var) |
ApiKeyCredential | ConfigDI.md:238-246 |
Config docs (lines 223-225): "Credential option classes also inherit
from ClientOptions, so the Diagnostics and Retry sections shown in Common Client
Configuration are available within the Credential section as well."
This means the new system can configure HTTP retry and logging behavior for credential requests independently from the main client. The old system has no equivalent ΓÇö credential options objects are created with hardcoded defaults.
New schema:
{
"Credential": {
"CredentialSource": "AzurePipelinesCredential",
"TenantId": "00000000-0000-0000-0000-000000000000",
"ClientId": "00000000-0000-0000-0000-000000000000",
"Retry": {
"MaxRetries": 5
},
"Diagnostics": {
"IsLoggingEnabled": true
}
}
}Finding: The old system appends configured values to the default header list. The new system replaces defaults entirely when the config section exists.
[DiagnosticsOptions.cs:194][diag-props] ΓÇö property has internal set:
The standard configuration.Bind() sees a public getter returning IList<string> but
no public setter. It retrieves the existing list (which contains
22 default headers) and adds items to it. There is no way to
remove or replace defaults.
Old schema:
{
"Diagnostics": {
"LoggedHeaderNames": [ "X-Custom" ]
}
}Result: list contains 22 defaults plus X-Custom (23 total).
DiagnosticsOptions.cs:53-61 ΓÇö creates a new list when the
config section exists, replacing defaults entirely. If the section is absent, defaults
are used. Same pattern for LoggedQueryParameters at
DiagnosticsOptions.cs:66-78.
New schema:
{
"Options": {
"Diagnostics": {
"LoggedHeaderNames": [ "X-Custom" ]
}
}
}Result: list contains only X-Custom (1 total).
| Scenario | Old Behavior | New Behavior |
|---|---|---|
Config specifies LoggedHeaderNames: ["X-Custom"] |
List = 22 defaults + X-Custom (23 total) |
List = ["X-Custom"] only |
Config omits LoggedHeaderNames |
List = 22 defaults | List = 22 defaults |
Finding: When the entire config section is a bare string value (not a nested
object), the old system silently interprets it as a connection string. This only works
for clients with a constructor parameter literally named connectionString.
ClientFactory.cs:32-43 ΓÇö when the config section is a bare string
value, it rewrites it to { "connectionString": "<value>" } where
ConnectionStringParameterName is hardcoded to "connectionString"
(line 21, test example).
Old schema (bare-value shorthand):
{
"MyClient": "Endpoint=sb://mybus.servicebus.windows.net/;SharedAccessKeyName=key;SharedAccessKey=value"
}This is silently rewritten internally to:
{
"MyClient": {
"connectionString": "Endpoint=sb://mybus.servicebus.windows.net/;SharedAccessKeyName=key;SharedAccessKey=value"
}
}This shorthand works for clients whose constructors have a connectionString parameter:
| Client | Constructor | Bare-String Works? |
|---|---|---|
BlobServiceClient |
(string connectionString, BlobClientOptions options) |
✅ |
ServiceBusClient |
(string connectionString, ServiceBusClientOptions options) |
✅ |
EventHubProducerClient |
(string connectionString, ...) |
✅ |
But fails silently for clients with differently-named string parameters:
| Client | Constructor | Bare-String Works? |
|---|---|---|
KeyClient |
(Uri vaultUri, TokenCredential, KeyClientOptions) |
❎ (Uri, not string) |
Hypothetical ChatClient |
(string modelId, ChatClientOptions) |
❎ (param name modelId ≠ connectionString) |
The bare string "gpt-4" would be rewritten to { "connectionString": "gpt-4" },
then constructor matching would look for a parameter named connectionString,
fail to find one, and throw InvalidOperationException.
No bare-value magic. Endpoint is always an explicit config key:
{
"MyClient": {
"Endpoint": "https://myservice.azure.com"
}
}Finding: The new system supports a $-prefixed reference syntax for sharing
credential configuration across multiple clients. No equivalent exists in the old system.
Config docs (lines 421-451):
{
"Shared": {
"Credential": {
"CredentialSource": "AzureCli"
}
},
"Client1": {
"Endpoint": "https://service1.azure.com",
"Credential": "$Shared:Credential"
},
"Client2": {
"Endpoint": "https://service2.azure.com",
"Credential": "$Shared:Credential"
}
}No equivalent. Each client registration must independently configure credentials
via flat keys or a shared ConfigureDefaults(configuration) call
(AzureClientFactoryBuilder.cs:83-95).
Finding: The old system supports loading client certificates from the Windows certificate store by thumbprint. This feature has no equivalent in the new system.
ClientFactory.cs:255-293 ΓÇö loads certs from X509Store by thumbprint.
Config keys involved: clientCertificate (thumbprint), clientCertificateStoreName
(default: "MY"), clientCertificateStoreLocation (CurrentUser or LocalMachine)
(test example).
Old schema:
{
"clientId": "00000000-0000-0000-0000-000000000000",
"tenantId": "00000000-0000-0000-0000-000000000000",
"clientCertificate": "<thumbprint>",
"clientCertificateStoreName": "MY",
"clientCertificateStoreLocation": "CurrentUser"
}No X509 store-based credential config. Certificate-based auth may still be available programmatically, but there is no config-driven equivalent.
Finding: The old system handles service version via reflection + a fluent API. The new system treats it as a regular configuration property.
ClientFactory.CreateClientOptions() ΓÇö uses reflection to find a
ServiceVersion enum parameter on the options constructor. Set via the fluent API
WithVersion<TVersion>(). Not configurable via JSON ΓÇö must be set
in code.
Old schema: not supported ΓÇö version is set only via fluent code, not config:
builder.AddBlobServiceClient(configuration)
.WithVersion(BlobClientOptions.ServiceVersion.V2019_02_02);Service version is a regular property on the Settings/Options class, bindable from config like any other property.
New schema:
{
"Options": {
"ServiceVersion": "V2024_08_04"
}
}| # | Category | Description | Old Source | New Source |
|---|---|---|---|---|
| 2 | Restructured | Options layer flattened (no Options: prefix) |
AzureClientBuilderExtensions.cs:86 |
ClientOptions.cs:89-90 |
| 3 | Restructured | Credentials flat → nested Credential: section |
ClientFactory.cs:97-111 |
ConfigDI.md:228-233 |
| 4 | Renamed | Credential keys renamed (case-insensitive; only actual name changes) | ClientFactory.cs:97-111 |
ConfigDI.md |
| 5 | Format change | additionallyAllowedTenants: semicolons → JSON array |
ClientFactory.cs:115-121 |
ConfigDI.md:231 |
| 6 | Restructured | 3 managed identity keys → unified ManagedIdentityId + Kind |
ClientFactory.cs:101-147 |
ConfigDI.md:353-364 |
| 7 | Missing in old | 9 credential types not supported | ClientFactory.cs:123-340 |
ConfigDI.md:236-419 |
| 8 | Missing in old | Many properties not configurable (credential sub-properties incl. EnvironmentCredential/WorkloadIdentityCredential properties added in PR #56721, credential-level Retry/Diagnostics, ServiceVersion, $ references) |
ClientFactory.cs:95-111 |
ConfigDI.md |
| 9 | Semantic | LoggedHeaderNames: append vs replace | [DiagnosticsOptions.cs:194][diag-props] |
DiagnosticsOptions.cs:53-61 |
| 10 | Removed | Bare-string → connectionString magic |
ClientFactory.cs:32-43 |
ΓÇö |
| 11 | Added | $ config reference syntax for sharing credentials |
ΓÇö | ConfigDI.md:421-451 |
| 12 | Removed | X509 certificate store credentials | ClientFactory.cs:255-293 |
ΓÇö |
| 13 | Changed | Service version: reflection → config property | ClientFactory.cs:342-405 |
Config property |
This section evaluates whether the new config system could accept configuration written in
the old Microsoft.Extensions.Azure format to simplify migration.
Feasibility: MEDIUM
The new system's ClientOptions(IConfigurationSection) receives the Options sub-section
and calls section.GetSection("Retry"). To accept the old format, the system would need
to also look for Retry at the parent section level (the client root) when the
Options section doesn't contain it.
var retrySection = section.GetSection("Retry");
if (!retrySection.Exists())
retrySection = parentSection?.GetSection("Retry"); // old format fallbackComplication: The Settings POCO currently mirrors the constructor params. Retry/Diagnostics
at the flat level collide with constructor params. The old system uses configuration.Bind()
which recursively matches by property name. The new system reads explicitly by section name,
so it would need to know about the parent section.
Migration benefit: HIGH ΓÇö this is the most visible structural difference users encounter.
Feasibility: MEDIUM-HARD
This is the most complex difference. The old format has credential keys mixed with constructor params and options at the same level. To accept old format:
- Check if
Credentialsection exists → use new format - If not, check for flat credential keys (
clientId,tenantId,clientSecret, etc.) → construct credential as old system did
Complication: The old system uses implicit type detection (which keys are present →
which credential type). The new system uses explicit CredentialSource. Supporting implicit
detection means re-implementing the old ClientFactory.CreateCredential() logic.
Migration benefit: HIGH ΓÇö credential config is the second most visible difference.
| Old Key | New Key |
|---|---|
credential |
CredentialSource |
serviceConnectionId |
AzurePipelinesServiceConnectionId |
systemAccessToken |
AzurePipelinesSystemAccessToken |
managedIdentityResourceId |
ManagedIdentityId + ManagedIdentityIdKind |
managedIdentityObjectId |
ManagedIdentityId + ManagedIdentityIdKind |
managedIdentityClientId |
ManagedIdentityId + ManagedIdentityIdKind |
clientSecret |
Credential:ClientSecret (added in PR #56721) |
tokenFilePath |
Credential:TokenFilePath (added in PR #56721) |
Feasibility: EASY
Just check for old key names as fallbacks:
var credSource = section["CredentialSource"] ?? section["credential"];Update:
clientSecretandtokenFilePathnow have direct config equivalents (added in PR #56721), reducing the migration gap for these two keys. OnlyclientCertificate,clientCertificateStoreName, andclientCertificateStoreLocation(X509 store — see §12) remain without equivalents.
Migration benefit: MEDIUM ΓÇö only affects users of these specific credential types.
Feasibility: EASY
Check if the value is a bare string; if so, split by semicolons:
if (section[key] is string value && !section.GetSection(key).GetChildren().Any())
return value.Split(';').ToList();
else
return section.GetSection(key).GetChildren().Select(c => c.Value).ToList();Migration benefit: LOW ΓÇö minor convenience.
Feasibility: EASY
Map old keys to new unified format:
if (section["managedIdentityResourceId"] is string resId)
{ ManagedIdentityId = resId; ManagedIdentityIdKind = "ResourceId"; }
else if (section["managedIdentityObjectId"] is string objId)
{ ManagedIdentityId = objId; ManagedIdentityIdKind = "ObjectId"; }
// etc.Migration benefit: MEDIUM ΓÇö affects managed identity users.
Feasibility: NOT RECOMMENDED
This is a semantic difference, not a structural one. Making the new system append would:
- Change behavior for all new users who expect replace semantics
- Make it impossible to have a clean "only log these headers" config
- Require a heuristic or flag to distinguish "user wants append" vs "user wants replace"
Recommendation: Document as a known behavioral change. Users who relied on append behavior need to include the default headers they want in their config.
Feasibility: EASY but NOT RECOMMENDED
Could detect bare string and rewrite, but this magic was problematic in the old system
(fails silently for non-connectionString clients). Better to require explicit keys.
Feasibility: MEDIUM-HARD
Would need to detect old cert-store keys, load cert from X509Store, and create
ClientCertificateCredential with the loaded cert. Adds Windows-specific logic to the
new cross-platform system.
Recommendation: Consider adding as a proper CredentialSource if demand exists,
rather than a backward-compat shim.
If the new system accepted old format, migrating would become:
- Code changes only ΓÇö swap
AddAzureClients()registration for new DI pattern - Config stays unchanged ΓÇö no need to restructure JSON/appsettings files across environments
- Gradual config migration ΓÇö update config files at your own pace per-environment
- Reduced deployment risk ΓÇö config changes are often the riskiest part of migration (they vary per environment: dev/staging/prod/sovereign clouds)
| Difference | Without compat | With compat |
|---|---|---|
| Options nesting (§2) | Must add Options: wrapper in every config file |
No change needed |
| Credential nesting (§3) | Must restructure flat keys into Credential: section |
No change needed |
| Property renames (§4) | Must rename keys | No change needed |
| Semicolon lists (§5) | Must convert to JSON arrays | No change needed |
| MI consolidation (§6) | Must restructure 3 keys → 2 | No change needed |
| Downside | Severity | Mitigable? |
|---|---|---|
Ambiguity when both formats present ΓÇö if config has both clientId (flat) AND Credential:ClientId (nested), which wins? |
HIGH | Partially ΓÇö document "new wins" rule |
Implicit credential detection is fragile ΓÇö re-introduces the design flaw CredentialSource was built to fix; adding new keys could change which credential type is selected |
HIGH | No ΓÇö fundamental design issue |
| Testing surface doubles ΓÇö every config-reading path needs tests for both old and new format plus edge cases | MEDIUM | Yes ΓÇö just more tests |
| Documentation confusion ΓÇö two valid formats for the same thing; users see conflicting examples | MEDIUM | Partially ΓÇö clear "legacy compat" labeling |
| Perpetual maintenance burden ΓÇö old format support becomes permanent tech debt that's very hard to deprecate | HIGH | No ΓÇö permanent cost |
| LoggedHeaderNames semantic gap ΓÇö append-vs-replace can't be bridged by format detection alone | LOW | Document as known change |
Partial compatibility is the sweet spot. Full old-format support re-introduces the design problems the new system was built to fix. Targeted compatibility for the highest-friction differences would significantly ease migration:
- §4 Property renames — Accept old key names as aliases (easy, no ambiguity)
- §5 Semicolon lists — Accept semicolon-delimited strings as well as JSON arrays (easy, unambiguous)
- §6 MI consolidation — Accept old 3-key format (easy, unambiguous mapping)
- §2 Options flattening — Accept Retry/Diagnostics at root level (medium complexity, but the #1 migration pain point)
- §3 Credential nesting — Accept flat credential keys when
Credential:section absent (medium complexity, #2 pain point)
- §9 LoggedHeaderNames semantic — Can't bridge; document as behavioral change
- §10 Connection string magic — Fragile design; don't perpetuate
- §12 X509 cert store — Add as proper
CredentialSourceif needed, not as compat shim