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 1/6] 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 00000000..52efa9bf
--- /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 2/6] 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 52efa9bf..e43f7c10 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 3/6] 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 cca16df8..0aa5fdc6 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 e5b6e258..bc351a01 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 fc92d129..4c1994ac 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 0498ba09..38c7a5a0 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 57505ff9..8046e8a0 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 1309e58c..5cedf993 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 7d549a06..6bdb980c 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 fdd7f2fd..a3431cd7 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 de13314e..5bcb3f30 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 d353439f..9d8826e7 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 b23077c2..2ba8fe8a 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 4/6] 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 09732dbf..a761e5ca 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 8c8241a7..c0f9d165 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 4a3c672c..d3bf3c88 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 935b3830..ec92fd17 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 1e5aaae1..48f1ab03 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 5/6] 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 72e54087..aaad0901 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 2531bbe3..89c1034e 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 c0f9d165..ccab0f8f 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
From 7597f96d5d699e6c450bfba7dc66fe653e6ccebe Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Wed, 29 Jul 2026 16:54:23 +0800
Subject: [PATCH 6/6] migrate ci to azure pipeline (#750)
---
.github/workflows/ci.yml | 63 ----------------------------------------
azure-pipelines.yml | 57 ++++++++++++++++++++++++++++++++++--
2 files changed, 54 insertions(+), 66 deletions(-)
delete mode 100644 .github/workflows/ci.yml
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
deleted file mode 100644
index 2928be53..00000000
--- a/.github/workflows/ci.yml
+++ /dev/null
@@ -1,63 +0,0 @@
-name: AppConfiguration-DotnetProvider CI
-
-on:
- push:
- branches:
- - main
- - preview
- - release/*
- pull_request:
- branches:
- - main
- - preview
- - release/*
-
-permissions:
- security-events: write
- id-token: write
-
-jobs:
- build:
- runs-on: windows-latest
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v2
-
- - name: Install .NET
- run: pwsh build/install-dotnet.ps1 -RestoreOnly
-
- - name: Restore
- run: pwsh build.ps1 -RestoreOnly
-
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v3
- with:
- languages: 'csharp'
-
- - name: Dotnet Build
- run: pwsh build.ps1
-
- - name: Dotnet Pack
- run: pwsh pack.ps1
-
- - name: Azure Login with OIDC
- uses: azure/login@v1
- with:
- client-id: ${{ secrets.AZURE_CLIENT_ID }}
- tenant-id: ${{ secrets.AZURE_TENANT_ID }}
- subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
-
- - name: Dotnet Test
- run: pwsh test.ps1
- env:
- AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
-
- - name: Publish Test Results
- uses: actions/upload-artifact@v4
- with:
- name: Unit Test Results
- path: ${{ github.workspace }}/tests/**/*.trx
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v3
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index e43f7c10..7c1f5ac6 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -9,8 +9,59 @@ pr:
- release/*
pool:
- vmImage: ubuntu-latest
+ vmImage: windows-latest
steps:
-- script: echo "Hello from Azure Pipelines"
- displayName: Hello world
+- task: PowerShell@2
+ displayName: 'Install .NET'
+ inputs:
+ filePath: build/install-dotnet.ps1
+
+- task: PowerShell@2
+ displayName: 'Restore'
+ inputs:
+ filePath: build.ps1
+ arguments: -RestoreOnly
+
+- task: PowerShell@2
+ displayName: 'Dotnet Build'
+ inputs:
+ filePath: build.ps1
+
+- task: PowerShell@2
+ displayName: 'Dotnet Pack'
+ inputs:
+ filePath: pack.ps1
+
+- task: AzureCLI@2
+ displayName: 'Dotnet Test'
+ inputs:
+ # ARM service connection using Workload Identity Federation (no client secret).
+ azureSubscription: '.NET Provider GitHub PR Integration Test'
+ addSpnToEnvironment: true
+ scriptType: ps
+ scriptLocation: inlineScript
+ inlineScript: |
+ # Configure the env vars that DefaultAzureCredential -> WorkloadIdentityCredential reads.
+ # Do NOT set AZURE_CLIENT_SECRET.
+ $env:AZURE_CLIENT_ID = $env:servicePrincipalId
+ $env:AZURE_TENANT_ID = $env:tenantId
+
+ $tokenPath = Join-Path "$(Agent.TempDirectory)" "azure-federated-token.txt"
+ Set-Content -Path $tokenPath -Value $env:idToken -NoNewline
+ $env:AZURE_FEDERATED_TOKEN_FILE = $tokenPath
+
+ $env:AZURE_SUBSCRIPTION_ID = (az account show --query id -o tsv)
+
+ & "$(Build.SourcesDirectory)\test.ps1"
+ workingDirectory: '$(Build.SourcesDirectory)'
+
+- task: PublishTestResults@2
+ displayName: 'Publish Test Results'
+ condition: succeededOrFailed()
+ inputs:
+ testResultsFormat: 'vstest'
+ testResultsFiles: '**/*.trx'
+ searchFolder: '$(Build.SourcesDirectory)'
+ failTaskOnFailedTests: true
+ testRunTitle: 'Unit Tests'