Skip to content

Instantly share code, notes, and snippets.

@m-nash
Last active March 4, 2026 21:56
Show Gist options
  • Select an option

  • Save m-nash/07d69489000bd50bf4af61c4744a9a45 to your computer and use it in GitHub Desktop.

Select an option

Save m-nash/07d69489000bd50bf4af61c4744a9a45 to your computer and use it in GitHub Desktop.
Configuration Schema Differences: Microsoft.Extensions.Azure (Old) vs New Config System

Configuration Schema Differences: Microsoft.Extensions.Azure (Old) vs New Config System

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 7c1e032 on the main branch.


Table of Contents

  1. Background: How Each System Works
  2. Options Layer Flattening
  3. Credential Configuration Restructured
  4. Credential Property Renames
  5. additionallyAllowedTenants Format Change
  6. Managed Identity ID Consolidation
  7. Missing Credential Types in Old System
  8. Properties Settable in New System but Not in Old
  9. LoggedHeaderNames / LoggedQueryParameters Semantic Difference
  10. Connection String Bare-Value Magic
  11. Configuration Reference Syntax
  12. X509 Certificate Store Credentials Removed
  13. Service Version Handling
  14. Summary
  15. Feasibility Analysis: Accepting Old Config Format in New System

1. Background: How Each System Works

Old System (Microsoft.Extensions.Azure)

The old system uses two separate binding mechanisms on the same IConfiguration section:

  1. Constructor parameters ΓÇö ClientFactory.CreateClient() iterates client constructors via reflection and matches config keys to parameter names using TryConvertArgument.

  2. Options properties ΓÇö AzureClientBuilderExtensions.ConfigureOptions() calls configuration.Bind(options) (the standard Microsoft.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"
    }
  }
}

New System (ClientOptions(IConfigurationSection, ...))

The new system uses a Settings POCO whose structure matches the constructor parameters exactly. Configuration hierarchy mirrors the object graph 1:1.

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"
      }
    }
  }
}

2. Options Layer Flattening

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.

Old system

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"
  }
}

New system

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"
    }
  }
}

Comparison

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

3. Credential Configuration Restructured

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.

Old system

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).

New system

Config docs (lines 228-233) ΓÇö nested section with explicit type:

{
  "Credential": {
    "CredentialSource": "AzureCliCredential",
    "TenantId": "00000000-0000-0000-0000-000000000000",
    "AdditionallyAllowedTenants": [ "*" ]
  }
}

Key differences

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, so clientId and ClientId resolve identically. Casing differences between the two systems are cosmetic, not functional.


4. Credential Property Renames

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): ClientId was always read from config in the DefaultAzureCredentialOptions constructor, but prior to PR #56721 it was not propagated to EnvironmentCredentialOptions because Clone() only copied DAC-specific properties. This is now fixed ΓÇö ClientId flows correctly to EnvironmentCredential from config.


5. additionallyAllowedTenants Format Change

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.

Old system

ClientFactory.cs:108,115-121 ΓÇö reads as a semicolon-delimited string and manually splits it (test example).

Old schema:

{
  "additionallyAllowedTenants": "tenant1;tenant2;tenant3"
}

New system

Config docs (line 231):

{
  "AdditionallyAllowedTenants": [ "tenant1", "tenant2", "tenant3" ]
}

6. Managed Identity ID Consolidation

Finding: The old system uses three separate config keys to identify a managed identity. The new system uses two fields (ManagedIdentityId + ManagedIdentityIdKind).

Old system

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"
}

New system

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.


7. Missing Credential Types in Old System

Finding: The old system supports 7 credential types (some implicit). The new system supports 14+ with explicit CredentialSource values.

Old system coverage

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

New credential types (not in old)

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
  }
}

8. Properties Settable in New System but Not in Old

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.

8a. Credential sub-properties (for credential types that exist in both systems)

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, and Password properties on EnvironmentCredential and TokenFilePath on WorkloadIdentityCredential were added in PR #56721 (fixing #56715). Prior to this fix, these properties defaulted to environment variables with no config override.

8b. Properties on credential types only in the new system

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

8c. Credential-level Diagnostics and Retry

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
    }
  }
}

9. LoggedHeaderNames / LoggedQueryParameters Semantic Difference

Finding: The old system appends configured values to the default header list. The new system replaces defaults entirely when the config section exists.

Old system behavior

[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).

New system behavior

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).

Impact

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

10. Connection String Bare-Value Magic

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.

Old system

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"
  }
}

Real-world impact

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.

New system

No bare-value magic. Endpoint is always an explicit config key:

{
  "MyClient": {
    "Endpoint": "https://myservice.azure.com"
  }
}

11. Configuration Reference Syntax

Finding: The new system supports a $-prefixed reference syntax for sharing credential configuration across multiple clients. No equivalent exists in the old system.

New 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"
  }
}

Old system

No equivalent. Each client registration must independently configure credentials via flat keys or a shared ConfigureDefaults(configuration) call (AzureClientFactoryBuilder.cs:83-95).


12. X509 Certificate Store Credentials Removed

Finding: The old system supports loading client certificates from the Windows certificate store by thumbprint. This feature has no equivalent in the new system.

Old 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"
}

New system

No X509 store-based credential config. Certificate-based auth may still be available programmatically, but there is no config-driven equivalent.


13. Service Version Handling

Finding: The old system handles service version via reflection + a fluent API. The new system treats it as a regular configuration property.

Old system

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);

New system

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"
  }
}

14. Summary

# 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

15. Feasibility Analysis: Accepting Old Config Format in New System

This section evaluates whether the new config system could accept configuration written in the old Microsoft.Extensions.Azure format to simplify migration.

Difference-by-Difference Assessment

§2 Options Layer Flattening — Retry:X vs Options:Retry:X

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 fallback

Complication: 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.


§3 Credential Restructured — flat vs nested Credential:

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:

  1. Check if Credential section exists → use new format
  2. 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.


§4 Credential Property Renames

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: clientSecret and tokenFilePath now have direct config equivalents (added in PR #56721), reducing the migration gap for these two keys. Only clientCertificate, clientCertificateStoreName, and clientCertificateStoreLocation (X509 store — see §12) remain without equivalents.

Migration benefit: MEDIUM ΓÇö only affects users of these specific credential types.


§5 additionallyAllowedTenants Format — semicolons vs JSON array

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.


§6 Managed Identity ID Consolidation — 3 keys vs unified

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.


§9 LoggedHeaderNames Semantic — append vs replace

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.


§10 Connection String Bare-Value Magic

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.


§12 X509 Certificate Store Credentials

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.


What Migration Gets Easier

If the new system accepted old format, migrating would become:

  1. Code changes only ΓÇö swap AddAzureClients() registration for new DI pattern
  2. Config stays unchanged ΓÇö no need to restructure JSON/appsettings files across environments
  3. Gradual config migration ΓÇö update config files at your own pace per-environment
  4. 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

What Would Be Worse / Downsides

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

Recommendation

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:

Recommended to support (high benefit, low cost/risk)

  • ┬º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)

Consider supporting (high benefit, moderate complexity)

  • ┬º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)

Do NOT support (low benefit or high risk)

  • ┬º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 CredentialSource if needed, not as compat shim
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment