From 61295ef1d9cd3dcef743a0c33650a528f002d4de Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Wed, 20 May 2026 15:46:36 +0800 Subject: [PATCH 01/12] resolve key vault references concurrently --- .../AzureAppConfigurationKeyVaultOptions.cs | 7 + .../AzureAppConfigurationOptions.cs | 6 + .../AzureAppConfigurationProvider.cs | 63 ++++-- .../AzureKeyVaultSecretProvider.cs | 68 ++++--- .../Unit/KeyVaultReferenceTests.cs | 183 ++++++++++++++++++ 5 files changed, 288 insertions(+), 39 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationKeyVaultOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationKeyVaultOptions.cs index cca16df80..70a4d75e5 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationKeyVaultOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationKeyVaultOptions.cs @@ -31,6 +31,13 @@ public class AzureAppConfigurationKeyVaultOptions internal TimeSpan? DefaultSecretRefreshInterval = null; internal bool IsKeyVaultRefreshConfigured = false; + /// + /// Specifies whether Key Vault references should be resolved in parallel. + /// Default value is false. Enabling this can reduce the time required to resolve Key Vault references + /// when many references are loaded from Azure App Configuration. + /// + public bool ParallelSecretResolutionEnabled { get; set; } + /// /// Sets the credentials used to authenticate to key vaults that have no registered . /// diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index e5b6e2585..8fd34d477 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -144,6 +144,11 @@ internal IEnumerable Adapters /// internal bool IsKeyVaultRefreshConfigured { get; private set; } = false; + /// + /// Flag to indicate whether Key Vault references should be resolved in parallel. + /// + internal bool IsParallelSecretResolutionEnabled { get; private set; } = false; + /// /// Indicates all feature flag features used by the application. /// @@ -520,6 +525,7 @@ public AzureAppConfigurationOptions ConfigureKeyVault(Action> PrepareData(Dictionary kvp in data) + bool parallelSecretResolution = _options.IsParallelSecretResolutionEnabled; + + if (parallelSecretResolution) { - IEnumerable> keyValuePairs = null; + // Dispatch adapter processing for all settings concurrently. Only Key Vault references + // perform network I/O during adapter processing; other adapters complete synchronously. + // Insertion order in 'data' is preserved when merging results so prefix-stripping and + // last-write-wins behavior remain unchanged. + var pendingTasks = new List>>>(data.Count); - if (_requestTracingEnabled && _requestTracingOptions != null) + foreach (KeyValuePair kvp in data) { - _requestTracingOptions.UpdateAiConfigurationTracing(kvp.Value.ContentType); + if (_requestTracingEnabled && _requestTracingOptions != null) + { + _requestTracingOptions.UpdateAiConfigurationTracing(kvp.Value.ContentType); + } + + pendingTasks.Add(ProcessAdapters(kvp.Value, cancellationToken)); } - keyValuePairs = await ProcessAdapters(kvp.Value, cancellationToken).ConfigureAwait(false); + IEnumerable>[] results = await Task.WhenAll(pendingTasks).ConfigureAwait(false); - foreach (KeyValuePair kv in keyValuePairs) + for (int i = 0; i < results.Length; i++) { - string key = kv.Key; + MergeIntoApplicationData(applicationData, results[i]); + } + } + else + { + foreach (KeyValuePair kvp in data) + { + IEnumerable> keyValuePairs = null; - foreach (string prefix in _options.KeyPrefixes) + if (_requestTracingEnabled && _requestTracingOptions != null) { - if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - key = key.Substring(prefix.Length); - break; - } + _requestTracingOptions.UpdateAiConfigurationTracing(kvp.Value.ContentType); } - applicationData[key] = kv.Value; + keyValuePairs = await ProcessAdapters(kvp.Value, cancellationToken).ConfigureAwait(false); + + MergeIntoApplicationData(applicationData, keyValuePairs); } } return applicationData; } + private void MergeIntoApplicationData(Dictionary applicationData, IEnumerable> keyValuePairs) + { + foreach (KeyValuePair kv in keyValuePairs) + { + string key = kv.Key; + + foreach (string prefix in _options.KeyPrefixes) + { + if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + key = key.Substring(prefix.Length); + break; + } + } + + applicationData[key] = kv.Value; + } + } + private async Task LoadAsync(bool ignoreFailures, CancellationToken cancellationToken) { var startupStopwatch = Stopwatch.StartNew(); diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs index 57505ff95..af03bcef9 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs @@ -16,6 +16,7 @@ internal class AzureKeyVaultSecretProvider private readonly AzureAppConfigurationKeyVaultOptions _keyVaultOptions; private readonly IDictionary _secretClients; private readonly Dictionary _cachedKeyVaultSecrets; + private readonly object _cacheLock = new object(); private Uri _nextRefreshSourceId; private DateTimeOffset? _nextRefreshTime; @@ -38,20 +39,25 @@ public AzureKeyVaultSecretProvider(AzureAppConfigurationKeyVaultOptions keyVault public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifier, string key, string label, Logger logger, CancellationToken cancellationToken) { string secretValue = null; + SecretClient client; - if (_cachedKeyVaultSecrets.TryGetValue(secretIdentifier.SourceId, out CachedKeyVaultSecret cachedSecret) && - (!cachedSecret.RefreshAt.HasValue || DateTimeOffset.UtcNow < cachedSecret.RefreshAt.Value)) + lock (_cacheLock) { - return cachedSecret.SecretValue; - } + if (_cachedKeyVaultSecrets.TryGetValue(secretIdentifier.SourceId, out CachedKeyVaultSecret cachedHit) && + (!cachedHit.RefreshAt.HasValue || DateTimeOffset.UtcNow < cachedHit.RefreshAt.Value)) + { + return cachedHit.SecretValue; + } - SecretClient client = GetSecretClient(secretIdentifier.SourceId); + client = GetSecretClient(secretIdentifier.SourceId); + } if (client == null && _keyVaultOptions.SecretResolver == null) { throw new UnauthorizedAccessException("No key vault credential or secret resolver callback configured, and no matching secret client could be found."); } + CachedKeyVaultSecret cachedSecret = null; bool success = false; try @@ -73,7 +79,10 @@ public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifi } finally { - SetSecretInCache(secretIdentifier.SourceId, key, cachedSecret, success); + lock (_cacheLock) + { + SetSecretInCache(secretIdentifier.SourceId, key, cachedSecret, success); + } } return secretValue; @@ -81,41 +90,50 @@ public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifi public bool ShouldRefreshKeyVaultSecrets() { - return _nextRefreshTime.HasValue && _nextRefreshTime.Value < DateTimeOffset.UtcNow; + lock (_cacheLock) + { + return _nextRefreshTime.HasValue && _nextRefreshTime.Value < DateTimeOffset.UtcNow; + } } public void ClearCache() { - var sourceIdsToRemove = new List(); + lock (_cacheLock) + { + var sourceIdsToRemove = new List(); - var utcNow = DateTimeOffset.UtcNow; + var utcNow = DateTimeOffset.UtcNow; - foreach (KeyValuePair secret in _cachedKeyVaultSecrets) - { - if (secret.Value.LastRefreshTime + RefreshConstants.MinimumSecretRefreshInterval < utcNow) + foreach (KeyValuePair secret in _cachedKeyVaultSecrets) { - sourceIdsToRemove.Add(secret.Key); + if (secret.Value.LastRefreshTime + RefreshConstants.MinimumSecretRefreshInterval < utcNow) + { + sourceIdsToRemove.Add(secret.Key); + } } - } - foreach (Uri sourceId in sourceIdsToRemove) - { - _cachedKeyVaultSecrets.Remove(sourceId); - } + foreach (Uri sourceId in sourceIdsToRemove) + { + _cachedKeyVaultSecrets.Remove(sourceId); + } - if (_cachedKeyVaultSecrets.Any()) - { - UpdateNextRefreshableSecretFromCache(); + if (_cachedKeyVaultSecrets.Any()) + { + UpdateNextRefreshableSecretFromCache(); + } } } public void RemoveSecretFromCache(Uri sourceId) { - _cachedKeyVaultSecrets.Remove(sourceId); - - if (sourceId == _nextRefreshSourceId) + lock (_cacheLock) { - UpdateNextRefreshableSecretFromCache(); + _cachedKeyVaultSecrets.Remove(sourceId); + + if (sourceId == _nextRefreshSourceId) + { + UpdateNextRefreshableSecretFromCache(); + } } } diff --git a/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs b/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs index 3e856a1b0..f1214f456 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs @@ -1050,5 +1050,188 @@ MockAsyncPageable GetTestKeys(SettingSelector selector, CancellationToken ct) Assert.Equal(_secretValue, config[setting.Key]); } } + + [Fact] + public void ParallelSecretResolution_ResolvesAllReferences() + { + // Build a collection of distinct Key Vault references. + const int referenceCount = 20; + var settings = new List(); + + for (int i = 0; i < referenceCount; i++) + { + settings.Add(ConfigurationModelFactory.ConfigurationSetting( + key: $"Key{i}", + value: $@"{{""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/Secret{i}""}}", + eTag: new ETag($"etag-{i}"), + contentType: KeyVaultConstants.ContentType + "; charset=utf-8")); + } + + var mockClient = new Mock(MockBehavior.Strict); + mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(new MockAsyncPageable(settings)); + + var mockSecretClient = new Mock(MockBehavior.Strict); + mockSecretClient.SetupGet(client => client.VaultUri).Returns(new Uri("https://keyvault-theclassics.vault.azure.net")); + mockSecretClient.Setup(client => client.GetSecretAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((string name, string version, CancellationToken cancellationToken) => + Task.FromResult((Response)new MockResponse(new KeyVaultSecret(name, $"value-of-{name}")))); + + var configuration = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object); + options.ConfigureKeyVault(kv => + { + kv.Register(mockSecretClient.Object); + kv.ParallelSecretResolutionEnabled = true; + }); + }) + .Build(); + + for (int i = 0; i < referenceCount; i++) + { + Assert.Equal($"value-of-Secret{i}", configuration[$"Key{i}"]); + } + } + + [Fact] + public void ParallelSecretResolution_RunsConcurrently() + { + // Use a gated mock secret client to detect concurrent in-flight calls. + const int referenceCount = 10; + var settings = new List(); + + for (int i = 0; i < referenceCount; i++) + { + settings.Add(ConfigurationModelFactory.ConfigurationSetting( + key: $"Key{i}", + value: $@"{{""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/Secret{i}""}}", + eTag: new ETag($"etag-{i}"), + contentType: KeyVaultConstants.ContentType + "; charset=utf-8")); + } + + int inFlight = 0; + int maxInFlight = 0; + var inFlightLock = new object(); + + var mockClient = new Mock(MockBehavior.Strict); + mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(new MockAsyncPageable(settings)); + + var mockSecretClient = new Mock(MockBehavior.Strict); + mockSecretClient.SetupGet(client => client.VaultUri).Returns(new Uri("https://keyvault-theclassics.vault.azure.net")); + mockSecretClient.Setup(client => client.GetSecretAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (string name, string version, CancellationToken cancellationToken) => + { + lock (inFlightLock) + { + inFlight++; + if (inFlight > maxInFlight) + { + maxInFlight = inFlight; + } + } + + try + { + await Task.Delay(50, cancellationToken).ConfigureAwait(false); + } + finally + { + lock (inFlightLock) + { + inFlight--; + } + } + + return (Response)new MockResponse(new KeyVaultSecret(name, $"value-of-{name}")); + }); + + var configuration = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object); + options.ConfigureKeyVault(kv => + { + kv.Register(mockSecretClient.Object); + kv.ParallelSecretResolutionEnabled = true; + }); + }) + .Build(); + + // Verify all references resolved. + for (int i = 0; i < referenceCount; i++) + { + Assert.Equal($"value-of-Secret{i}", configuration[$"Key{i}"]); + } + + // When run in parallel, more than one secret request must have been in flight at the same time. + Assert.True(maxInFlight > 1, $"Expected concurrent Key Vault requests, but observed max in-flight = {maxInFlight}."); + } + + [Fact] + public void ParallelSecretResolution_DisabledByDefault_RunsSequentially() + { + const int referenceCount = 5; + var settings = new List(); + + for (int i = 0; i < referenceCount; i++) + { + settings.Add(ConfigurationModelFactory.ConfigurationSetting( + key: $"Key{i}", + value: $@"{{""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/Secret{i}""}}", + eTag: new ETag($"etag-{i}"), + contentType: KeyVaultConstants.ContentType + "; charset=utf-8")); + } + + int inFlight = 0; + int maxInFlight = 0; + var inFlightLock = new object(); + + var mockClient = new Mock(MockBehavior.Strict); + mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(new MockAsyncPageable(settings)); + + var mockSecretClient = new Mock(MockBehavior.Strict); + mockSecretClient.SetupGet(client => client.VaultUri).Returns(new Uri("https://keyvault-theclassics.vault.azure.net")); + mockSecretClient.Setup(client => client.GetSecretAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(async (string name, string version, CancellationToken cancellationToken) => + { + lock (inFlightLock) + { + inFlight++; + if (inFlight > maxInFlight) + { + maxInFlight = inFlight; + } + } + + try + { + await Task.Delay(20, cancellationToken).ConfigureAwait(false); + } + finally + { + lock (inFlightLock) + { + inFlight--; + } + } + + return (Response)new MockResponse(new KeyVaultSecret(name, $"value-of-{name}")); + }); + + new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object); + options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object)); + }) + .Build(); + + // Default (sequential) path should never have more than one in-flight Key Vault request. + Assert.Equal(1, maxInFlight); + } } } From 3567b4911fd024c7c103301c9cafeac26220859b Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Wed, 20 May 2026 17:00:16 +0800 Subject: [PATCH 02/12] only parallelize kvr --- .../AzureAppConfigurationProvider.cs | 47 +++++++++++++++---- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index f865c8d16..3d2ccd36c 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -15,6 +15,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Mime; using System.Net.Sockets; using System.Text; using System.Threading; @@ -629,11 +630,16 @@ private async Task> PrepareData(Dictionary>>>(data.Count); + // Only Key Vault references perform network I/O during adapter processing; other + // adapters complete synchronously. To avoid the overhead of wrapping non-I/O work + // in tasks, only Key Vault references are dispatched concurrently. Non-Key Vault + // settings are processed inline in their original order; their results, along with + // those of the in-flight Key Vault tasks, are merged in insertion order to preserve + // prefix-stripping and last-write-wins behavior. + var results = new IEnumerable>[data.Count]; + var pendingKeyVaultTasks = new List<(int Index, Task>> Task)>(); + + int index = 0; foreach (KeyValuePair kvp in data) { @@ -642,14 +648,31 @@ private async Task> PrepareData(Dictionary>[] results = await Task.WhenAll(pendingTasks).ConfigureAwait(false); + if (pendingKeyVaultTasks.Count > 0) + { + await Task.WhenAll(pendingKeyVaultTasks.Select(p => p.Task)).ConfigureAwait(false); + + foreach ((int Index, Task>> Task) entry in pendingKeyVaultTasks) + { + results[entry.Index] = entry.Task.Result; + } + } - for (int i = 0; i < results.Length; i++) + foreach (IEnumerable> keyValuePairs in results) { - MergeIntoApplicationData(applicationData, results[i]); + MergeIntoApplicationData(applicationData, keyValuePairs); } } else @@ -672,6 +695,12 @@ private async Task> PrepareData(Dictionary applicationData, IEnumerable> keyValuePairs) { foreach (KeyValuePair kv in keyValuePairs) From 8843c4479d582aee0288daf9b707981088e687e6 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Tue, 26 May 2026 10:18:31 +0800 Subject: [PATCH 03/12] resolve comments --- .../AzureAppConfigurationProvider.cs | 100 +++++--------- .../AzureKeyVaultSecretProvider.cs | 123 +++++------------- 2 files changed, 67 insertions(+), 156 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 3d2ccd36c..50017a736 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -628,67 +628,56 @@ private async Task> PrepareData(Dictionary>[data.Count]; - var pendingKeyVaultTasks = new List<(int Index, Task>> Task)>(); - - int index = 0; + // Only Key Vault references perform network I/O during adapter processing; other + // adapters complete synchronously. When parallel resolution is enabled, Key Vault + // references are dispatched concurrently while non-Key Vault settings are processed + // inline. Results are merged once at the end. + var results = new List>>(data.Count); + List>>> pendingKeyVaultTasks = parallelSecretResolution + ? new List>>>() + : null; - foreach (KeyValuePair kvp in data) + foreach (KeyValuePair kvp in data) + { + if (_requestTracingEnabled && _requestTracingOptions != null) { - if (_requestTracingEnabled && _requestTracingOptions != null) - { - _requestTracingOptions.UpdateAiConfigurationTracing(kvp.Value.ContentType); - } - - if (IsKeyVaultReference(kvp.Value)) - { - pendingKeyVaultTasks.Add((index, ProcessAdapters(kvp.Value, cancellationToken))); - } - else - { - results[index] = await ProcessAdapters(kvp.Value, cancellationToken).ConfigureAwait(false); - } - - index++; + _requestTracingOptions.UpdateAiConfigurationTracing(kvp.Value.ContentType); } - if (pendingKeyVaultTasks.Count > 0) + if (parallelSecretResolution && IsKeyVaultReference(kvp.Value)) { - await Task.WhenAll(pendingKeyVaultTasks.Select(p => p.Task)).ConfigureAwait(false); - - foreach ((int Index, Task>> Task) entry in pendingKeyVaultTasks) - { - results[entry.Index] = entry.Task.Result; - } + pendingKeyVaultTasks.Add(ProcessAdapters(kvp.Value, cancellationToken)); } - - foreach (IEnumerable> keyValuePairs in results) + else { - MergeIntoApplicationData(applicationData, keyValuePairs); + results.Add(await ProcessAdapters(kvp.Value, cancellationToken).ConfigureAwait(false)); } } - else + + if (pendingKeyVaultTasks?.Count > 0) + { + IEnumerable>[] keyVaultResults = + await Task.WhenAll(pendingKeyVaultTasks).ConfigureAwait(false); + + results.AddRange(keyVaultResults); + } + + foreach (IEnumerable> keyValuePairs in results) { - foreach (KeyValuePair kvp in data) + foreach (KeyValuePair kv in keyValuePairs) { - IEnumerable> keyValuePairs = null; + string key = kv.Key; - if (_requestTracingEnabled && _requestTracingOptions != null) + foreach (string prefix in _options.KeyPrefixes) { - _requestTracingOptions.UpdateAiConfigurationTracing(kvp.Value.ContentType); + if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + key = key.Substring(prefix.Length); + break; + } } - keyValuePairs = await ProcessAdapters(kvp.Value, cancellationToken).ConfigureAwait(false); - - MergeIntoApplicationData(applicationData, keyValuePairs); + applicationData[key] = kv.Value; } } @@ -701,25 +690,6 @@ private static bool IsKeyVaultReference(ConfigurationSetting setting) && contentType.IsKeyVaultReference(); } - private void MergeIntoApplicationData(Dictionary applicationData, IEnumerable> keyValuePairs) - { - foreach (KeyValuePair kv in keyValuePairs) - { - string key = kv.Key; - - foreach (string prefix in _options.KeyPrefixes) - { - if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - key = key.Substring(prefix.Length); - break; - } - } - - applicationData[key] = kv.Value; - } - } - private async Task LoadAsync(bool ignoreFailures, CancellationToken cancellationToken) { var startupStopwatch = Stopwatch.StartNew(); diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs index af03bcef9..696e02a7c 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs @@ -4,8 +4,8 @@ using Azure.Security.KeyVault.Secrets; using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions; using System; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -14,17 +14,14 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault internal class AzureKeyVaultSecretProvider { private readonly AzureAppConfigurationKeyVaultOptions _keyVaultOptions; - private readonly IDictionary _secretClients; - private readonly Dictionary _cachedKeyVaultSecrets; - private readonly object _cacheLock = new object(); - private Uri _nextRefreshSourceId; - private DateTimeOffset? _nextRefreshTime; + private readonly ConcurrentDictionary _secretClients; + private readonly ConcurrentDictionary _cachedKeyVaultSecrets; public AzureKeyVaultSecretProvider(AzureAppConfigurationKeyVaultOptions keyVaultOptions = null) { _keyVaultOptions = keyVaultOptions ?? new AzureAppConfigurationKeyVaultOptions(); - _cachedKeyVaultSecrets = new Dictionary(); - _secretClients = new Dictionary(StringComparer.OrdinalIgnoreCase); + _cachedKeyVaultSecrets = new ConcurrentDictionary(); + _secretClients = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); if (_keyVaultOptions.SecretClients != null) { @@ -39,19 +36,15 @@ public AzureKeyVaultSecretProvider(AzureAppConfigurationKeyVaultOptions keyVault public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifier, string key, string label, Logger logger, CancellationToken cancellationToken) { string secretValue = null; - SecretClient client; - lock (_cacheLock) + if (_cachedKeyVaultSecrets.TryGetValue(secretIdentifier.SourceId, out CachedKeyVaultSecret cachedHit) && + (!cachedHit.RefreshAt.HasValue || DateTimeOffset.UtcNow < cachedHit.RefreshAt.Value)) { - if (_cachedKeyVaultSecrets.TryGetValue(secretIdentifier.SourceId, out CachedKeyVaultSecret cachedHit) && - (!cachedHit.RefreshAt.HasValue || DateTimeOffset.UtcNow < cachedHit.RefreshAt.Value)) - { - return cachedHit.SecretValue; - } - - client = GetSecretClient(secretIdentifier.SourceId); + return cachedHit.SecretValue; } + SecretClient client = GetSecretClient(secretIdentifier.SourceId); + if (client == null && _keyVaultOptions.SecretResolver == null) { throw new UnauthorizedAccessException("No key vault credential or secret resolver callback configured, and no matching secret client could be found."); @@ -79,10 +72,7 @@ public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifi } finally { - lock (_cacheLock) - { - SetSecretInCache(secretIdentifier.SourceId, key, cachedSecret, success); - } + SetSecretInCache(secretIdentifier.SourceId, key, cachedSecret, success); } return secretValue; @@ -90,51 +80,35 @@ public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifi public bool ShouldRefreshKeyVaultSecrets() { - lock (_cacheLock) + DateTimeOffset utcNow = DateTimeOffset.UtcNow; + + foreach (KeyValuePair secret in _cachedKeyVaultSecrets) { - return _nextRefreshTime.HasValue && _nextRefreshTime.Value < DateTimeOffset.UtcNow; + if (secret.Value.RefreshAt.HasValue && secret.Value.RefreshAt.Value < utcNow) + { + return true; + } } + + return false; } public void ClearCache() { - lock (_cacheLock) - { - var sourceIdsToRemove = new List(); + DateTimeOffset utcNow = DateTimeOffset.UtcNow; - var utcNow = DateTimeOffset.UtcNow; - - foreach (KeyValuePair secret in _cachedKeyVaultSecrets) - { - if (secret.Value.LastRefreshTime + RefreshConstants.MinimumSecretRefreshInterval < utcNow) - { - sourceIdsToRemove.Add(secret.Key); - } - } - - foreach (Uri sourceId in sourceIdsToRemove) - { - _cachedKeyVaultSecrets.Remove(sourceId); - } - - if (_cachedKeyVaultSecrets.Any()) + foreach (KeyValuePair secret in _cachedKeyVaultSecrets) + { + if (secret.Value.LastRefreshTime + RefreshConstants.MinimumSecretRefreshInterval < utcNow) { - UpdateNextRefreshableSecretFromCache(); + _cachedKeyVaultSecrets.TryRemove(secret.Key, out _); } } } public void RemoveSecretFromCache(Uri sourceId) { - lock (_cacheLock) - { - _cachedKeyVaultSecrets.Remove(sourceId); - - if (sourceId == _nextRefreshSourceId) - { - UpdateNextRefreshableSecretFromCache(); - } - } + _cachedKeyVaultSecrets.TryRemove(sourceId, out _); } private SecretClient GetSecretClient(Uri secretUri) @@ -151,14 +125,12 @@ private SecretClient GetSecretClient(Uri secretUri) return null; } - client = new SecretClient( - new Uri(secretUri.GetLeftPart(UriPartial.Authority)), - _keyVaultOptions.Credential, - _keyVaultOptions.ClientOptions); - - _secretClients.Add(keyVaultId, client); - - return client; + return _secretClients.GetOrAdd( + keyVaultId, + _ => new SecretClient( + new Uri(secretUri.GetLeftPart(UriPartial.Authority)), + _keyVaultOptions.Credential, + _keyVaultOptions.ClientOptions)); } private void SetSecretInCache(Uri sourceId, string key, CachedKeyVaultSecret cachedSecret, bool success = true) @@ -170,37 +142,6 @@ private void SetSecretInCache(Uri sourceId, string key, CachedKeyVaultSecret cac UpdateCacheExpirationTimeForSecret(key, cachedSecret, success); _cachedKeyVaultSecrets[sourceId] = cachedSecret; - - if (sourceId == _nextRefreshSourceId) - { - UpdateNextRefreshableSecretFromCache(); - } - else if ((cachedSecret.RefreshAt.HasValue && _nextRefreshTime.HasValue && cachedSecret.RefreshAt.Value < _nextRefreshTime.Value) - || (cachedSecret.RefreshAt.HasValue && !_nextRefreshTime.HasValue)) - { - _nextRefreshSourceId = sourceId; - _nextRefreshTime = cachedSecret.RefreshAt.Value; - } - } - - private void UpdateNextRefreshableSecretFromCache() - { - _nextRefreshSourceId = null; - _nextRefreshTime = DateTimeOffset.MaxValue; - - foreach (KeyValuePair secret in _cachedKeyVaultSecrets) - { - if (secret.Value.RefreshAt.HasValue && secret.Value.RefreshAt.Value < _nextRefreshTime) - { - _nextRefreshTime = secret.Value.RefreshAt; - _nextRefreshSourceId = secret.Key; - } - } - - if (_nextRefreshTime == DateTimeOffset.MaxValue) - { - _nextRefreshTime = null; - } } private void UpdateCacheExpirationTimeForSecret(string key, CachedKeyVaultSecret cachedSecret, bool success) From af2dd46ec415632a6d97ad5588c95f2c4e54b280 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Thu, 28 May 2026 14:44:02 +0800 Subject: [PATCH 04/12] resolve comments --- .../AzureAppConfigurationOptions.cs | 6 ++-- .../AzureAppConfigurationProvider.cs | 34 +++++++++---------- .../AzureKeyVaultKeyValueAdapter.cs | 8 ++--- .../AzureKeyVaultSecretProvider.cs | 20 +++++------ .../ConfigurationSettingExtensions.cs | 19 +++++++++++ 5 files changed, 49 insertions(+), 38 deletions(-) create mode 100644 src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationSettingExtensions.cs diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 8fd34d477..68862f518 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -137,17 +137,17 @@ internal IEnumerable Adapters /// /// Flag to indicate whether Key Vault options have been configured. /// - internal bool IsKeyVaultConfigured { get; private set; } = false; + internal bool IsKeyVaultConfigured { get; private set; } /// /// Flag to indicate whether Key Vault secret values will be refreshed automatically. /// - internal bool IsKeyVaultRefreshConfigured { get; private set; } = false; + internal bool IsKeyVaultRefreshConfigured { get; private set; } /// /// Flag to indicate whether Key Vault references should be resolved in parallel. /// - internal bool IsParallelSecretResolutionEnabled { get; private set; } = false; + internal bool IsParallelSecretResolutionEnabled { get; private set; } /// /// Indicates all feature flag features used by the application. diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 50017a736..419421aa3 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -15,7 +15,6 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Mime; using System.Net.Sockets; using System.Text; using System.Threading; @@ -631,12 +630,15 @@ private async Task> PrepareData(Dictionary>>(data.Count); - List>>> pendingKeyVaultTasks = parallelSecretResolution - ? new List>>>() + // inline. Results are slotted by index so that the original ordering of settings + // (and the selector precedence it encodes) is preserved. + var results = new IEnumerable>[data.Count]; + List<(int Index, Task>> Task)> pendingKeyVaultTasks = parallelSecretResolution + ? new List<(int, Task>>)>() : null; + int index = 0; + foreach (KeyValuePair kvp in data) { if (_requestTracingEnabled && _requestTracingOptions != null) @@ -644,22 +646,26 @@ private async Task> PrepareData(Dictionary 0) { - IEnumerable>[] keyVaultResults = - await Task.WhenAll(pendingKeyVaultTasks).ConfigureAwait(false); + await Task.WhenAll(pendingKeyVaultTasks.Select(p => p.Task)).ConfigureAwait(false); - results.AddRange(keyVaultResults); + foreach ((int Index, Task>> Task) entry in pendingKeyVaultTasks) + { + results[entry.Index] = entry.Task.Result; + } } foreach (IEnumerable> keyValuePairs in results) @@ -684,12 +690,6 @@ private async Task> PrepareData(Dictionary GetSecretValue(KeyVaultSecretIdentifier secretIdentifi { string secretValue = null; - if (_cachedKeyVaultSecrets.TryGetValue(secretIdentifier.SourceId, out CachedKeyVaultSecret cachedHit) && - (!cachedHit.RefreshAt.HasValue || DateTimeOffset.UtcNow < cachedHit.RefreshAt.Value)) + if (_cachedKeyVaultSecrets.TryGetValue(secretIdentifier.SourceId, out CachedKeyVaultSecret cachedSecret) && + (!cachedSecret.RefreshAt.HasValue || DateTimeOffset.UtcNow < cachedSecret.RefreshAt.Value)) { - return cachedHit.SecretValue; + return cachedSecret.SecretValue; } SecretClient client = GetSecretClient(secretIdentifier.SourceId); @@ -50,7 +50,7 @@ public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifi throw new UnauthorizedAccessException("No key vault credential or secret resolver callback configured, and no matching secret client could be found."); } - CachedKeyVaultSecret cachedSecret = null; + CachedKeyVaultSecret updatedCachedSecret = null; bool success = false; try @@ -67,12 +67,12 @@ public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifi secretValue = await _keyVaultOptions.SecretResolver(secretIdentifier.SourceId).ConfigureAwait(false); } - cachedSecret = new CachedKeyVaultSecret(secretValue, secretIdentifier.SourceId); + updatedCachedSecret = new CachedKeyVaultSecret(secretValue, secretIdentifier.SourceId); success = true; } finally { - SetSecretInCache(secretIdentifier.SourceId, key, cachedSecret, success); + SetSecretInCache(secretIdentifier.SourceId, key, updatedCachedSecret, success); } return secretValue; @@ -80,11 +80,9 @@ public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifi public bool ShouldRefreshKeyVaultSecrets() { - DateTimeOffset utcNow = DateTimeOffset.UtcNow; - foreach (KeyValuePair secret in _cachedKeyVaultSecrets) { - if (secret.Value.RefreshAt.HasValue && secret.Value.RefreshAt.Value < utcNow) + if (secret.Value.RefreshAt.HasValue && secret.Value.RefreshAt.Value < DateTimeOffset.UtcNow) { return true; } @@ -95,11 +93,9 @@ public bool ShouldRefreshKeyVaultSecrets() public void ClearCache() { - DateTimeOffset utcNow = DateTimeOffset.UtcNow; - foreach (KeyValuePair secret in _cachedKeyVaultSecrets) { - if (secret.Value.LastRefreshTime + RefreshConstants.MinimumSecretRefreshInterval < utcNow) + if (secret.Value.LastRefreshTime + RefreshConstants.MinimumSecretRefreshInterval < DateTimeOffset.UtcNow) { _cachedKeyVaultSecrets.TryRemove(secret.Key, out _); } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationSettingExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationSettingExtensions.cs new file mode 100644 index 000000000..28639337c --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationSettingExtensions.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Azure.Data.AppConfiguration; +using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault; +using System.Net.Mime; + +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions +{ + internal static class ConfigurationSettingExtensions + { + public static bool IsKeyVaultReference(this ConfigurationSetting setting) + { + return setting != null + && setting.ContentType.TryParseContentType(out ContentType contentType) + && contentType.IsKeyVaultReference(); + } + } +} From a2783823373318f2314b67a7a290d99c41bca2d4 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Thu, 28 May 2026 20:16:45 +0800 Subject: [PATCH 05/12] add PreloadAsync --- .../AzureAppConfigurationProvider.cs | 47 +++--------- .../AzureKeyVaultKeyValueAdapter.cs | 73 +++++++++++++++++++ .../FeatureManagementKeyValueAdapter.cs | 5 ++ .../IKeyValueAdapter.cs | 4 + .../JsonKeyValueAdapter.cs | 5 ++ 5 files changed, 98 insertions(+), 36 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 419421aa3..98247d430 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -625,19 +625,16 @@ private async Task> PrepareData(Dictionary>[data.Count]; - List<(int Index, Task>> Task)> pendingKeyVaultTasks = parallelSecretResolution - ? new List<(int, Task>>)>() - : null; - - int index = 0; + // When parallel secret resolution is enabled, let each adapter pre-warm its caches + // (Key Vault references are dispatched concurrently here) so the sequential loop below + // can process settings in the original order without losing precedence on key collisions. + if (_options.IsParallelSecretResolutionEnabled) + { + await Task.WhenAll( + _options.Adapters.Select(adapter => + adapter.PreloadAsync(data.Values, _logger, cancellationToken))) + .ConfigureAwait(false); + } foreach (KeyValuePair kvp in data) { @@ -646,30 +643,8 @@ private async Task> PrepareData(Dictionary> keyValuePairs = await ProcessAdapters(kvp.Value, cancellationToken).ConfigureAwait(false); - if (pendingKeyVaultTasks?.Count > 0) - { - await Task.WhenAll(pendingKeyVaultTasks.Select(p => p.Task)).ConfigureAwait(false); - - foreach ((int Index, Task>> Task) entry in pendingKeyVaultTasks) - { - results[entry.Index] = entry.Task.Result; - } - } - - foreach (IEnumerable> keyValuePairs in results) - { foreach (KeyValuePair kv in keyValuePairs) { string key = kv.Key; diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs index ef0fda913..5e7118ab5 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs @@ -112,6 +112,79 @@ public bool NeedsRefresh() return _secretProvider.ShouldRefreshKeyVaultSecrets(); } + public async Task PreloadAsync(IEnumerable settings, Logger logger, CancellationToken cancellationToken) + { + if (settings == null) + { + return; + } + + HashSet seen = null; + List<(KeyVaultSecretIdentifier Identifier, string Key, string Label)> toFetch = null; + + foreach (ConfigurationSetting setting in settings) + { + if (!CanProcess(setting)) + { + continue; + } + + string secretRefUri = ParseSecretReferenceUri(setting); + + if (string.IsNullOrEmpty(secretRefUri) || + !Uri.TryCreate(secretRefUri, UriKind.Absolute, out Uri secretUri) || + !KeyVaultSecretIdentifier.TryCreate(secretUri, out KeyVaultSecretIdentifier secretIdentifier)) + { + // Invalid references are surfaced from ProcessKeyValue with full exception context. + continue; + } + + seen = seen ?? new HashSet(); + + if (!seen.Add(secretIdentifier.SourceId)) + { + continue; + } + + toFetch = toFetch ?? new List<(KeyVaultSecretIdentifier, string, string)>(); + toFetch.Add((secretIdentifier, setting.Key, setting.Label)); + } + + if (toFetch == null) + { + return; + } + + var tasks = new Task[toFetch.Count]; + + for (int i = 0; i < toFetch.Count; i++) + { + (KeyVaultSecretIdentifier identifier, string key, string label) = toFetch[i]; + tasks[i] = PreloadSecretAsync(identifier, key, label, logger, cancellationToken); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + private async Task PreloadSecretAsync(KeyVaultSecretIdentifier identifier, string key, string label, Logger logger, CancellationToken cancellationToken) + { + try + { + await _secretProvider.GetSecretValue(identifier, key, label, logger, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch + { + // Per-secret failures are deferred so ProcessKeyValue can throw a properly populated + // KeyVaultReferenceException. Evict the negative cache entry written by GetSecretValue + // so the retry actually re-fetches instead of returning a cached null within the backoff. + _secretProvider.RemoveSecretFromCache(identifier.SourceId); + } + } + private string ParseSecretReferenceUri(ConfigurationSetting setting) { string secretRefUri = null; diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs index fdd7f2fdf..a3431cd7b 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs @@ -87,6 +87,11 @@ public void OnConfigUpdated() return; } + public Task PreloadAsync(IEnumerable settings, Logger logger, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + private List> ProcessDotnetSchemaFeatureFlag(FeatureFlag featureFlag, ConfigurationSetting setting, Uri endpoint) { var keyValues = new List>(); diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IKeyValueAdapter.cs index de13314e1..9dddc0e02 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IKeyValueAdapter.cs @@ -13,6 +13,10 @@ internal interface IKeyValueAdapter { Task>> ProcessKeyValue(ConfigurationSetting setting, Uri endpoint, Logger logger, CancellationToken cancellationToken); + // Pre-warm any per-setting state (e.g. Key Vault secret cache) before ProcessKeyValue is invoked + // on each setting. Adapters with no pre-fetchable state can return a completed task. + Task PreloadAsync(IEnumerable settings, Logger logger, CancellationToken cancellationToken); + bool CanProcess(ConfigurationSetting setting); void OnChangeDetected(ConfigurationSetting setting = null); diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs index d353439fd..9d8826e78 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs @@ -80,5 +80,10 @@ public bool NeedsRefresh() { return false; } + + public Task PreloadAsync(IEnumerable settings, Logger logger, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } } } From 6cb26ebdd9b91672766241838f824561ca7edd5b Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Mon, 8 Jun 2026 15:02:29 +0800 Subject: [PATCH 06/12] resolve comments --- .../AzureAppConfigurationKeyVaultOptions.cs | 4 +--- .../AzureAppConfigurationProvider.cs | 12 +++--------- .../AzureKeyVaultKeyValueAdapter.cs | 2 +- .../AzureKeyVaultSecretProvider.cs | 2 ++ .../IKeyValueAdapter.cs | 2 -- .../Unit/KeyVaultReferenceTests.cs | 2 ++ 6 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationKeyVaultOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationKeyVaultOptions.cs index 70a4d75e5..0aa5fdc62 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationKeyVaultOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationKeyVaultOptions.cs @@ -32,9 +32,7 @@ public class AzureAppConfigurationKeyVaultOptions internal bool IsKeyVaultRefreshConfigured = false; /// - /// Specifies whether Key Vault references should be resolved in parallel. - /// Default value is false. Enabling this can reduce the time required to resolve Key Vault references - /// when many references are loaded from Azure App Configuration. + /// Flag to indicate whether Key Vault references should be resolved in parallel. Disabled by default. /// public bool ParallelSecretResolutionEnabled { get; set; } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 98247d430..4c1994acf 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -625,15 +625,9 @@ private async Task> PrepareData(Dictionary - adapter.PreloadAsync(data.Values, _logger, cancellationToken))) - .ConfigureAwait(false); + foreach (IKeyValueAdapter adapter in _options.Adapters) + { + await adapter.PreloadAsync(data.Values, _logger, cancellationToken).ConfigureAwait(false); } foreach (KeyValuePair kvp in data) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs index 5e7118ab5..88625f499 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs @@ -114,7 +114,7 @@ public bool NeedsRefresh() public async Task PreloadAsync(IEnumerable settings, Logger logger, CancellationToken cancellationToken) { - if (settings == null) + if (settings == null || !_secretProvider.IsParallelSecretResolutionEnabled) { return; } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs index 145cee7aa..10550b606 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs @@ -17,6 +17,8 @@ internal class AzureKeyVaultSecretProvider private readonly ConcurrentDictionary _secretClients; private readonly ConcurrentDictionary _cachedKeyVaultSecrets; + public bool IsParallelSecretResolutionEnabled => _keyVaultOptions.ParallelSecretResolutionEnabled; + public AzureKeyVaultSecretProvider(AzureAppConfigurationKeyVaultOptions keyVaultOptions = null) { _keyVaultOptions = keyVaultOptions ?? new AzureAppConfigurationKeyVaultOptions(); diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IKeyValueAdapter.cs index 9dddc0e02..5bcb3f30e 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IKeyValueAdapter.cs @@ -13,8 +13,6 @@ internal interface IKeyValueAdapter { Task>> ProcessKeyValue(ConfigurationSetting setting, Uri endpoint, Logger logger, CancellationToken cancellationToken); - // Pre-warm any per-setting state (e.g. Key Vault secret cache) before ProcessKeyValue is invoked - // on each setting. Adapters with no pre-fetchable state can return a completed task. Task PreloadAsync(IEnumerable settings, Logger logger, CancellationToken cancellationToken); bool CanProcess(ConfigurationSetting setting); diff --git a/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs b/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs index f1214f456..c0f7cecc0 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs @@ -507,6 +507,8 @@ public void DoesNotThrowKeyVaultExceptionWhenProviderIsOptional() var mockKeyValueAdapter = new Mock(MockBehavior.Strict); mockKeyValueAdapter.Setup(adapter => adapter.CanProcess(It.IsAny())) .Returns(true); + mockKeyValueAdapter.Setup(adapter => adapter.PreloadAsync(It.IsAny>(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); mockKeyValueAdapter.Setup(adapter => adapter.ProcessKeyValue(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new KeyVaultReferenceException("Key vault error", null)); mockKeyValueAdapter.Setup(adapter => adapter.OnChangeDetected(null)); From 72ce15657d541b1a063532dd45f364bcdf5da7d3 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Tue, 9 Jun 2026 17:12:29 +0800 Subject: [PATCH 07/12] resolve comments --- .../AzureAppConfigurationOptions.cs | 6 --- .../AzureKeyVaultKeyValueAdapter.cs | 42 +++++++++++++++---- .../KeyVaultConstants.cs | 2 + .../ConfigurationSettingExtensions.cs | 19 --------- 4 files changed, 35 insertions(+), 34 deletions(-) delete mode 100644 src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationSettingExtensions.cs diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 68862f518..bc351a01f 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -144,11 +144,6 @@ internal IEnumerable Adapters /// internal bool IsKeyVaultRefreshConfigured { get; private set; } - /// - /// Flag to indicate whether Key Vault references should be resolved in parallel. - /// - internal bool IsParallelSecretResolutionEnabled { get; private set; } - /// /// Indicates all feature flag features used by the application. /// @@ -525,7 +520,6 @@ public AzureAppConfigurationOptions ConfigureKeyVault(Action settings, Logger logger, CancellationToken cancellationToken) { - if (settings == null || !_secretProvider.IsParallelSecretResolutionEnabled) + if (settings == null) { return; } @@ -155,19 +157,37 @@ public async Task PreloadAsync(IEnumerable settings, Logge return; } - var tasks = new Task[toFetch.Count]; + if (_secretProvider.IsParallelSecretResolutionEnabled) + { + using (var throttle = new SemaphoreSlim(KeyVaultConstants.MaxParallelSecretResolution)) + { + var tasks = new Task[toFetch.Count]; + + for (int i = 0; i < toFetch.Count; i++) + { + (KeyVaultSecretIdentifier identifier, string key, string label) = toFetch[i]; + tasks[i] = PreloadSecretAsync(identifier, key, label, throttle, logger, cancellationToken); + } - for (int i = 0; i < toFetch.Count; i++) + await Task.WhenAll(tasks).ConfigureAwait(false); + } + } + else { - (KeyVaultSecretIdentifier identifier, string key, string label) = toFetch[i]; - tasks[i] = PreloadSecretAsync(identifier, key, label, logger, cancellationToken); + foreach ((KeyVaultSecretIdentifier identifier, string key, string label) in toFetch) + { + await PreloadSecretAsync(identifier, key, label, throttle: null, logger, cancellationToken).ConfigureAwait(false); + } } - - await Task.WhenAll(tasks).ConfigureAwait(false); } - private async Task PreloadSecretAsync(KeyVaultSecretIdentifier identifier, string key, string label, Logger logger, CancellationToken cancellationToken) + private async Task PreloadSecretAsync(KeyVaultSecretIdentifier identifier, string key, string label, SemaphoreSlim throttle, Logger logger, CancellationToken cancellationToken) { + if (throttle != null) + { + await throttle.WaitAsync(cancellationToken).ConfigureAwait(false); + } + try { await _secretProvider.GetSecretValue(identifier, key, label, logger, cancellationToken).ConfigureAwait(false); @@ -183,6 +203,10 @@ private async Task PreloadSecretAsync(KeyVaultSecretIdentifier identifier, strin // so the retry actually re-fetches instead of returning a cached null within the backoff. _secretProvider.RemoveSecretFromCache(identifier.SourceId); } + finally + { + throttle?.Release(); + } } private string ParseSecretReferenceUri(ConfigurationSetting setting) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultConstants.cs index 1309e58cf..5cedf993e 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultConstants.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultConstants.cs @@ -8,5 +8,7 @@ internal class KeyVaultConstants public const string ContentType = "application/vnd.microsoft.appconfig.keyvaultref+json"; public const string SecretReferenceUriJsonPropertyName = "uri"; + + public const int MaxParallelSecretResolution = 16; } } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationSettingExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationSettingExtensions.cs deleted file mode 100644 index 28639337c..000000000 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationSettingExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Azure.Data.AppConfiguration; -using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault; -using System.Net.Mime; - -namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions -{ - internal static class ConfigurationSettingExtensions - { - public static bool IsKeyVaultReference(this ConfigurationSetting setting) - { - return setting != null - && setting.ContentType.TryParseContentType(out ContentType contentType) - && contentType.IsKeyVaultReference(); - } - } -} From caaee6b5e8d961f1d05ab9bd966f4cdc9eb30b97 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Wed, 10 Jun 2026 13:24:44 +0800 Subject: [PATCH 08/12] resolve comments --- .../AzureKeyVaultKeyValueAdapter.cs | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs index 062a50676..b55fe9475 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs @@ -75,7 +75,9 @@ KeyVaultReferenceException CreateKeyVaultReferenceException(string message, Conf public bool CanProcess(ConfigurationSetting setting) { - if (setting == null || string.IsNullOrWhiteSpace(setting.Value)) + if (setting == null || + string.IsNullOrWhiteSpace(setting.Value) || + string.IsNullOrWhiteSpace(setting.ContentType)) { return false; } @@ -122,7 +124,7 @@ public async Task PreloadAsync(IEnumerable settings, Logge } HashSet seen = null; - List<(KeyVaultSecretIdentifier Identifier, string Key, string Label)> toFetch = null; + List<(KeyVaultSecretIdentifier Identifier, ConfigurationSetting Setting, string SecretRefUri)> toFetch = null; foreach (ConfigurationSetting setting in settings) { @@ -137,8 +139,7 @@ public async Task PreloadAsync(IEnumerable settings, Logge !Uri.TryCreate(secretRefUri, UriKind.Absolute, out Uri secretUri) || !KeyVaultSecretIdentifier.TryCreate(secretUri, out KeyVaultSecretIdentifier secretIdentifier)) { - // Invalid references are surfaced from ProcessKeyValue with full exception context. - continue; + throw CreateKeyVaultReferenceException("Invalid Key vault secret identifier.", setting, null, secretRefUri); } seen = seen ?? new HashSet(); @@ -148,8 +149,8 @@ public async Task PreloadAsync(IEnumerable settings, Logge continue; } - toFetch = toFetch ?? new List<(KeyVaultSecretIdentifier, string, string)>(); - toFetch.Add((secretIdentifier, setting.Key, setting.Label)); + toFetch = toFetch ?? new List<(KeyVaultSecretIdentifier, ConfigurationSetting, string)>(); + toFetch.Add((secretIdentifier, setting, secretRefUri)); } if (toFetch == null) @@ -165,8 +166,8 @@ public async Task PreloadAsync(IEnumerable settings, Logge for (int i = 0; i < toFetch.Count; i++) { - (KeyVaultSecretIdentifier identifier, string key, string label) = toFetch[i]; - tasks[i] = PreloadSecretAsync(identifier, key, label, throttle, logger, cancellationToken); + (KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri) = toFetch[i]; + tasks[i] = PreloadSecretAsync(identifier, setting, secretRefUri, throttle, logger, cancellationToken); } await Task.WhenAll(tasks).ConfigureAwait(false); @@ -174,14 +175,14 @@ public async Task PreloadAsync(IEnumerable settings, Logge } else { - foreach ((KeyVaultSecretIdentifier identifier, string key, string label) in toFetch) + foreach ((KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri) in toFetch) { - await PreloadSecretAsync(identifier, key, label, throttle: null, logger, cancellationToken).ConfigureAwait(false); + await PreloadSecretAsync(identifier, setting, secretRefUri, throttle: null, logger, cancellationToken).ConfigureAwait(false); } } } - private async Task PreloadSecretAsync(KeyVaultSecretIdentifier identifier, string key, string label, SemaphoreSlim throttle, Logger logger, CancellationToken cancellationToken) + private async Task PreloadSecretAsync(KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri, SemaphoreSlim throttle, Logger logger, CancellationToken cancellationToken) { if (throttle != null) { @@ -190,18 +191,19 @@ private async Task PreloadSecretAsync(KeyVaultSecretIdentifier identifier, strin try { - await _secretProvider.GetSecretValue(identifier, key, label, logger, cancellationToken).ConfigureAwait(false); + await _secretProvider.GetSecretValue(identifier, setting.Key, setting.Label, logger, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { throw; } - catch + catch (Exception e) when (e is UnauthorizedAccessException || (e.Source?.Equals(AzureIdentityAssemblyName, StringComparison.OrdinalIgnoreCase) ?? false)) { - // Per-secret failures are deferred so ProcessKeyValue can throw a properly populated - // KeyVaultReferenceException. Evict the negative cache entry written by GetSecretValue - // so the retry actually re-fetches instead of returning a cached null within the backoff. - _secretProvider.RemoveSecretFromCache(identifier.SourceId); + throw CreateKeyVaultReferenceException(e.Message, setting, e, secretRefUri); + } + catch (Exception e) when (e is RequestFailedException || ((e as AggregateException)?.InnerExceptions?.All(e => e is RequestFailedException) ?? false)) + { + throw CreateKeyVaultReferenceException("Key vault error.", setting, e, secretRefUri); } finally { From b49f262df4e714d940ac6771533093ac76e97169 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Tue, 14 Jul 2026 15:31:23 +0800 Subject: [PATCH 09/12] update --- .../AzureKeyVaultKeyValueAdapter.cs | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs index b55fe9475..0c588da6f 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs @@ -123,8 +123,8 @@ public async Task PreloadAsync(IEnumerable settings, Logge return; } - HashSet seen = null; - List<(KeyVaultSecretIdentifier Identifier, ConfigurationSetting Setting, string SecretRefUri)> toFetch = null; + HashSet seen = new HashSet(); + List<(KeyVaultSecretIdentifier Identifier, ConfigurationSetting Setting, string SecretRefUri)> toFetch = new List<(KeyVaultSecretIdentifier, ConfigurationSetting, string)>(); foreach (ConfigurationSetting setting in settings) { @@ -142,32 +142,29 @@ public async Task PreloadAsync(IEnumerable settings, Logge throw CreateKeyVaultReferenceException("Invalid Key vault secret identifier.", setting, null, secretRefUri); } - seen = seen ?? new HashSet(); - if (!seen.Add(secretIdentifier.SourceId)) { continue; } - toFetch = toFetch ?? new List<(KeyVaultSecretIdentifier, ConfigurationSetting, string)>(); toFetch.Add((secretIdentifier, setting, secretRefUri)); } - if (toFetch == null) + if (toFetch.Count == 0) { return; } if (_secretProvider.IsParallelSecretResolutionEnabled) { - using (var throttle = new SemaphoreSlim(KeyVaultConstants.MaxParallelSecretResolution)) + using (var semaphore = new SemaphoreSlim(KeyVaultConstants.MaxParallelSecretResolution)) { var tasks = new Task[toFetch.Count]; for (int i = 0; i < toFetch.Count; i++) { (KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri) = toFetch[i]; - tasks[i] = PreloadSecretAsync(identifier, setting, secretRefUri, throttle, logger, cancellationToken); + tasks[i] = PreloadSecretAsync(identifier, setting, secretRefUri, semaphore, logger, cancellationToken); } await Task.WhenAll(tasks).ConfigureAwait(false); @@ -177,16 +174,16 @@ public async Task PreloadAsync(IEnumerable settings, Logge { foreach ((KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri) in toFetch) { - await PreloadSecretAsync(identifier, setting, secretRefUri, throttle: null, logger, cancellationToken).ConfigureAwait(false); + await PreloadSecretAsync(identifier, setting, secretRefUri, semaphore: null, logger, cancellationToken).ConfigureAwait(false); } } } - private async Task PreloadSecretAsync(KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri, SemaphoreSlim throttle, Logger logger, CancellationToken cancellationToken) + private async Task PreloadSecretAsync(KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri, SemaphoreSlim semaphore, Logger logger, CancellationToken cancellationToken) { - if (throttle != null) + if (semaphore != null) { - await throttle.WaitAsync(cancellationToken).ConfigureAwait(false); + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); } try @@ -207,7 +204,7 @@ private async Task PreloadSecretAsync(KeyVaultSecretIdentifier identifier, Confi } finally { - throttle?.Release(); + semaphore?.Release(); } } From 0077d1e8e45fe7e692c4f0c18ded9f8bd0602f06 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Tue, 21 Jul 2026 15:40:03 +0800 Subject: [PATCH 10/12] refactor PreloadAsync: use worker pool pattern --- .../AzureKeyVaultKeyValueAdapter.cs | 119 +++++++----------- .../AzureKeyVaultSecretProvider.cs | 27 +++- .../Exceptions/KeyVaultReferenceException.cs | 17 +++ 3 files changed, 87 insertions(+), 76 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs index 0c588da6f..a35bd808d 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs @@ -1,15 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using Azure; using Azure.Data.AppConfiguration; using Azure.Security.KeyVault.Secrets; using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions; using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement; using System; using System.Collections.Generic; -using System.Linq; using System.Net.Mime; +using System.Runtime.ExceptionServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -18,8 +17,6 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault { internal class AzureKeyVaultKeyValueAdapter : IKeyValueAdapter { - private const string AzureIdentityAssemblyName = "Azure.Identity"; - private readonly AzureKeyVaultSecretProvider _secretProvider; public AzureKeyVaultKeyValueAdapter(AzureKeyVaultSecretProvider secretProvider) @@ -37,23 +34,10 @@ public async Task>> ProcessKeyValue(Con // Uri validation if (string.IsNullOrEmpty(secretRefUri) || !Uri.TryCreate(secretRefUri, UriKind.Absolute, out Uri secretUri) || !KeyVaultSecretIdentifier.TryCreate(secretUri, out KeyVaultSecretIdentifier secretIdentifier)) { - throw CreateKeyVaultReferenceException("Invalid Key vault secret identifier.", setting, null, secretRefUri); + throw KeyVaultReferenceException.Create("Invalid Key vault secret identifier.", setting, null, secretRefUri); } - string secret; - - try - { - secret = await _secretProvider.GetSecretValue(secretIdentifier, setting.Key, setting.Label, logger, cancellationToken).ConfigureAwait(false); - } - catch (Exception e) when (e is UnauthorizedAccessException || (e.Source?.Equals(AzureIdentityAssemblyName, StringComparison.OrdinalIgnoreCase) ?? false)) - { - throw CreateKeyVaultReferenceException(e.Message, setting, e, secretRefUri); - } - catch (Exception e) when (e is RequestFailedException || ((e as AggregateException)?.InnerExceptions?.All(e => e is RequestFailedException) ?? false)) - { - throw CreateKeyVaultReferenceException("Key vault error.", setting, e, secretRefUri); - } + string secret = await _secretProvider.GetSecretValue(secretIdentifier, setting, secretRefUri, logger, cancellationToken).ConfigureAwait(false); return new KeyValuePair[] { @@ -61,18 +45,6 @@ public async Task>> ProcessKeyValue(Con }; } - KeyVaultReferenceException CreateKeyVaultReferenceException(string message, ConfigurationSetting setting, Exception inner, string secretRefUri = null) - { - return new KeyVaultReferenceException(message, inner) - { - Key = setting.Key, - Label = setting.Label, - Etag = setting.ETag.ToString(), - ErrorCode = (inner as RequestFailedException)?.ErrorCode, - SecretIdentifier = secretRefUri - }; - } - public bool CanProcess(ConfigurationSetting setting) { if (setting == null || @@ -139,7 +111,7 @@ public async Task PreloadAsync(IEnumerable settings, Logge !Uri.TryCreate(secretRefUri, UriKind.Absolute, out Uri secretUri) || !KeyVaultSecretIdentifier.TryCreate(secretUri, out KeyVaultSecretIdentifier secretIdentifier)) { - throw CreateKeyVaultReferenceException("Invalid Key vault secret identifier.", setting, null, secretRefUri); + throw KeyVaultReferenceException.Create("Invalid Key vault secret identifier.", setting, null, secretRefUri); } if (!seen.Add(secretIdentifier.SourceId)) @@ -157,57 +129,62 @@ public async Task PreloadAsync(IEnumerable settings, Logge if (_secretProvider.IsParallelSecretResolutionEnabled) { - using (var semaphore = new SemaphoreSlim(KeyVaultConstants.MaxParallelSecretResolution)) + int workerCount = Math.Min(KeyVaultConstants.MaxParallelSecretResolution, toFetch.Count); + int nextIndex = -1; + Exception firstException = null; + Task[] workers = new Task[workerCount]; + + using (var failFastCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) { - var tasks = new Task[toFetch.Count]; + async Task WorkerLoopAsync() + { + while (!failFastCts.IsCancellationRequested) + { + int index = Interlocked.Increment(ref nextIndex); + + if (index >= toFetch.Count) + { + return; + } + + (KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri) = toFetch[index]; + + try + { + await _secretProvider.GetSecretValue(identifier, setting, secretRefUri, logger, failFastCts.Token).ConfigureAwait(false); + } + catch (Exception e) + { + // Stop other workers from picking up new work and remember the first failure so it can be surfaced below. + Interlocked.CompareExchange(ref firstException, e, null); + failFastCts.Cancel(); + return; + } + } + } - for (int i = 0; i < toFetch.Count; i++) + for (int i = 0; i < workerCount; i++) { - (KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri) = toFetch[i]; - tasks[i] = PreloadSecretAsync(identifier, setting, secretRefUri, semaphore, logger, cancellationToken); + workers[i] = WorkerLoopAsync(); } - await Task.WhenAll(tasks).ConfigureAwait(false); + await Task.WhenAll(workers).ConfigureAwait(false); + } + + if (firstException != null) + { + ExceptionDispatchInfo.Capture(firstException).Throw(); } } else { foreach ((KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri) in toFetch) { - await PreloadSecretAsync(identifier, setting, secretRefUri, semaphore: null, logger, cancellationToken).ConfigureAwait(false); + await _secretProvider.GetSecretValue(identifier, setting, secretRefUri, logger, cancellationToken).ConfigureAwait(false); } } } - private async Task PreloadSecretAsync(KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri, SemaphoreSlim semaphore, Logger logger, CancellationToken cancellationToken) - { - if (semaphore != null) - { - await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - try - { - await _secretProvider.GetSecretValue(identifier, setting.Key, setting.Label, logger, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception e) when (e is UnauthorizedAccessException || (e.Source?.Equals(AzureIdentityAssemblyName, StringComparison.OrdinalIgnoreCase) ?? false)) - { - throw CreateKeyVaultReferenceException(e.Message, setting, e, secretRefUri); - } - catch (Exception e) when (e is RequestFailedException || ((e as AggregateException)?.InnerExceptions?.All(e => e is RequestFailedException) ?? false)) - { - throw CreateKeyVaultReferenceException("Key vault error.", setting, e, secretRefUri); - } - finally - { - semaphore?.Release(); - } - } - private string ParseSecretReferenceUri(ConfigurationSetting setting) { string secretRefUri = null; @@ -218,7 +195,7 @@ private string ParseSecretReferenceUri(ConfigurationSetting setting) if (reader.Read() && reader.TokenType != JsonTokenType.StartObject) { - throw CreateKeyVaultReferenceException(ErrorMessages.InvalidKeyVaultReference, setting, null, null); + throw KeyVaultReferenceException.Create(ErrorMessages.InvalidKeyVaultReference, setting, null, null); } while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) @@ -236,7 +213,7 @@ private string ParseSecretReferenceUri(ConfigurationSetting setting) } else { - throw CreateKeyVaultReferenceException(ErrorMessages.InvalidKeyVaultReference, setting, null, null); + throw KeyVaultReferenceException.Create(ErrorMessages.InvalidKeyVaultReference, setting, null, null); } } else @@ -247,7 +224,7 @@ private string ParseSecretReferenceUri(ConfigurationSetting setting) } catch (JsonException e) { - throw CreateKeyVaultReferenceException(ErrorMessages.InvalidKeyVaultReference, setting, e, null); + throw KeyVaultReferenceException.Create(ErrorMessages.InvalidKeyVaultReference, setting, e, null); } return secretRefUri; diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs index 10550b606..0d407248b 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs @@ -1,11 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using Azure; +using Azure.Data.AppConfiguration; using Azure.Security.KeyVault.Secrets; using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -13,6 +16,8 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault { internal class AzureKeyVaultSecretProvider { + private const string AzureIdentityAssemblyName = "Azure.Identity"; + private readonly AzureAppConfigurationKeyVaultOptions _keyVaultOptions; private readonly ConcurrentDictionary _secretClients; private readonly ConcurrentDictionary _cachedKeyVaultSecrets; @@ -35,7 +40,7 @@ public AzureKeyVaultSecretProvider(AzureAppConfigurationKeyVaultOptions keyVault } } - public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifier, string key, string label, Logger logger, CancellationToken cancellationToken) + public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifier, ConfigurationSetting setting, string secretRefUri, Logger logger, CancellationToken cancellationToken) { string secretValue = null; @@ -49,7 +54,7 @@ public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifi if (client == null && _keyVaultOptions.SecretResolver == null) { - throw new UnauthorizedAccessException("No key vault credential or secret resolver callback configured, and no matching secret client could be found."); + throw KeyVaultReferenceException.Create("No key vault credential or secret resolver callback configured, and no matching secret client could be found.", setting, null, secretRefUri); } CachedKeyVaultSecret updatedCachedSecret = null; @@ -60,8 +65,8 @@ public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifi if (client != null) { KeyVaultSecret secret = await client.GetSecretAsync(secretIdentifier.Name, secretIdentifier.Version, cancellationToken).ConfigureAwait(false); - logger.LogDebug(LogHelper.BuildKeyVaultSecretReadMessage(key, label)); - logger.LogInformation(LogHelper.BuildKeyVaultSettingUpdatedMessage(key)); + logger.LogDebug(LogHelper.BuildKeyVaultSecretReadMessage(setting.Key, setting.Label)); + logger.LogInformation(LogHelper.BuildKeyVaultSettingUpdatedMessage(setting.Key)); secretValue = secret.Value; } else if (_keyVaultOptions.SecretResolver != null) @@ -72,9 +77,21 @@ public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifi updatedCachedSecret = new CachedKeyVaultSecret(secretValue, secretIdentifier.SourceId); success = true; } + catch (OperationCanceledException) + { + throw; + } + catch (Exception e) when (e is UnauthorizedAccessException || (e.Source?.Equals(AzureIdentityAssemblyName, StringComparison.OrdinalIgnoreCase) ?? false)) + { + throw KeyVaultReferenceException.Create(e.Message, setting, e, secretRefUri); + } + catch (Exception e) when (e is RequestFailedException || ((e as AggregateException)?.InnerExceptions?.All(e => e is RequestFailedException) ?? false)) + { + throw KeyVaultReferenceException.Create("Key vault error.", setting, e, secretRefUri); + } finally { - SetSecretInCache(secretIdentifier.SourceId, key, updatedCachedSecret, success); + SetSecretInCache(secretIdentifier.SourceId, setting.Key, updatedCachedSecret, success); } return secretValue; diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Exceptions/KeyVaultReferenceException.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Exceptions/KeyVaultReferenceException.cs index 7d549a068..6bdb980c8 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Exceptions/KeyVaultReferenceException.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Exceptions/KeyVaultReferenceException.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using Azure; +using Azure.Data.AppConfiguration; using System; namespace Microsoft.Extensions.Configuration.AzureAppConfiguration @@ -55,5 +57,20 @@ public KeyVaultReferenceException(string message, /// The error code, if available, describing the cause of the exception. /// public string ErrorCode { get; set; } + + /// + /// Creates a populated with identifying information from the setting that failed to resolve. + /// + internal static KeyVaultReferenceException Create(string message, ConfigurationSetting setting, Exception inner, string secretRefUri = null) + { + return new KeyVaultReferenceException(message, inner) + { + Key = setting.Key, + Label = setting.Label, + Etag = setting.ETag.ToString(), + ErrorCode = (inner as RequestFailedException)?.ErrorCode, + SecretIdentifier = secretRefUri + }; + } } } From f9b28bccde8cf8958f60e62a324c1784359e43e0 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Tue, 21 Jul 2026 18:37:25 +0800 Subject: [PATCH 11/12] update --- .../AzureKeyVaultKeyValueAdapter.cs | 57 ++++++------------- .../AzureKeyVaultSecretProvider.cs | 4 -- 2 files changed, 18 insertions(+), 43 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs index a35bd808d..5258a1472 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.Net.Mime; -using System.Runtime.ExceptionServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -95,8 +94,8 @@ public async Task PreloadAsync(IEnumerable settings, Logge return; } - HashSet seen = new HashSet(); - List<(KeyVaultSecretIdentifier Identifier, ConfigurationSetting Setting, string SecretRefUri)> toFetch = new List<(KeyVaultSecretIdentifier, ConfigurationSetting, string)>(); + var seen = new HashSet(); + var toFetch = new List<(KeyVaultSecretIdentifier, ConfigurationSetting, string)>(); foreach (ConfigurationSetting setting in settings) { @@ -129,51 +128,31 @@ public async Task PreloadAsync(IEnumerable settings, Logge if (_secretProvider.IsParallelSecretResolutionEnabled) { - int workerCount = Math.Min(KeyVaultConstants.MaxParallelSecretResolution, toFetch.Count); - int nextIndex = -1; - Exception firstException = null; - Task[] workers = new Task[workerCount]; - - using (var failFastCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) + using (var semaphore = new SemaphoreSlim(KeyVaultConstants.MaxParallelSecretResolution)) { - async Task WorkerLoopAsync() + async Task ResolveSecretAsync(KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri) { - while (!failFastCts.IsCancellationRequested) + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + await _secretProvider.GetSecretValue(identifier, setting, secretRefUri, logger, cancellationToken).ConfigureAwait(false); + } + finally { - int index = Interlocked.Increment(ref nextIndex); - - if (index >= toFetch.Count) - { - return; - } - - (KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri) = toFetch[index]; - - try - { - await _secretProvider.GetSecretValue(identifier, setting, secretRefUri, logger, failFastCts.Token).ConfigureAwait(false); - } - catch (Exception e) - { - // Stop other workers from picking up new work and remember the first failure so it can be surfaced below. - Interlocked.CompareExchange(ref firstException, e, null); - failFastCts.Cancel(); - return; - } + semaphore.Release(); } } - for (int i = 0; i < workerCount; i++) + Task[] tasks = new Task[toFetch.Count]; + + for (int i = 0; i < toFetch.Count; i++) { - workers[i] = WorkerLoopAsync(); + (KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri) = toFetch[i]; + tasks[i] = ResolveSecretAsync(identifier, setting, secretRefUri); } - await Task.WhenAll(workers).ConfigureAwait(false); - } - - if (firstException != null) - { - ExceptionDispatchInfo.Capture(firstException).Throw(); + await Task.WhenAll(tasks).ConfigureAwait(false); } } else diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs index 0d407248b..51381083a 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs @@ -77,10 +77,6 @@ public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifi updatedCachedSecret = new CachedKeyVaultSecret(secretValue, secretIdentifier.SourceId); success = true; } - catch (OperationCanceledException) - { - throw; - } catch (Exception e) when (e is UnauthorizedAccessException || (e.Source?.Equals(AzureIdentityAssemblyName, StringComparison.OrdinalIgnoreCase) ?? false)) { throw KeyVaultReferenceException.Create(e.Message, setting, e, secretRefUri); From e1fcdc454818c31a4ea1bd968b290ca544ec5d78 Mon Sep 17 00:00:00 2001 From: "Lingling Ye (from Dev Box)" Date: Thu, 23 Jul 2026 10:52:15 +0800 Subject: [PATCH 12/12] update --- .../AzureKeyVaultKeyValueAdapter.cs | 22 +++++++++---------- .../AzureKeyVaultSecretProvider.cs | 8 +++---- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs index 5258a1472..38c7a5a02 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs @@ -36,7 +36,7 @@ public async Task>> ProcessKeyValue(Con throw KeyVaultReferenceException.Create("Invalid Key vault secret identifier.", setting, null, secretRefUri); } - string secret = await _secretProvider.GetSecretValue(secretIdentifier, setting, secretRefUri, logger, cancellationToken).ConfigureAwait(false); + string secret = await _secretProvider.GetSecretValue(secretIdentifier, setting, logger, cancellationToken).ConfigureAwait(false); return new KeyValuePair[] { @@ -95,7 +95,7 @@ public async Task PreloadAsync(IEnumerable settings, Logge } var seen = new HashSet(); - var toFetch = new List<(KeyVaultSecretIdentifier, ConfigurationSetting, string)>(); + var toFetch = new List<(KeyVaultSecretIdentifier, ConfigurationSetting)>(); foreach (ConfigurationSetting setting in settings) { @@ -113,12 +113,10 @@ public async Task PreloadAsync(IEnumerable settings, Logge throw KeyVaultReferenceException.Create("Invalid Key vault secret identifier.", setting, null, secretRefUri); } - if (!seen.Add(secretIdentifier.SourceId)) + if (seen.Add(secretIdentifier.SourceId)) { - continue; + toFetch.Add((secretIdentifier, setting)); } - - toFetch.Add((secretIdentifier, setting, secretRefUri)); } if (toFetch.Count == 0) @@ -130,13 +128,13 @@ public async Task PreloadAsync(IEnumerable settings, Logge { using (var semaphore = new SemaphoreSlim(KeyVaultConstants.MaxParallelSecretResolution)) { - async Task ResolveSecretAsync(KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri) + async Task ResolveSecretAsync(KeyVaultSecretIdentifier identifier, ConfigurationSetting setting) { await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { - await _secretProvider.GetSecretValue(identifier, setting, secretRefUri, logger, cancellationToken).ConfigureAwait(false); + await _secretProvider.GetSecretValue(identifier, setting, logger, cancellationToken).ConfigureAwait(false); } finally { @@ -148,8 +146,8 @@ async Task ResolveSecretAsync(KeyVaultSecretIdentifier identifier, Configuration for (int i = 0; i < toFetch.Count; i++) { - (KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri) = toFetch[i]; - tasks[i] = ResolveSecretAsync(identifier, setting, secretRefUri); + (KeyVaultSecretIdentifier identifier, ConfigurationSetting setting) = toFetch[i]; + tasks[i] = ResolveSecretAsync(identifier, setting); } await Task.WhenAll(tasks).ConfigureAwait(false); @@ -157,9 +155,9 @@ async Task ResolveSecretAsync(KeyVaultSecretIdentifier identifier, Configuration } else { - foreach ((KeyVaultSecretIdentifier identifier, ConfigurationSetting setting, string secretRefUri) in toFetch) + foreach ((KeyVaultSecretIdentifier identifier, ConfigurationSetting setting) in toFetch) { - await _secretProvider.GetSecretValue(identifier, setting, secretRefUri, logger, cancellationToken).ConfigureAwait(false); + await _secretProvider.GetSecretValue(identifier, setting, logger, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs index 51381083a..8046e8a01 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs @@ -40,7 +40,7 @@ public AzureKeyVaultSecretProvider(AzureAppConfigurationKeyVaultOptions keyVault } } - public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifier, ConfigurationSetting setting, string secretRefUri, Logger logger, CancellationToken cancellationToken) + public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifier, ConfigurationSetting setting, Logger logger, CancellationToken cancellationToken) { string secretValue = null; @@ -54,7 +54,7 @@ public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifi if (client == null && _keyVaultOptions.SecretResolver == null) { - throw KeyVaultReferenceException.Create("No key vault credential or secret resolver callback configured, and no matching secret client could be found.", setting, null, secretRefUri); + throw KeyVaultReferenceException.Create("No key vault credential or secret resolver callback configured, and no matching secret client could be found.", setting, null, secretIdentifier.SourceId.ToString()); } CachedKeyVaultSecret updatedCachedSecret = null; @@ -79,11 +79,11 @@ public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifi } catch (Exception e) when (e is UnauthorizedAccessException || (e.Source?.Equals(AzureIdentityAssemblyName, StringComparison.OrdinalIgnoreCase) ?? false)) { - throw KeyVaultReferenceException.Create(e.Message, setting, e, secretRefUri); + throw KeyVaultReferenceException.Create(e.Message, setting, e, secretIdentifier.SourceId.ToString()); } catch (Exception e) when (e is RequestFailedException || ((e as AggregateException)?.InnerExceptions?.All(e => e is RequestFailedException) ?? false)) { - throw KeyVaultReferenceException.Create("Key vault error.", setting, e, secretRefUri); + throw KeyVaultReferenceException.Create("Key vault error.", setting, e, secretIdentifier.SourceId.ToString()); } finally {