From ab0ce78a46952a103b5473fa103e44e2ab6cb789 Mon Sep 17 00:00:00 2001 From: linglingye001 <143174321+linglingye001@users.noreply.github.com> Date: Tue, 19 May 2026 14:40:36 +0800 Subject: [PATCH 1/7] add null check in examples (#732) --- examples/ConfigStoreDemo/Program.cs | 8 ++++++++ examples/ConsoleAppWithFailOver/Program.cs | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/examples/ConfigStoreDemo/Program.cs b/examples/ConfigStoreDemo/Program.cs index 40d0b9edc..2614c7465 100644 --- a/examples/ConfigStoreDemo/Program.cs +++ b/examples/ConfigStoreDemo/Program.cs @@ -24,6 +24,14 @@ public static IWebHost BuildWebHost(string[] args) // 3. Set up the provider to listen for changes to the background color key-value in Azure App Configuration var settings = config.AddJsonFile("appsettings.json").Build(); + + if (string.IsNullOrEmpty(settings["connection_string"])) + { + throw new InvalidOperationException( + "Connection string not found. " + + "Please set the 'connection_string' in appsettings.json."); + } + config.AddAzureAppConfiguration(options => { options.Connect(settings["connection_string"]) diff --git a/examples/ConsoleAppWithFailOver/Program.cs b/examples/ConsoleAppWithFailOver/Program.cs index decbba29e..dd89670d5 100644 --- a/examples/ConsoleAppWithFailOver/Program.cs +++ b/examples/ConsoleAppWithFailOver/Program.cs @@ -31,7 +31,10 @@ private static void Configure() IConfiguration configuration = builder.Build(); IConfigurationSection endpointsSection = configuration.GetSection("AppConfig:Endpoints"); - IEnumerable endpoints = endpointsSection.GetChildren().Select(endpoint => new Uri(endpoint.Value)); + IEnumerable endpoints = endpointsSection.GetChildren() + .Select(endpoint => endpoint.Value) + .Where(value => !string.IsNullOrEmpty(value)) + .Select(value => new Uri(value)); if (endpoints == null || !endpoints.Any()) { From 1b02267ce75cb5e3cb298a2443868d860f9ff5df Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:25:14 +0800 Subject: [PATCH 2/7] Merge pull request #739 from Azure/zhiyuanliang/fix-test-secret-cleanup Fix test secret not being cleaned up --- .../Integration/IntegrationTests.cs | 10 ++++++---- .../Unit/KeyVaultReferenceTests.cs | 12 ++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs index b7be4f9b9..532fc0ef5 100644 --- a/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs +++ b/tests/Tests.AzureAppConfiguration/Integration/IntegrationTests.cs @@ -1661,13 +1661,15 @@ public async Task KeyVaultReference_DifferentRefreshIntervals() TestContext testContext = CreateTestContext("KeyVaultDifferentIntervals"); IConfigurationRefresher refresher = null; - // Create a secret in Key Vault with short refresh interval - string secretName1 = $"test-secret1-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; + // Create a secret in Key Vault with short refresh interval. + // Name must start with TestKeyPrefix so CleanupStaleResources() can find and delete it. + string secretName1 = $"{testContext.KeyPrefix}-secret1"; string secretValue1 = $"SecretValue1-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; await _secretClient.SetSecretAsync(secretName1, secretValue1); - // Create another secret in Key Vault with long refresh interval - string secretName2 = $"test-secret2-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; + // Create another secret in Key Vault with long refresh interval. + // Name must start with TestKeyPrefix so CleanupStaleResources() can find and delete it. + string secretName2 = $"{testContext.KeyPrefix}-secret2"; string secretValue2 = $"SecretValue2-{Guid.NewGuid().ToString("N").Substring(0, 8)}"; await _secretClient.SetSecretAsync(secretName2, secretValue2); diff --git a/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs b/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs index 3e856a1b0..b23077c2a 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs @@ -744,7 +744,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o // Update sentinel key-value sentinelKv = TestHelpers.ChangeValue(sentinelKv, "Value2"); - Thread.Sleep(refreshInterval); + Thread.Sleep(refreshInterval + TimeSpan.FromSeconds(1)); // buffer avoids the boundary race on the refresh interval await refresher.RefreshAsync(); Assert.Equal("Value2", config["Sentinel"]); @@ -816,7 +816,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o // Update sentinel key-value to trigger refresh operation sentinelKv = TestHelpers.ChangeValue(sentinelKv, "Value2"); - Thread.Sleep(refreshInterval); + Thread.Sleep(refreshInterval + TimeSpan.FromSeconds(1)); // buffer avoids the boundary race on the refresh interval await refresher.RefreshAsync(); Assert.Equal("Value2", config["Sentinel"]); @@ -861,7 +861,7 @@ public async Task SecretIsReloadedFromKeyVaultWhenCacheExpires() Assert.Equal(_secretValue, config[_kv.Key]); // Sleep to let the secret cache expire - Thread.Sleep(refreshInterval); + Thread.Sleep(refreshInterval + TimeSpan.FromSeconds(1)); // buffer avoids the boundary race on the refresh interval await refresher.RefreshAsync(); Assert.Equal(_secretValue, config[_kv.Key]); @@ -905,7 +905,7 @@ public async Task SecretsWithDefaultRefreshInterval() Assert.Equal(_secretValue, config["TK2"]); // Sleep to let the secret cache expire for both secrets - Thread.Sleep(shortRefreshInterval); + Thread.Sleep(shortRefreshInterval + TimeSpan.FromSeconds(1)); // buffer avoids the boundary race on the refresh interval await refresher.RefreshAsync(); Assert.Equal(_secretValue, config["TK1"]); @@ -951,8 +951,8 @@ public async Task SecretsWithDifferentRefreshIntervals() Assert.Equal(_secretValue, config["TK1"]); Assert.Equal(_secretValue, config["TK2"]); - // Sleep to let the secret cache expire for one secret - Thread.Sleep(shortRefreshInterval); + // Sleep past the short interval (plus a small buffer to avoid a boundary race) so only that secret's cache expires + Thread.Sleep(shortRefreshInterval + TimeSpan.FromSeconds(1)); await refresher.RefreshAsync(); Assert.Equal(_secretValue, config["TK1"]); From 5d87a5334fad55a66ec56b277d2b63c5de841c4d Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:06:01 +0800 Subject: [PATCH 3/7] Add azure pipeline (#742) * add azure pipeline * place holder pipeline --- azure-pipelines.yml | 92 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 azure-pipelines.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 000000000..52efa9bf8 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,92 @@ +# Azure DevOps (OneBranch) placeholder pipeline for AppConfiguration-DotnetProvider. +# +# TEMPORARY validation pipeline. It does NOT build or run tests yet - it only: +# 1. Confirms pull requests trigger this pipeline in Azure DevOps. +# 2. Confirms Azure login works via the shared ARM service connection. +# 3. Logs a message. +# +# Once verified, this will be replaced with the full build + integration-test +# pipeline. Per GitHub inside Microsoft (GiM) guidance (https://aka.ms/gim/pipelines), +# it extends the OneBranch governed templates - the same setup the OOBReleases +# pipeline uses for this repo. Create it via StartRight (Governed Pipeline) in the +# msazure / "Azure AppConfig" project with the "I'm creating a OneBranch pipeline" +# box CHECKED. StartRight reads the YAML from the default branch, so this file +# must be on main. + +trigger: + branches: + include: + - main + - preview + - release/* + - zhiyuanliang/ci # TEMP: lets pushes to this branch run the pipeline + +pr: + branches: + include: + - main + - preview + - release/* + +variables: + CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] + system.debug: false + REPOROOT: $(Build.SourcesDirectory) + WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2022/vse2022:latest' + # ARM service connection shared with the OOBReleases pipeline (its identity + # already has access to the integration-test resources). Same id OOBReleases + # references - replace with its display name if you prefer. + azureServiceConnection: 'ba69879b-1ca3-4e17-93f9-0f52dcf69bc5' + +resources: + repositories: + - repository: templates + type: git + name: OneBranch.Pipelines/GovernedTemplates + ref: refs/heads/main + +extends: + template: v2/OneBranch.NonOfficial.CrossPlat.yml@templates + parameters: + cloudvault: + enabled: false + globalSdl: + tsa: + enabled: false + binskim: + break: false + policheck: + break: false + cg: + failOnAlert: false + stages: + - stage: Validate + jobs: + - job: Validate + displayName: 'Verify PR trigger and Azure login' + pool: + type: windows + variables: + ob_outputDirectory: '$(REPOROOT)\out' + steps: + - checkout: self + + - script: if not exist "$(REPOROOT)\out" mkdir "$(REPOROOT)\out" + displayName: 'Create output directory' + + - script: echo Hello from Azure Pipelines - verifying PR trigger + displayName: 'Log a message' + + # Proves the shared ARM service connection authenticates from this + # pipeline. Prints the signed-in account (no secrets are logged). + - task: AzureCLI@2 + displayName: 'Verify Azure login' + inputs: + azureSubscription: '$(azureServiceConnection)' + scriptType: ps + scriptLocation: inlineScript + inlineScript: | + Write-Host "Verifying Azure login via the service connection..." + az account show + Write-Host "Client id: $env:AZURESUBSCRIPTION_CLIENT_ID" + Write-Host "Tenant id: $env:AZURESUBSCRIPTION_TENANT_ID" From fb7818229f71be419b866114f6548e1d3081023c Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Wed, 22 Jul 2026 00:51:49 +0800 Subject: [PATCH 4/7] Add minimal validation Azure Pipeline (#743) * add azure pipeline * place holder pipeline * pipeline placeholder --- azure-pipelines.yml | 98 +++++---------------------------------------- 1 file changed, 11 insertions(+), 87 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 52efa9bf8..e43f7c101 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,92 +1,16 @@ -# Azure DevOps (OneBranch) placeholder pipeline for AppConfiguration-DotnetProvider. -# -# TEMPORARY validation pipeline. It does NOT build or run tests yet - it only: -# 1. Confirms pull requests trigger this pipeline in Azure DevOps. -# 2. Confirms Azure login works via the shared ARM service connection. -# 3. Logs a message. -# -# Once verified, this will be replaced with the full build + integration-test -# pipeline. Per GitHub inside Microsoft (GiM) guidance (https://aka.ms/gim/pipelines), -# it extends the OneBranch governed templates - the same setup the OOBReleases -# pipeline uses for this repo. Create it via StartRight (Governed Pipeline) in the -# msazure / "Azure AppConfig" project with the "I'm creating a OneBranch pipeline" -# box CHECKED. StartRight reads the YAML from the default branch, so this file -# must be on main. - trigger: - branches: - include: - - main - - preview - - release/* - - zhiyuanliang/ci # TEMP: lets pushes to this branch run the pipeline +- main +- preview +- release/* pr: - branches: - include: - - main - - preview - - release/* - -variables: - CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] - system.debug: false - REPOROOT: $(Build.SourcesDirectory) - WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2022/vse2022:latest' - # ARM service connection shared with the OOBReleases pipeline (its identity - # already has access to the integration-test resources). Same id OOBReleases - # references - replace with its display name if you prefer. - azureServiceConnection: 'ba69879b-1ca3-4e17-93f9-0f52dcf69bc5' - -resources: - repositories: - - repository: templates - type: git - name: OneBranch.Pipelines/GovernedTemplates - ref: refs/heads/main - -extends: - template: v2/OneBranch.NonOfficial.CrossPlat.yml@templates - parameters: - cloudvault: - enabled: false - globalSdl: - tsa: - enabled: false - binskim: - break: false - policheck: - break: false - cg: - failOnAlert: false - stages: - - stage: Validate - jobs: - - job: Validate - displayName: 'Verify PR trigger and Azure login' - pool: - type: windows - variables: - ob_outputDirectory: '$(REPOROOT)\out' - steps: - - checkout: self - - - script: if not exist "$(REPOROOT)\out" mkdir "$(REPOROOT)\out" - displayName: 'Create output directory' +- main +- preview +- release/* - - script: echo Hello from Azure Pipelines - verifying PR trigger - displayName: 'Log a message' +pool: + vmImage: ubuntu-latest - # Proves the shared ARM service connection authenticates from this - # pipeline. Prints the signed-in account (no secrets are logged). - - task: AzureCLI@2 - displayName: 'Verify Azure login' - inputs: - azureSubscription: '$(azureServiceConnection)' - scriptType: ps - scriptLocation: inlineScript - inlineScript: | - Write-Host "Verifying Azure login via the service connection..." - az account show - Write-Host "Client id: $env:AZURESUBSCRIPTION_CLIENT_ID" - Write-Host "Tenant id: $env:AZURESUBSCRIPTION_TENANT_ID" +steps: +- script: echo "Hello from Azure Pipelines" + displayName: Hello world From 6ee5a6534d1b2138203ce2fb8cb63244185d24e4 Mon Sep 17 00:00:00 2001 From: linglingye001 <143174321+linglingye001@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:21:43 +0800 Subject: [PATCH 5/7] Resolve key vault references concurrently (#736) * resolve key vault references concurrently * only parallelize kvr * resolve comments * resolve comments * add PreloadAsync * resolve comments * resolve comments * resolve comments * update * refactor PreloadAsync: use worker pool pattern * update * update --- .../AzureAppConfigurationKeyVaultOptions.cs | 5 + .../AzureAppConfigurationOptions.cs | 4 +- .../AzureAppConfigurationProvider.cs | 9 +- .../AzureKeyVaultKeyValueAdapter.cs | 114 +++++++---- .../AzureKeyVaultSecretProvider.cs | 118 +++++------ .../KeyVaultConstants.cs | 2 + .../Exceptions/KeyVaultReferenceException.cs | 17 ++ .../FeatureManagementKeyValueAdapter.cs | 5 + .../IKeyValueAdapter.cs | 2 + .../JsonKeyValueAdapter.cs | 5 + .../Unit/KeyVaultReferenceTests.cs | 185 ++++++++++++++++++ 11 files changed, 353 insertions(+), 113 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationKeyVaultOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationKeyVaultOptions.cs index cca16df80..0aa5fdc62 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationKeyVaultOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationKeyVaultOptions.cs @@ -31,6 +31,11 @@ public class AzureAppConfigurationKeyVaultOptions internal TimeSpan? DefaultSecretRefreshInterval = null; internal bool IsKeyVaultRefreshConfigured = false; + /// + /// Flag to indicate whether Key Vault references should be resolved in parallel. Disabled by default. + /// + 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..bc351a01f 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -137,12 +137,12 @@ 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; } /// /// 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 fc92d1290..4c1994acf 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -625,16 +625,19 @@ private async Task> PrepareData(Dictionary kvp in data) + foreach (IKeyValueAdapter adapter in _options.Adapters) { - IEnumerable> keyValuePairs = null; + await adapter.PreloadAsync(data.Values, _logger, cancellationToken).ConfigureAwait(false); + } + foreach (KeyValuePair kvp in data) + { if (_requestTracingEnabled && _requestTracingOptions != null) { _requestTracingOptions.UpdateAiConfigurationTracing(kvp.Value.ContentType); } - keyValuePairs = await ProcessAdapters(kvp.Value, cancellationToken).ConfigureAwait(false); + IEnumerable> keyValuePairs = await ProcessAdapters(kvp.Value, cancellationToken).ConfigureAwait(false); foreach (KeyValuePair kv in keyValuePairs) { diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs index 0498ba09e..38c7a5a02 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs @@ -1,14 +1,12 @@ // 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.Text.Json; using System.Threading; @@ -18,8 +16,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 +33,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, logger, cancellationToken).ConfigureAwait(false); return new KeyValuePair[] { @@ -61,18 +44,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 || @@ -116,6 +87,81 @@ public bool NeedsRefresh() return _secretProvider.ShouldRefreshKeyVaultSecrets(); } + public async Task PreloadAsync(IEnumerable settings, Logger logger, CancellationToken cancellationToken) + { + if (settings == null) + { + return; + } + + var seen = new HashSet(); + var toFetch = new List<(KeyVaultSecretIdentifier, ConfigurationSetting)>(); + + 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)) + { + throw KeyVaultReferenceException.Create("Invalid Key vault secret identifier.", setting, null, secretRefUri); + } + + if (seen.Add(secretIdentifier.SourceId)) + { + toFetch.Add((secretIdentifier, setting)); + } + } + + if (toFetch.Count == 0) + { + return; + } + + if (_secretProvider.IsParallelSecretResolutionEnabled) + { + using (var semaphore = new SemaphoreSlim(KeyVaultConstants.MaxParallelSecretResolution)) + { + async Task ResolveSecretAsync(KeyVaultSecretIdentifier identifier, ConfigurationSetting setting) + { + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + await _secretProvider.GetSecretValue(identifier, setting, logger, cancellationToken).ConfigureAwait(false); + } + finally + { + semaphore.Release(); + } + } + + Task[] tasks = new Task[toFetch.Count]; + + for (int i = 0; i < toFetch.Count; i++) + { + (KeyVaultSecretIdentifier identifier, ConfigurationSetting setting) = toFetch[i]; + tasks[i] = ResolveSecretAsync(identifier, setting); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + } + else + { + foreach ((KeyVaultSecretIdentifier identifier, ConfigurationSetting setting) in toFetch) + { + await _secretProvider.GetSecretValue(identifier, setting, logger, cancellationToken).ConfigureAwait(false); + } + } + } + private string ParseSecretReferenceUri(ConfigurationSetting setting) { string secretRefUri = null; @@ -126,7 +172,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) @@ -144,7 +190,7 @@ private string ParseSecretReferenceUri(ConfigurationSetting setting) } else { - throw CreateKeyVaultReferenceException(ErrorMessages.InvalidKeyVaultReference, setting, null, null); + throw KeyVaultReferenceException.Create(ErrorMessages.InvalidKeyVaultReference, setting, null, null); } } else @@ -155,7 +201,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 57505ff95..8046e8a01 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultSecretProvider.cs @@ -1,9 +1,12 @@ // 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; @@ -13,17 +16,19 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault { internal class AzureKeyVaultSecretProvider { + private const string AzureIdentityAssemblyName = "Azure.Identity"; + private readonly AzureAppConfigurationKeyVaultOptions _keyVaultOptions; - private readonly IDictionary _secretClients; - private readonly Dictionary _cachedKeyVaultSecrets; - private Uri _nextRefreshSourceId; - private DateTimeOffset? _nextRefreshTime; + private readonly ConcurrentDictionary _secretClients; + private readonly ConcurrentDictionary _cachedKeyVaultSecrets; + + public bool IsParallelSecretResolutionEnabled => _keyVaultOptions.ParallelSecretResolutionEnabled; 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) { @@ -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, Logger logger, CancellationToken cancellationToken) { string secretValue = null; @@ -49,9 +54,10 @@ 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, secretIdentifier.SourceId.ToString()); } + CachedKeyVaultSecret updatedCachedSecret = null; bool success = false; try @@ -59,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) @@ -68,12 +74,20 @@ 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; } + catch (Exception e) when (e is UnauthorizedAccessException || (e.Source?.Equals(AzureIdentityAssemblyName, StringComparison.OrdinalIgnoreCase) ?? false)) + { + 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, secretIdentifier.SourceId.ToString()); + } finally { - SetSecretInCache(secretIdentifier.SourceId, key, cachedSecret, success); + SetSecretInCache(secretIdentifier.SourceId, setting.Key, updatedCachedSecret, success); } return secretValue; @@ -81,42 +95,31 @@ public async Task GetSecretValue(KeyVaultSecretIdentifier secretIdentifi public bool ShouldRefreshKeyVaultSecrets() { - return _nextRefreshTime.HasValue && _nextRefreshTime.Value < DateTimeOffset.UtcNow; - } - - public void ClearCache() - { - var sourceIdsToRemove = new List(); - - var utcNow = DateTimeOffset.UtcNow; - foreach (KeyValuePair secret in _cachedKeyVaultSecrets) { - if (secret.Value.LastRefreshTime + RefreshConstants.MinimumSecretRefreshInterval < utcNow) + if (secret.Value.RefreshAt.HasValue && secret.Value.RefreshAt.Value < DateTimeOffset.UtcNow) { - sourceIdsToRemove.Add(secret.Key); + return true; } } - foreach (Uri sourceId in sourceIdsToRemove) - { - _cachedKeyVaultSecrets.Remove(sourceId); - } + return false; + } - if (_cachedKeyVaultSecrets.Any()) + public void ClearCache() + { + foreach (KeyValuePair secret in _cachedKeyVaultSecrets) { - UpdateNextRefreshableSecretFromCache(); + if (secret.Value.LastRefreshTime + RefreshConstants.MinimumSecretRefreshInterval < DateTimeOffset.UtcNow) + { + _cachedKeyVaultSecrets.TryRemove(secret.Key, out _); + } } } public void RemoveSecretFromCache(Uri sourceId) { - _cachedKeyVaultSecrets.Remove(sourceId); - - if (sourceId == _nextRefreshSourceId) - { - UpdateNextRefreshableSecretFromCache(); - } + _cachedKeyVaultSecrets.TryRemove(sourceId, out _); } private SecretClient GetSecretClient(Uri secretUri) @@ -133,14 +136,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) @@ -152,37 +153,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) 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/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 + }; + } } } 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..5bcb3f30e 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IKeyValueAdapter.cs @@ -13,6 +13,8 @@ internal interface IKeyValueAdapter { Task>> ProcessKeyValue(ConfigurationSetting setting, Uri endpoint, Logger logger, CancellationToken cancellationToken); + 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; + } } } diff --git a/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs b/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs index b23077c2a..2ba8fe8a5 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)); @@ -1050,5 +1052,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 e8ec1632cd8011d7591ffc5c5a0bddfa3317e833 Mon Sep 17 00:00:00 2001 From: linglingye001 <143174321+linglingye001@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:12:42 +0800 Subject: [PATCH 6/7] Upgrade package version (#745) --- .../ConsoleAppWithFailOver.csproj | 2 +- ...tensions.Configuration.AzureAppConfiguration.csproj | 10 +++++----- .../Tests.AzureAppConfiguration.AspNetCore.csproj | 4 ++-- ...Tests.AzureAppConfiguration.Functions.Worker.csproj | 2 +- .../Tests.AzureAppConfiguration.csproj | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj b/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj index 09732dbf2..a761e5cab 100644 --- a/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj +++ b/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj index 8c8241a79..c0f9d1653 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj @@ -15,16 +15,16 @@ - + - + - - - + + + diff --git a/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj b/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj index 4a3c672cd..d3bf3c88c 100644 --- a/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj +++ b/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj @@ -1,4 +1,4 @@ - + net8.0;net9.0 @@ -10,7 +10,7 @@ - + diff --git a/tests/Tests.AzureAppConfiguration.Functions.Worker/Tests.AzureAppConfiguration.Functions.Worker.csproj b/tests/Tests.AzureAppConfiguration.Functions.Worker/Tests.AzureAppConfiguration.Functions.Worker.csproj index 935b38306..ec92fd17f 100644 --- a/tests/Tests.AzureAppConfiguration.Functions.Worker/Tests.AzureAppConfiguration.Functions.Worker.csproj +++ b/tests/Tests.AzureAppConfiguration.Functions.Worker/Tests.AzureAppConfiguration.Functions.Worker.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj index 1e5aaae18..48f1ab03b 100644 --- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj +++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj @@ -1,4 +1,4 @@ - + net48;net8.0;net9.0 @@ -11,7 +11,7 @@ - + From b3b0a0a3de438084e16c55e97298a9e1f6a62e1e Mon Sep 17 00:00:00 2001 From: linglingye001 <143174321+linglingye001@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:33:24 +0800 Subject: [PATCH 7/7] version bump 8.6.0 (#746) --- .../Microsoft.Azure.AppConfiguration.AspNetCore.csproj | 2 +- .../Microsoft.Azure.AppConfiguration.Functions.Worker.csproj | 2 +- ...rosoft.Extensions.Configuration.AzureAppConfiguration.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj b/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj index 72e540879..aaad0901c 100644 --- a/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj +++ b/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj @@ -21,7 +21,7 @@ - 8.5.0 + 8.6.0 diff --git a/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj b/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj index 2531bbe3e..89c1034e5 100644 --- a/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj +++ b/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj @@ -24,7 +24,7 @@ - 8.5.0 + 8.6.0 diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj index c0f9d1653..ccab0f8fc 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj @@ -38,7 +38,7 @@ - 8.5.0 + 8.6.0