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..71c15e683 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-preview
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..806e3373b 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-preview
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdConfigurationClientManager.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdConfigurationClientManager.cs
new file mode 100644
index 000000000..fbf057c1b
--- /dev/null
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdConfigurationClientManager.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Azure.Data.AppConfiguration;
+using Microsoft.Extensions.Azure;
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Afd
+{
+ internal class AfdConfigurationClientManager : IConfigurationClientManager
+ {
+ private readonly ConfigurationClientWrapper _clientWrapper;
+
+ public AfdConfigurationClientManager(
+ IAzureClientFactory clientFactory,
+ Uri endpoint)
+ {
+ if (clientFactory == null)
+ {
+ throw new ArgumentNullException(nameof(clientFactory));
+ }
+
+ if (endpoint == null)
+ {
+ throw new ArgumentNullException(nameof(endpoint));
+ }
+
+ _clientWrapper = new ConfigurationClientWrapper(endpoint, clientFactory.CreateClient(endpoint.AbsoluteUri));
+ }
+
+ public IEnumerable GetClients()
+ {
+ return new List { _clientWrapper.Client };
+ }
+
+ public void RefreshClients()
+ {
+ return;
+ }
+
+ public bool UpdateSyncToken(Uri endpoint, string syncToken)
+ {
+ return false;
+ }
+
+ public Uri GetEndpointForClient(ConfigurationClient client)
+ {
+ if (client == null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ return _clientWrapper.Client == client ? _clientWrapper.Endpoint : null;
+ }
+ }
+}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdPolicy.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdPolicy.cs
new file mode 100644
index 000000000..da461b7bf
--- /dev/null
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdPolicy.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Azure.Core;
+using Azure.Core.Pipeline;
+using System;
+
+namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Afd
+{
+ ///
+ /// HTTP pipeline policy that removes Authorization and Sync-Token headers from outgoing requests.
+ ///
+ internal class AfdPolicy : HttpPipelinePolicy
+ {
+ private const string AuthorizationHeader = "Authorization";
+ private const string SyncTokenHeader = "Sync-Token";
+
+ ///
+ /// Processes the HTTP message and removes Authorization and Sync-Token headers.
+ ///
+ /// The HTTP message.
+ /// The pipeline.
+ public override void Process(HttpMessage message, ReadOnlyMemory pipeline)
+ {
+ message.Request.Headers.Remove(AuthorizationHeader);
+
+ message.Request.Headers.Remove(SyncTokenHeader);
+
+ ProcessNext(message, pipeline);
+ }
+
+ ///
+ /// Processes the HTTP message and removes Authorization and Sync-Token headers.
+ ///
+ /// The HTTP message.
+ /// The pipeline.
+ /// A task representing the asynchronous operation.
+ public override async System.Threading.Tasks.ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline)
+ {
+ message.Request.Headers.Remove(AuthorizationHeader);
+
+ message.Request.Headers.Remove(SyncTokenHeader);
+
+ await ProcessNextAsync(message, pipeline).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/EmptyTokenCredential.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/EmptyTokenCredential.cs
new file mode 100644
index 000000000..1239676f7
--- /dev/null
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/EmptyTokenCredential.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Azure.Core;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Afd
+{
+ ///
+ /// A token credential that provides an empty token.
+ ///
+ internal class EmptyTokenCredential : TokenCredential
+ {
+ ///
+ /// Gets an empty token.
+ ///
+ /// The context of the token request.
+ /// A cancellation token to cancel the operation.
+ /// An empty access token.
+ public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ {
+ return new AccessToken(string.Empty, DateTimeOffset.MaxValue);
+ }
+
+ ///
+ /// Asynchronously gets an empty token.
+ ///
+ /// The context of the token request.
+ /// A cancellation token to cancel the operation.
+ /// A task that represents the asynchronous operation. The task result contains an empty access token.
+ public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ {
+ return new ValueTask(new AccessToken(string.Empty, DateTimeOffset.MaxValue));
+ }
+ }
+}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
index e5b6e2585..52fdcece7 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
@@ -4,6 +4,7 @@
using Azure.Core;
using Azure.Data.AppConfiguration;
using Microsoft.Extensions.Azure;
+using Microsoft.Extensions.Configuration.AzureAppConfiguration.Afd;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
@@ -132,7 +133,7 @@ internal IEnumerable Adapters
///
/// Options used to configure the client used to communicate with Azure App Configuration.
///
- internal ConfigurationClientOptions ClientOptions { get; } = GetDefaultClientOptions();
+ internal ConfigurationClientOptions ClientOptions { get; private set; } = GetDefaultClientOptions();
///
/// Flag to indicate whether Key Vault options have been configured.
@@ -154,6 +155,11 @@ internal IEnumerable Adapters
///
internal StartupOptions Startup { get; set; } = new StartupOptions();
+ ///
+ /// Gets a value indicating whether Azure Front Door is used.
+ ///
+ internal bool IsAfdUsed { get; private set; }
+
///
/// Client factory that is responsible for creating instances of ConfigurationClient.
///
@@ -186,11 +192,12 @@ public AzureAppConfigurationOptions()
public AzureAppConfigurationOptions SetClientFactory(IAzureClientFactory factory)
{
ClientFactory = factory ?? throw new ArgumentNullException(nameof(factory));
+
return this;
}
///
- /// Specify what key-values to include in the configuration provider.
+ /// Specifies what key-values to include in the configuration provider.
/// can be called multiple times to include multiple sets of key-values.
///
///
@@ -262,7 +269,7 @@ public AzureAppConfigurationOptions Select(string keyFilter, string labelFilter
}
///
- /// Specify a snapshot and include its contained key-values in the configuration provider.
+ /// Specifies a snapshot and include its contained key-values in the configuration provider.
/// can be called multiple times to include key-values from multiple snapshots.
///
/// The name of the snapshot in Azure App Configuration.
@@ -351,7 +358,7 @@ public AzureAppConfigurationOptions Connect(string connectionString)
throw new ArgumentNullException(nameof(connectionString));
}
- return Connect(new List { connectionString });
+ return Connect(new string[] { connectionString });
}
///
@@ -362,6 +369,11 @@ public AzureAppConfigurationOptions Connect(string connectionString)
///
public AzureAppConfigurationOptions Connect(IEnumerable connectionStrings)
{
+ if (IsAfdUsed)
+ {
+ throw new InvalidOperationException(ErrorMessages.ConnectionConflict);
+ }
+
if (connectionStrings == null || !connectionStrings.Any())
{
throw new ArgumentNullException(nameof(connectionStrings));
@@ -395,7 +407,7 @@ public AzureAppConfigurationOptions Connect(Uri endpoint, TokenCredential creden
throw new ArgumentNullException(nameof(credential));
}
- return Connect(new List() { endpoint }, credential);
+ return Connect(new Uri[] { endpoint }, credential);
}
///
@@ -405,6 +417,11 @@ public AzureAppConfigurationOptions Connect(Uri endpoint, TokenCredential creden
/// Token credential to use to connect.
public AzureAppConfigurationOptions Connect(IEnumerable endpoints, TokenCredential credential)
{
+ if (IsAfdUsed)
+ {
+ throw new InvalidOperationException(ErrorMessages.ConnectionConflict);
+ }
+
if (endpoints == null || !endpoints.Any())
{
throw new ArgumentNullException(nameof(endpoints));
@@ -416,12 +433,40 @@ public AzureAppConfigurationOptions Connect(IEnumerable endpoints, TokenCre
}
Credential = credential ?? throw new ArgumentNullException(nameof(credential));
-
Endpoints = endpoints;
ConnectionStrings = null;
return this;
}
+ ///
+ /// Connect the provider to Azure Front Door endpoint.
+ ///
+ /// The endpoint of the Azure Front Door instance to connect to.
+ public AzureAppConfigurationOptions ConnectAzureFrontDoor(Uri endpoint)
+ {
+ if ((Credential != null && !(Credential is EmptyTokenCredential)) || (ConnectionStrings?.Any() ?? false))
+ {
+ throw new InvalidOperationException(ErrorMessages.ConnectionConflict);
+ }
+
+ if (IsAfdUsed)
+ {
+ throw new InvalidOperationException(ErrorMessages.AfdConnectionConflict);
+ }
+
+ if (endpoint == null)
+ {
+ throw new ArgumentNullException(nameof(endpoint));
+ }
+
+ Credential ??= new EmptyTokenCredential();
+
+ Endpoints = new Uri[] { endpoint };
+ ConnectionStrings = null;
+ IsAfdUsed = true;
+ return this;
+ }
+
///
/// Trims the provided prefix from the keys of all key-values retrieved from Azure App Configuration.
///
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
index fc92d1290..b2a79db3d 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
@@ -35,8 +35,8 @@ internal class AzureAppConfigurationProvider : ConfigurationProvider, IConfigura
private Dictionary _mappedData;
private Dictionary _watchedIndividualKvs = new Dictionary();
private HashSet _ffKeys = new HashSet();
- private Dictionary> _kvEtags = new Dictionary>();
- private Dictionary> _ffEtags = new Dictionary>();
+ private Dictionary> _watchedKvPages = new Dictionary>();
+ private Dictionary> _watchedFfPages = new Dictionary>();
private RequestTracingOptions _requestTracingOptions;
private Dictionary _configClientBackoffs = new Dictionary();
private DateTimeOffset _nextCollectionRefreshTime;
@@ -276,14 +276,14 @@ public async Task RefreshAsync(CancellationToken cancellationToken)
//
// Avoid instance state modification
- Dictionary> kvEtags = null;
- Dictionary> ffEtags = null;
+ Dictionary> kvEtags = null;
+ Dictionary> ffEtags = null;
HashSet ffKeys = null;
Dictionary watchedIndividualKvs = null;
- List keyValueChanges = null;
+ List watchedIndividualKvChanges = null;
Dictionary data = null;
Dictionary ffCollectionData = null;
- bool ffCollectionUpdated = false;
+ bool refreshFeatureFlag = false;
bool refreshAll = false;
StringBuilder logInfoBuilder = new StringBuilder();
StringBuilder logDebugBuilder = new StringBuilder();
@@ -294,10 +294,10 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) =>
ffEtags = null;
ffKeys = null;
watchedIndividualKvs = null;
- keyValueChanges = new List();
+ watchedIndividualKvChanges = new List();
data = null;
ffCollectionData = null;
- ffCollectionUpdated = false;
+ refreshFeatureFlag = false;
refreshAll = false;
logDebugBuilder.Clear();
logInfoBuilder.Clear();
@@ -305,12 +305,11 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) =>
if (_options.RegisterAllEnabled)
{
- // Get key value collection changes if RegisterAll was called
if (isRefreshDue)
{
refreshAll = await HaveCollectionsChanged(
_options.Selectors.Where(selector => !selector.IsFeatureFlagSelector),
- _kvEtags,
+ _watchedKvPages,
client,
cancellationToken).ConfigureAwait(false);
}
@@ -319,7 +318,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) =>
{
refreshAll = await RefreshIndividualKvWatchers(
client,
- keyValueChanges,
+ watchedIndividualKvChanges,
refreshableIndividualKvWatchers,
endpoint,
logDebugBuilder,
@@ -331,18 +330,21 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) =>
{
// Trigger a single load-all operation if a change was detected in one or more key-values with refreshAll: true,
// or if any key-value collection change was detected.
- kvEtags = new Dictionary>();
- ffEtags = new Dictionary>();
+ kvEtags = new Dictionary>();
+ ffEtags = new Dictionary>();
ffKeys = new HashSet();
data = await LoadSelected(client, kvEtags, ffEtags, _options.Selectors, ffKeys, cancellationToken).ConfigureAwait(false);
- watchedIndividualKvs = await LoadKeyValuesRegisteredForRefresh(client, data, cancellationToken).ConfigureAwait(false);
+
+ watchedIndividualKvs = await LoadIndividualWatchedSettings(client, data, cancellationToken).ConfigureAwait(false);
+
logInfoBuilder.AppendLine(LogHelper.BuildConfigurationUpdatedMessage());
+
return;
}
// Get feature flag changes
- ffCollectionUpdated = await HaveCollectionsChanged(
+ refreshFeatureFlag = await HaveCollectionsChanged(
refreshableFfWatchers.Select(watcher => new KeyValueSelector
{
KeyFilter = watcher.Key,
@@ -350,18 +352,18 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) =>
TagFilters = watcher.Tags,
IsFeatureFlagSelector = true
}),
- _ffEtags,
+ _watchedFfPages,
client,
cancellationToken).ConfigureAwait(false);
- if (ffCollectionUpdated)
+ if (refreshFeatureFlag)
{
- ffEtags = new Dictionary>();
+ ffEtags = new Dictionary>();
ffKeys = new HashSet();
ffCollectionData = await LoadSelected(
client,
- new Dictionary>(),
+ new Dictionary>(),
ffEtags,
_options.Selectors.Where(selector => selector.IsFeatureFlagSelector),
ffKeys,
@@ -397,9 +399,9 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) =>
{
watchedIndividualKvs = new Dictionary(_watchedIndividualKvs);
- await ProcessKeyValueChangesAsync(keyValueChanges, _mappedData, watchedIndividualKvs).ConfigureAwait(false);
+ await ProcessKeyValueChangesAsync(watchedIndividualKvChanges, _mappedData, watchedIndividualKvs).ConfigureAwait(false);
- if (ffCollectionUpdated)
+ if (refreshFeatureFlag)
{
// Remove all feature flag keys that are not present in the latest loading of feature flags, but were loaded previously
foreach (string key in _ffKeys.Except(ffKeys))
@@ -428,13 +430,13 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) =>
_nextCollectionRefreshTime = DateTimeOffset.UtcNow.Add(_options.KvCollectionRefreshInterval);
}
- if (_options.Adapters.Any(adapter => adapter.NeedsRefresh()) || keyValueChanges.Any() || refreshAll || ffCollectionUpdated)
+ if (_options.Adapters.Any(adapter => adapter.NeedsRefresh()) || watchedIndividualKvChanges.Any() || refreshAll || refreshFeatureFlag)
{
_watchedIndividualKvs = watchedIndividualKvs ?? _watchedIndividualKvs;
- _ffEtags = ffEtags ?? _ffEtags;
+ _watchedFfPages = ffEtags ?? _watchedFfPages;
- _kvEtags = kvEtags ?? _kvEtags;
+ _watchedKvPages = kvEtags ?? _watchedKvPages;
_ffKeys = ffKeys ?? _ffKeys;
@@ -770,8 +772,8 @@ private async Task TryInitializeAsync(IEnumerable cli
private async Task InitializeAsync(IEnumerable clients, CancellationToken cancellationToken = default)
{
Dictionary data = null;
- Dictionary> kvEtags = new Dictionary>();
- Dictionary> ffEtags = new Dictionary>();
+ Dictionary> kvEtags = new Dictionary>();
+ Dictionary> ffEtags = new Dictionary>();
Dictionary watchedIndividualKvs = null;
HashSet ffKeys = new HashSet();
@@ -788,7 +790,7 @@ await ExecuteWithFailOverPolicyAsync(
cancellationToken)
.ConfigureAwait(false);
- watchedIndividualKvs = await LoadKeyValuesRegisteredForRefresh(
+ watchedIndividualKvs = await LoadIndividualWatchedSettings(
client,
data,
cancellationToken)
@@ -821,8 +823,8 @@ await ExecuteWithFailOverPolicyAsync(
SetData(await PrepareData(mappedData, cancellationToken).ConfigureAwait(false));
_mappedData = mappedData;
- _kvEtags = kvEtags;
- _ffEtags = ffEtags;
+ _watchedKvPages = kvEtags;
+ _watchedFfPages = ffEtags;
_watchedIndividualKvs = watchedIndividualKvs;
_ffKeys = ffKeys;
}
@@ -830,8 +832,8 @@ await ExecuteWithFailOverPolicyAsync(
private async Task> LoadSelected(
ConfigurationClient client,
- Dictionary> kvEtags,
- Dictionary> ffEtags,
+ Dictionary> kvPageWatchers,
+ Dictionary> ffPageWatchers,
IEnumerable selectors,
HashSet ffKeys,
CancellationToken cancellationToken)
@@ -856,7 +858,7 @@ private async Task> LoadSelected(
}
}
- var matchConditions = new List();
+ var pageWatchers = new List();
await CallWithRequestTracing(async () =>
{
@@ -864,7 +866,8 @@ await CallWithRequestTracing(async () =>
await foreach (Page page in pageableSettings.AsPages(_options.ConfigurationSettingPageIterator).ConfigureAwait(false))
{
- using Response response = page.GetRawResponse();
+ using Response rawResponse = page.GetRawResponse();
+ DateTimeOffset serverResponseTime = rawResponse.GetMsDate();
foreach (ConfigurationSetting setting in page.Values)
{
@@ -903,17 +906,21 @@ await CallWithRequestTracing(async () =>
// The ETag will never be null here because it's not a conditional request
// Each successful response should have 200 status code and an ETag
- matchConditions.Add(new MatchConditions { IfNoneMatch = response.Headers.ETag });
+ pageWatchers.Add(new WatchedPage()
+ {
+ MatchConditions = new MatchConditions { IfNoneMatch = rawResponse.Headers.ETag },
+ LastServerResponseTime = serverResponseTime
+ });
}
}).ConfigureAwait(false);
if (loadOption.IsFeatureFlagSelector)
{
- ffEtags[loadOption] = matchConditions;
+ ffPageWatchers[loadOption] = pageWatchers;
}
else
{
- kvEtags[loadOption] = matchConditions;
+ kvPageWatchers[loadOption] = pageWatchers;
}
}
else
@@ -968,24 +975,27 @@ await CallWithRequestTracing(async () =>
return resolvedSettings;
}
- private async Task> LoadKeyValuesRegisteredForRefresh(
+ private async Task> LoadIndividualWatchedSettings(
ConfigurationClient client,
IDictionary existingSettings,
CancellationToken cancellationToken)
{
- var watchedIndividualKvs = new Dictionary();
+ var watchedIndividualKvs = new Dictionary(_watchedIndividualKvs);
+
+ Debug.Assert(!_options.IsAfdUsed || !_options.IndividualKvWatchers.Any());
foreach (KeyValueWatcher kvWatcher in _options.IndividualKvWatchers)
{
string watchedKey = kvWatcher.Key;
string watchedLabel = kvWatcher.Label;
- KeyValueIdentifier watchedKeyLabel = new KeyValueIdentifier(watchedKey, watchedLabel);
+ var watchedKeyLabel = new KeyValueIdentifier(watchedKey, watchedLabel);
// Skip the loading for the key-value in case it has already been loaded
if (existingSettings.TryGetValue(watchedKey, out ConfigurationSetting loadedKv)
&& watchedKeyLabel.Equals(new KeyValueIdentifier(loadedKv.Key, loadedKv.Label)))
{
+ // create a new instance to avoid that reference could be modified when mapping data
watchedIndividualKvs[watchedKeyLabel] = new ConfigurationSetting(loadedKv.Key, loadedKv.Value, loadedKv.Label, loadedKv.ETag);
continue;
}
@@ -1004,6 +1014,7 @@ private async Task> LoadKey
// If the key-value was found, store it for updating the settings
if (watchedKv != null)
{
+ // create a new instance to avoid that reference could be modified when mapping data
watchedIndividualKvs[watchedKeyLabel] = new ConfigurationSetting(watchedKv.Key, watchedKv.Value, watchedKv.Label, watchedKv.ETag);
if (watchedKv.ContentType == SnapshotReferenceConstants.ContentType)
@@ -1047,6 +1058,8 @@ private async Task RefreshIndividualKvWatchers(
StringBuilder logInfoBuilder,
CancellationToken cancellationToken)
{
+ Debug.Assert(!_options.IsAfdUsed || !_options.IndividualKvWatchers.Any());
+
foreach (KeyValueWatcher kvWatcher in refreshableIndividualKvWatchers)
{
string watchedKey = kvWatcher.Key;
@@ -1060,17 +1073,23 @@ private async Task RefreshIndividualKvWatchers(
// Find if there is a change associated with watcher
if (_watchedIndividualKvs.TryGetValue(watchedKeyLabel, out ConfigurationSetting watchedKv))
{
- await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions,
- async () => change = await client.GetKeyValueChange(watchedKv, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
+ await CallWithRequestTracing(async () =>
+ change = await client.GetKeyValueChange(
+ watchedKv,
+ cancellationToken).ConfigureAwait(false)
+ ).ConfigureAwait(false);
}
else
{
// Load the key-value in case the previous load attempts had failed
-
try
{
- await CallWithRequestTracing(
- async () => watchedKv = await client.GetConfigurationSettingAsync(watchedKey, watchedLabel, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
+ await CallWithRequestTracing(async () =>
+ watchedKv = await client.GetConfigurationSettingAsync(
+ watchedKey,
+ watchedLabel,
+ cancellationToken).ConfigureAwait(false)
+ ).ConfigureAwait(false);
}
catch (RequestFailedException e) when (e.Status == (int)HttpStatusCode.NotFound)
{
@@ -1164,7 +1183,8 @@ private void SetRequestTracingOptions()
IsKeyVaultConfigured = _options.IsKeyVaultConfigured,
IsKeyVaultRefreshConfigured = _options.IsKeyVaultRefreshConfigured,
FeatureFlagTracing = _options.FeatureFlagTracing,
- IsLoadBalancingEnabled = _options.LoadBalancingEnabled
+ IsLoadBalancingEnabled = _options.LoadBalancingEnabled,
+ IsAfdUsed = _options.IsAfdUsed
};
}
@@ -1431,7 +1451,7 @@ private void UpdateClientBackoffStatus(Uri endpoint, bool successful)
private async Task HaveCollectionsChanged(
IEnumerable selectors,
- Dictionary> pageEtags,
+ Dictionary> pageWatchers,
ConfigurationClient client,
CancellationToken cancellationToken)
{
@@ -1439,19 +1459,20 @@ private async Task HaveCollectionsChanged(
foreach (KeyValueSelector selector in selectors)
{
- if (pageEtags.TryGetValue(selector, out IEnumerable matchConditions))
+ if (pageWatchers.TryGetValue(selector, out IEnumerable watchers))
{
await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions,
async () => haveCollectionsChanged = await client.HaveCollectionsChanged(
selector,
- matchConditions,
+ watchers,
_options.ConfigurationSettingPageIterator,
+ makeConditionalRequest: !_options.IsAfdUsed,
cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
- }
- if (haveCollectionsChanged)
- {
- return true;
+ if (haveCollectionsChanged)
+ {
+ return true;
+ }
}
}
@@ -1466,6 +1487,7 @@ private async Task ProcessKeyValueChangesAsync(
foreach (KeyValueChange change in keyValueChanges)
{
KeyValueIdentifier changeIdentifier = new KeyValueIdentifier(change.Key, change.Label);
+ Debug.Assert(watchedIndividualKvs.ContainsKey(changeIdentifier));
if (change.ChangeType == KeyValueChangeType.Modified)
{
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs
index 83d20e2fb..230b99cba 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs
@@ -1,8 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
+using Azure.Core;
using Azure.Data.AppConfiguration;
using Microsoft.Extensions.Azure;
+using Microsoft.Extensions.Configuration.AzureAppConfiguration.Afd;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -34,13 +36,36 @@ public IConfigurationProvider Build(IConfigurationBuilder builder)
{
AzureAppConfigurationOptions options = _optionsProvider();
+ IAzureClientFactory clientFactory = options.ClientFactory;
+
+ if (options.IsAfdUsed)
+ {
+ if (options.LoadBalancingEnabled)
+ {
+ throw new InvalidOperationException(ErrorMessages.AfdLoadBalancingUnsupported);
+ }
+
+ if (clientFactory != null)
+ {
+ throw new InvalidOperationException(ErrorMessages.AfdCustomClientFactoryUnsupported);
+ }
+
+ if (options.IndividualKvWatchers.Any())
+ {
+ throw new InvalidOperationException($"Registering individual keys for refresh via `{nameof(AzureAppConfigurationRefreshOptions)}.{nameof(AzureAppConfigurationRefreshOptions.Register)}` is not supported when connecting to Azure Front Door. Instead, to enable configuration refresh, use `{nameof(AzureAppConfigurationRefreshOptions)}.{nameof(AzureAppConfigurationRefreshOptions.RegisterAll)}`.");
+ }
+
+ options.ReplicaDiscoveryEnabled = false;
+
+ options.ClientOptions.AddPolicy(new AfdPolicy(), HttpPipelinePosition.PerRetry);
+ }
+
if (options.ClientManager != null)
{
return new AzureAppConfigurationProvider(options.ClientManager, options, _optional);
}
IEnumerable endpoints;
- IAzureClientFactory clientFactory = options.ClientFactory;
if (options.ConnectionStrings != null)
{
@@ -56,10 +81,17 @@ public IConfigurationProvider Build(IConfigurationBuilder builder)
}
else
{
- throw new ArgumentException($"Please call {nameof(AzureAppConfigurationOptions)}.{nameof(AzureAppConfigurationOptions.Connect)} to specify how to connect to Azure App Configuration.");
+ throw new ArgumentException($"Please call {nameof(AzureAppConfigurationOptions)}.{nameof(AzureAppConfigurationOptions.Connect)} or {nameof(AzureAppConfigurationOptions)}.{nameof(AzureAppConfigurationOptions.ConnectAzureFrontDoor)} to specify how to connect to Azure App Configuration.");
}
- provider = new AzureAppConfigurationProvider(new ConfigurationClientManager(clientFactory, endpoints, options.ReplicaDiscoveryEnabled, options.LoadBalancingEnabled), options, _optional);
+ if (options.IsAfdUsed)
+ {
+ provider = new AzureAppConfigurationProvider(new AfdConfigurationClientManager(clientFactory, endpoints.First()), options, _optional);
+ }
+ else
+ {
+ provider = new AzureAppConfigurationProvider(new ConfigurationClientManager(clientFactory, endpoints, options.ReplicaDiscoveryEnabled, options.LoadBalancingEnabled), options, _optional);
+ }
}
catch (InvalidOperationException ex) // InvalidOperationException is thrown when any problems are found while configuring AzureAppConfigurationOptions or when SDK fails to create a configurationClient.
{
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
index 7bc0e84f9..7b6e0acdd 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
@@ -14,5 +14,9 @@ internal class ErrorMessages
public const string SnapshotReferenceInvalidJsonProperty = "Invalid snapshot reference format for key '{0}' (label: '{1}'). The '{2}' property must be a string value, but found {3}.";
public const string SnapshotReferencePropertyMissing = "Invalid snapshot reference format for key '{0}' (label: '{1}'). The '{2}' property is required.";
public const string SnapshotInvalidComposition = "{0} for the selected snapshot with name '{1}' must be 'key', found '{2}'.";
+ public const string ConnectionConflict = "Cannot connect to both Azure App Configuration and Azure Front Door at the same time.";
+ public const string AfdConnectionConflict = "Cannot connect to multiple Azure Front Doors.";
+ public const string AfdLoadBalancingUnsupported = "Load balancing is not supported when connecting to Azure Front Door. For guidance on how to take advantage of geo-replication when Azure Front Door is used, visit https://aka.ms/appconfig/geo-replication-with-afd";
+ public const string AfdCustomClientFactoryUnsupported = "Custom client factory is not supported when connecting to Azure Front Door.";
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs
index 5c4df33ec..e3e7f6160 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs
@@ -37,6 +37,7 @@ internal class RequestTracingConstants
public const string SignalRUsedTag = "SignalR";
public const string FailoverRequestTag = "Failover";
public const string PushRefreshTag = "PushRefresh";
+ public const string AfdTag = "AFD";
public const string FeatureFlagFilterTypeKey = "Filter";
public const string CustomFilter = "CSTM";
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs
index c4edfb0ee..572a8b4e5 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs
@@ -6,6 +6,7 @@
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Models;
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
@@ -29,7 +30,8 @@ public static async Task GetKeyValueChange(this ConfigurationCli
try
{
Response response = await client.GetConfigurationSettingAsync(setting, onlyIfChanged: true, cancellationToken).ConfigureAwait(false);
- if (response.GetRawResponse().Status == (int)HttpStatusCode.OK &&
+ using Response rawResponse = response.GetRawResponse();
+ if (rawResponse.Status == (int)HttpStatusCode.OK &&
!response.Value.ETag.Equals(setting.ETag))
{
return new KeyValueChange
@@ -64,11 +66,17 @@ public static async Task GetKeyValueChange(this ConfigurationCli
};
}
- public static async Task HaveCollectionsChanged(this ConfigurationClient client, KeyValueSelector keyValueSelector, IEnumerable matchConditions, IConfigurationSettingPageIterator pageIterator, CancellationToken cancellationToken)
+ public static async Task HaveCollectionsChanged(
+ this ConfigurationClient client,
+ KeyValueSelector keyValueSelector,
+ IEnumerable pageWatchers,
+ IConfigurationSettingPageIterator pageIterator,
+ bool makeConditionalRequest,
+ CancellationToken cancellationToken)
{
- if (matchConditions == null)
+ if (pageWatchers == null)
{
- throw new ArgumentNullException(nameof(matchConditions));
+ throw new ArgumentNullException(nameof(pageWatchers));
}
if (keyValueSelector == null)
@@ -87,25 +95,31 @@ public static async Task HaveCollectionsChanged(this ConfigurationClient c
LabelFilter = keyValueSelector.LabelFilter
};
- AsyncPageable pageable = client.GetConfigurationSettingsAsync(selector, cancellationToken);
+ AsyncPageable pageable = client.CheckConfigurationSettingsAsync(selector, cancellationToken);
- using IEnumerator existingMatchConditionsEnumerator = matchConditions.GetEnumerator();
+ using IEnumerator existingPageWatcherEnumerator = pageWatchers.GetEnumerator();
- await foreach (Page page in pageable.AsPages(pageIterator, matchConditions).ConfigureAwait(false))
+ IAsyncEnumerable> pages = makeConditionalRequest
+ ? pageable.AsPages(pageIterator, pageWatchers.Select(p => p.MatchConditions))
+ : pageable.AsPages(pageIterator);
+
+ await foreach (Page page in pages.ConfigureAwait(false))
{
- using Response response = page.GetRawResponse();
+ using Response rawResponse = page.GetRawResponse();
+ DateTimeOffset serverResponseTime = rawResponse.GetMsDate();
- // Return true if the lists of etags are different
- if ((!existingMatchConditionsEnumerator.MoveNext() ||
- !existingMatchConditionsEnumerator.Current.IfNoneMatch.Equals(response.Headers.ETag)) &&
- response.Status == (int)HttpStatusCode.OK)
+ if (!existingPageWatcherEnumerator.MoveNext() ||
+ (rawResponse.Status == (int)HttpStatusCode.OK &&
+ // if the server response time is later than last server response time, the change is considered detected
+ serverResponseTime >= existingPageWatcherEnumerator.Current.LastServerResponseTime &&
+ !existingPageWatcherEnumerator.Current.MatchConditions.IfNoneMatch.Equals(rawResponse.Headers.ETag)))
{
return true;
}
}
- // Need to check if pages were deleted and no change was found within the new shorter list of match conditions
- return existingMatchConditionsEnumerator.MoveNext();
+ // Need to check if pages were deleted and no change was found within the new shorter list of page
+ return existingPageWatcherEnumerator.MoveNext();
}
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ResponseExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ResponseExtensions.cs
new file mode 100644
index 000000000..9cf8833ae
--- /dev/null
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ResponseExtensions.cs
@@ -0,0 +1,22 @@
+using Azure;
+using Azure.Core;
+using System;
+
+namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions
+{
+ internal static class ResponseExtensions
+ {
+ public static DateTimeOffset GetMsDate(this Response response)
+ {
+ if (response.Headers.TryGetValue(HttpHeader.Names.XMsDate, out string value))
+ {
+ if (DateTimeOffset.TryParse(value, out DateTimeOffset date))
+ {
+ return date;
+ }
+ }
+
+ return DateTimeOffset.UtcNow;
+ }
+ }
+}
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..d74e726be 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-preview
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs
index 2f50a3856..d6e32ddc0 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs
@@ -3,7 +3,6 @@
//
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
-using Microsoft.Extensions.Configuration.AzureAppConfiguration.SnapshotReference;
using System.Net.Mime;
using System.Text;
@@ -76,6 +75,11 @@ internal class RequestTracingOptions
///
public bool IsPushRefreshUsed { get; set; } = false;
+ ///
+ /// Flag to indicate wether the request is sent to a AFD.
+ ///
+ public bool IsAfdUsed { get; set; } = false;
+
///
/// Flag to indicate whether any key-value uses the json content type and contains
/// a parameter indicating an AI profile.
@@ -132,7 +136,8 @@ public bool UsesAnyTracingFeature()
IsSignalRUsed ||
UsesAIConfiguration ||
UsesAIChatCompletionConfiguration ||
- UsesSnapshotReference;
+ UsesSnapshotReference ||
+ IsAfdUsed;
}
///
@@ -193,6 +198,16 @@ public string CreateFeaturesString()
sb.Append(RequestTracingConstants.SnapshotReferenceTag);
}
+ if (IsAfdUsed)
+ {
+ if (sb.Length > 0)
+ {
+ sb.Append(RequestTracingConstants.Delimiter);
+ }
+
+ sb.Append(RequestTracingConstants.AfdTag);
+ }
+
return sb.ToString();
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/WatchedPage.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/WatchedPage.cs
new file mode 100644
index 000000000..5fd14a444
--- /dev/null
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/WatchedPage.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Azure;
+using System;
+
+namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
+{
+ internal class WatchedPage
+ {
+ public MatchConditions MatchConditions { get; set; }
+ public DateTimeOffset LastServerResponseTime { get; set; }
+ }
+}
diff --git a/tests/Tests.AzureAppConfiguration/Azure.Core.Testing/MockResponse.cs b/tests/Tests.AzureAppConfiguration/Azure.Core.Testing/MockResponse.cs
index c60c2a255..a205f0888 100644
--- a/tests/Tests.AzureAppConfiguration/Azure.Core.Testing/MockResponse.cs
+++ b/tests/Tests.AzureAppConfiguration/Azure.Core.Testing/MockResponse.cs
@@ -13,15 +13,17 @@ public class MockResponse : Response
{
private readonly Dictionary> _headers = new Dictionary>(StringComparer.OrdinalIgnoreCase);
- public MockResponse(int status, string reasonPhrase = null)
+ public MockResponse(int status, string etag = null, DateTimeOffset? date = null, string reasonPhrase = null)
{
Status = status;
ReasonPhrase = reasonPhrase;
if (status == 200)
{
- AddHeader(new HttpHeader(HttpHeader.Names.ETag, "\"" + Guid.NewGuid().ToString() + "\""));
+ AddHeader(new HttpHeader(HttpHeader.Names.ETag, "\"" + (etag ?? Guid.NewGuid().ToString()) + "\""));
}
+
+ AddHeader(new HttpHeader(HttpHeader.Names.XMsDate, date?.ToString() ?? DateTimeOffset.UtcNow.ToString()));
}
public override int Status { get; }
diff --git a/tests/Tests.AzureAppConfiguration/Unit/AfdTests.cs b/tests/Tests.AzureAppConfiguration/Unit/AfdTests.cs
new file mode 100644
index 000000000..1d2250291
--- /dev/null
+++ b/tests/Tests.AzureAppConfiguration/Unit/AfdTests.cs
@@ -0,0 +1,426 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Azure;
+using Azure.Core.Testing;
+using Azure.Data.AppConfiguration;
+using Azure.Identity;
+using Microsoft.Extensions.Azure;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Configuration.AzureAppConfiguration;
+using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement; // Added for feature flag constants
+using Moq;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Tests.AzureAppConfiguration
+{
+ public class AfdTests
+ {
+ List _kvCollection = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting("TestKey1", "TestValue1", "label",
+ eTag: new ETag("0a76e3d7-7ec1-4e37-883c-9ea6d0d89e63"),
+ contentType:"text"),
+ ConfigurationModelFactory.ConfigurationSetting("TestKey2", "TestValue2", "label",
+ eTag: new ETag("31c38369-831f-4bf1-b9ad-79db56c8b989"),
+ contentType: "text"),
+ ConfigurationModelFactory.ConfigurationSetting("TestKey3", "TestValue3", "label",
+ eTag: new ETag("bb203f2b-c113-44fc-995d-b933c2143339"),
+ contentType: "text"),
+ ConfigurationModelFactory.ConfigurationSetting("TestKey4", "TestValue4", "label",
+ eTag: new ETag("3ca43b3e-d544-4b0c-b3a2-e7a7284217a2"),
+ contentType: "text")
+ };
+
+ private class TestClientFactory : IAzureClientFactory
+ {
+ public ConfigurationClient CreateClient(string name)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ [Fact]
+ public void AfdTests_ConnectThrowsAfterConnectAzureFrontDoor()
+ {
+ var afdEndpoint = new Uri("https://test.b01.azurefd.net");
+ var endpoint = new Uri("https://fake-endpoint.azconfig.io");
+ var connectionString = "Endpoint=https://fake-endpoint.azconfig.io;Id=test;Secret=123456";
+ var builder = new ConfigurationBuilder();
+ var exception = Record.Exception(() =>
+ {
+ builder.AddAzureAppConfiguration(options =>
+ {
+ options.ConnectAzureFrontDoor(afdEndpoint);
+ options.Connect(endpoint, new DefaultAzureCredential());
+ });
+ builder.Build();
+ });
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
+ Assert.IsType(exception.InnerException);
+ Assert.Equal(ErrorMessages.ConnectionConflict, exception.InnerException.Message);
+
+ exception = Record.Exception(() =>
+ {
+ builder.AddAzureAppConfiguration(options =>
+ {
+ options.ConnectAzureFrontDoor(afdEndpoint);
+ options.Connect(connectionString);
+ });
+ builder.Build();
+ });
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
+ Assert.IsType(exception.InnerException);
+ Assert.Equal(ErrorMessages.ConnectionConflict, exception.InnerException.Message);
+ }
+
+ [Fact]
+ public void AfdTests_ConnectAzureFrontDoorThrowsAfterConnect()
+ {
+ var afdEndpoint = new Uri("https://test.b01.azurefd.net");
+ var endpoint = new Uri("https://fake-endpoint.azconfig.io");
+ var connectionString = "Endpoint=https://fake-endpoint.azconfig.io;Id=test;Secret=123456";
+ var builder = new ConfigurationBuilder();
+ var exception = Record.Exception(() =>
+ {
+ builder.AddAzureAppConfiguration(options =>
+ {
+ options.Connect(endpoint, new DefaultAzureCredential());
+ options.ConnectAzureFrontDoor(afdEndpoint);
+ });
+ builder.Build();
+ });
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
+ Assert.IsType(exception.InnerException);
+ Assert.Equal(ErrorMessages.ConnectionConflict, exception.InnerException.Message);
+
+ exception = Record.Exception(() =>
+ {
+ builder.AddAzureAppConfiguration(options =>
+ {
+ options.Connect(connectionString);
+ options.ConnectAzureFrontDoor(afdEndpoint);
+ });
+ builder.Build();
+ });
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
+ Assert.IsType(exception.InnerException);
+ Assert.Equal(ErrorMessages.ConnectionConflict, exception.InnerException.Message);
+ }
+
+ [Fact]
+ public void AfdTests_ThrowsWhenConnectMultipleAzureFrontDoor()
+ {
+ var afdEndpoint = new Uri("https://test.b01.azurefd.net");
+ var afdEndpoint2 = new Uri("https://test.b02.azurefd.net");
+ var builder = new ConfigurationBuilder();
+ var exception = Record.Exception(() =>
+ {
+ builder.AddAzureAppConfiguration(options =>
+ {
+ options.ConnectAzureFrontDoor(afdEndpoint);
+ options.ConnectAzureFrontDoor(afdEndpoint2);
+ });
+ builder.Build();
+ });
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
+ Assert.IsType(exception.InnerException);
+ Assert.Equal(ErrorMessages.AfdConnectionConflict, exception.InnerException.Message);
+ }
+
+ [Fact]
+ public void AfdTests_WatchedSettingIsUnsupportedWhenConnectAzureFrontDoor()
+ {
+ var afdEndpoint = new Uri("https://test.b01.azurefd.net");
+ var builder = new ConfigurationBuilder();
+ var exception = Record.Exception(() =>
+ {
+ builder.AddAzureAppConfiguration(options =>
+ {
+ options.ConnectAzureFrontDoor(afdEndpoint);
+ options.ConfigureRefresh(refreshOptions =>
+ {
+ refreshOptions.Register("TestKey1", "label", true);
+ });
+ });
+ builder.Build();
+ });
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
+ Assert.IsType(exception.InnerException);
+ Assert.Contains("Registering individual keys for refresh via `AzureAppConfigurationRefreshOptions.Register` is not supported when connecting to Azure Front Door.", exception.InnerException.Message);
+ }
+
+ [Fact]
+ public void AfdTests_LoadbalancingIsUnsupportedWhenConnectAzureFrontDoor()
+ {
+ var afdEndpoint = new Uri("https://test.b01.azurefd.net");
+ var builder = new ConfigurationBuilder();
+ var exception = Record.Exception(() =>
+ {
+ builder.AddAzureAppConfiguration(options =>
+ {
+ options.ConnectAzureFrontDoor(afdEndpoint);
+ options.LoadBalancingEnabled = true;
+ });
+ builder.Build();
+ });
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
+ Assert.IsType(exception.InnerException);
+ Assert.Equal(ErrorMessages.AfdLoadBalancingUnsupported, exception.InnerException.Message);
+ }
+
+ [Fact]
+ public void AfdTests_CustomClientOptionsNotSupported()
+ {
+ var afdEndpoint = new Uri("https://test.b01.azurefd.net");
+ var builder = new ConfigurationBuilder();
+ var exception = Record.Exception(() =>
+ {
+ builder.AddAzureAppConfiguration(options =>
+ {
+ options.ConnectAzureFrontDoor(afdEndpoint);
+ options.SetClientFactory(new TestClientFactory());
+ });
+ builder.Build();
+ });
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
+ Assert.IsType(exception.InnerException);
+ Assert.Equal(ErrorMessages.AfdCustomClientFactoryUnsupported, exception.InnerException.Message);
+ }
+
+ [Fact]
+ public async Task AfdTests_RegisterAllRefresh()
+ {
+ var mockClient = new Mock(MockBehavior.Strict);
+
+ var keyValueCollection1 = new List(_kvCollection);
+ string page1_etag = Guid.NewGuid().ToString();
+ string page2_etag = Guid.NewGuid().ToString();
+ var responses = new List()
+ {
+ new MockResponse(200, page1_etag, DateTimeOffset.Parse("2025-10-17T09:00:00+08:00")),
+ new MockResponse(200, page2_etag, DateTimeOffset.Parse("2025-10-17T09:00:00+08:00"))
+ };
+ var mockAsyncPageable1 = new MockAsyncPageable(keyValueCollection1, null, 3, responses);
+
+ var keyValueCollection2 = new List(_kvCollection);
+ keyValueCollection2[3].Value = "old-value";
+ string page2_etag2 = Guid.NewGuid().ToString();
+ var responses2 = new List()
+ {
+ new MockResponse(200, page1_etag, DateTimeOffset.Parse("2025-10-17T09:00:00+08:00")),
+ new MockResponse(200, page2_etag2, DateTimeOffset.Parse("2025-10-17T08:59:59+08:00")) // stale, should not refresh
+ };
+ var mockAsyncPageable2 = new MockAsyncPageable(keyValueCollection2, null, 3, responses2);
+
+ var keyValueCollection3 = new List(_kvCollection);
+ keyValueCollection3[3].Value = "new-value";
+ var responses3 = new List()
+ {
+ new MockResponse(200, page1_etag, DateTimeOffset.Parse("2025-10-17T09:00:00+08:00")), // up-to-date, should refresh
+ new MockResponse(200, page2_etag2, DateTimeOffset.Parse("2025-10-17T09:00:02+08:00"))
+ };
+ var mockAsyncPageable3 = new MockAsyncPageable(keyValueCollection3, null, 3, responses3);
+
+ var responses4 = new List()
+ {
+ new MockResponse(200, page1_etag, DateTimeOffset.Parse("2025-10-17T09:00:03+08:00")), // up-to-date
+ new MockResponse(200, page2_etag2, DateTimeOffset.Parse("2025-10-17T09:00:03+08:00"))
+ };
+ var mockAsyncPageable4 = new MockAsyncPageable(keyValueCollection3, null, 3, responses4);
+
+ mockClient.SetupSequence(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns(mockAsyncPageable1) // initial load
+ .Returns(mockAsyncPageable3); // reload after change detected
+
+ mockClient.SetupSequence(c => c.CheckConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns(mockAsyncPageable2) // first check - stale, should not refresh
+ .Returns(mockAsyncPageable3); // second check - should trigger refresh
+
+ var afdEndpoint = new Uri("https://test.b01.azurefd.net");
+ IConfigurationRefresher refresher = null;
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ConnectAzureFrontDoor(afdEndpoint);
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.ConfigurationSettingPageIterator = new MockConfigurationSettingPageIterator();
+ options.Select("TestKey*", "label");
+ options.ConfigureRefresh(refreshOptions =>
+ {
+ refreshOptions.RegisterAll()
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
+ });
+ refresher = options.GetRefresher();
+ })
+ .Build();
+
+ Assert.Equal("TestValue4", config["TestKey4"]);
+
+ await Task.Delay(1500);
+
+ await refresher.RefreshAsync();
+
+ Assert.Equal("TestValue4", config["TestKey4"]); // should not refresh, because page 2 is stale
+
+ await Task.Delay(1500);
+
+ await refresher.RefreshAsync();
+
+ Assert.Equal("new-value", config["TestKey4"]);
+ }
+
+ [Fact]
+ public async Task AfdTests_FeatureFlagsRefresh()
+ {
+ var mockClient = new Mock(MockBehavior.Strict);
+
+ var featureFlag = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "BetaFlag",
+ value: @"
+ {
+ ""id"": ""BetaFlag"",
+ ""enabled"": true,
+ ""conditions"": {
+ ""client_filters"": [
+ {
+ ""name"": ""Browser"",
+ ""parameters"": {
+ ""AllowedBrowsers"": [ ""Firefox"", ""Safari"" ]
+ }
+ }
+ ]
+ }
+ }",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag(Guid.NewGuid().ToString()))
+ };
+
+ var staleFeatureFlag = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "BetaFlag",
+ value: @"
+ {
+ ""id"": ""BetaFlag"",
+ ""enabled"": true,
+ ""conditions"": {
+ ""client_filters"": [
+ {
+ ""name"": ""Browser"",
+ ""parameters"": {
+ ""AllowedBrowsers"": [ ""360"" ]
+ }
+ }
+ ]
+ }
+ }",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag(Guid.NewGuid().ToString()))
+ };
+
+ var newFeatureFlag = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "BetaFlag",
+ value: @"
+ {
+ ""id"": ""BetaFlag"",
+ ""enabled"": true,
+ ""conditions"": {
+ ""client_filters"": [
+ {
+ ""name"": ""Browser"",
+ ""parameters"": {
+ ""AllowedBrowsers"": [ ""Chrome"", ""Edge"" ]
+ }
+ }
+ ]
+ }
+ }",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag(Guid.NewGuid().ToString()))
+ };
+
+ string etag1 = Guid.NewGuid().ToString();
+ var responses = new List()
+ {
+ new MockResponse(200, etag1, DateTimeOffset.Parse("2025-10-17T09:00:00+08:00"))
+ };
+ var mockAsyncPageable1 = new MockAsyncPageable(featureFlag, null, 10, responses);
+
+ string etag2 = Guid.NewGuid().ToString();
+ var responses2 = new List()
+ {
+ new MockResponse(200, etag2, DateTimeOffset.Parse("2025-10-17T08:59:59+08:00"))
+ };
+ var mockAsyncPageable2 = new MockAsyncPageable(staleFeatureFlag, null, 10, responses);
+
+ string etag3 = Guid.NewGuid().ToString();
+ var responses3 = new List()
+ {
+ new MockResponse(200, etag3, DateTimeOffset.Parse("2025-10-17T09:00:02+08:00"))
+ };
+ var mockAsyncPageable3 = new MockAsyncPageable(newFeatureFlag, null, 10, responses3);
+
+ mockClient.SetupSequence(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns(mockAsyncPageable1) // default load configuration settings
+ .Returns(mockAsyncPageable1) // load feature flag
+ .Returns(mockAsyncPageable3) // reload after change detected
+ .Returns(mockAsyncPageable3); // reload feature flags
+
+ mockClient.SetupSequence(c => c.CheckConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns(mockAsyncPageable2) // watch request, should not trigger refresh
+ .Returns(mockAsyncPageable3); // watch request, should trigger refresh
+
+ var afdEndpoint = new Uri("https://test.b01.azurefd.net");
+ IConfigurationRefresher refresher = null;
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ConnectAzureFrontDoor(afdEndpoint);
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.ConfigurationSettingPageIterator = new MockConfigurationSettingPageIterator();
+ options.UseFeatureFlags(o => o.SetRefreshInterval(TimeSpan.FromSeconds(1)));
+ refresher = options.GetRefresher();
+ })
+ .Build();
+
+ Assert.Equal("Browser", config["FeatureManagement:BetaFlag:EnabledFor:0:Name"]);
+ Assert.Equal("Firefox", config["FeatureManagement:BetaFlag:EnabledFor:0:Parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Safari", config["FeatureManagement:BetaFlag:EnabledFor:0:Parameters:AllowedBrowsers:1"]);
+
+ await Task.Delay(1500);
+
+ await refresher.RefreshAsync();
+
+ // Still old values because page timestamp was stale
+ Assert.Equal("Firefox", config["FeatureManagement:BetaFlag:EnabledFor:0:Parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Safari", config["FeatureManagement:BetaFlag:EnabledFor:0:Parameters:AllowedBrowsers:1"]);
+
+ await Task.Delay(1500);
+
+ await refresher.RefreshAsync();
+
+ Assert.Equal("Chrome", config["FeatureManagement:BetaFlag:EnabledFor:0:Parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Edge", config["FeatureManagement:BetaFlag:EnabledFor:0:Parameters:AllowedBrowsers:1"]);
+ }
+ }
+}
diff --git a/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs b/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs
index 57735f379..72b3a8260 100644
--- a/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs
+++ b/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs
@@ -2,6 +2,8 @@
// Licensed under the MIT license.
//
using Azure;
+using Azure.Core;
+using Azure.Core.Testing;
using Azure.Data.AppConfiguration;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
@@ -27,7 +29,6 @@ public async Task FailOverTests_ReturnsAllClientsIfAllBackedOff()
{
// Arrange
IConfigurationRefresher refresher = null;
- var mockResponse = new Mock();
var mockClient1 = new Mock();
mockClient1.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
@@ -96,7 +97,6 @@ public void FailOverTests_PropagatesNonFailOverableExceptions()
{
// Arrange
IConfigurationRefresher refresher = null;
- var mockResponse = new Mock();
var mockClient1 = new Mock();
mockClient1.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
@@ -148,7 +148,7 @@ public async Task FailOverTests_BackoffStateIsUpdatedOnSuccessfulRequest()
{
// Arrange
IConfigurationRefresher refresher = null;
- var mockResponse = new Mock();
+ var mockResponse = new MockResponse(200);
var mockClient1 = new Mock();
mockClient1.SetupSequence(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
@@ -156,11 +156,11 @@ public async Task FailOverTests_BackoffStateIsUpdatedOnSuccessfulRequest()
.Returns(new MockAsyncPageable(Enumerable.Empty().ToList()))
.Returns(new MockAsyncPageable(Enumerable.Empty().ToList()));
mockClient1.SetupSequence(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)));
+ .Returns(Task.FromResult(Response.FromValue(kv, mockResponse)))
+ .Returns(Task.FromResult(Response.FromValue(kv, mockResponse)));
mockClient1.SetupSequence(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)));
+ .Returns(Task.FromResult(Response.FromValue(kv, mockResponse)))
+ .Returns(Task.FromResult(Response.FromValue(kv, mockResponse)));
mockClient1.Setup(c => c.Equals(mockClient1)).Returns(true);
var mockClient2 = new Mock();
@@ -168,11 +168,11 @@ public async Task FailOverTests_BackoffStateIsUpdatedOnSuccessfulRequest()
.Returns(new MockAsyncPageable(Enumerable.Empty().ToList()))
.Returns(new MockAsyncPageable(Enumerable.Empty().ToList()));
mockClient2.SetupSequence(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)));
+ .Returns(Task.FromResult(Response.FromValue(kv, mockResponse)))
+ .Returns(Task.FromResult(Response.FromValue(kv, mockResponse)));
mockClient2.SetupSequence(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)));
+ .Returns(Task.FromResult(Response.FromValue(kv, mockResponse)))
+ .Returns(Task.FromResult(Response.FromValue(kv, mockResponse)));
mockClient2.Setup(c => c.Equals(mockClient2)).Returns(true);
ConfigurationClientWrapper cw1 = new ConfigurationClientWrapper(TestHelpers.PrimaryConfigStoreEndpoint, mockClient1.Object);
@@ -222,7 +222,7 @@ public void FailOverTests_AutoFailover()
{
// Arrange
IConfigurationRefresher refresher = null;
- var mockResponse = new Mock();
+ var mockResponse = new MockResponse(200);
var mockClient1 = new Mock();
mockClient1.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
@@ -237,9 +237,9 @@ public void FailOverTests_AutoFailover()
mockClient2.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns(new MockAsyncPageable(Enumerable.Empty().ToList()));
mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)));
+ .Returns(Task.FromResult(Response.FromValue(kv, mockResponse)));
mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)));
+ .Returns(Task.FromResult(Response.FromValue(kv, mockResponse)));
mockClient2.Setup(c => c.Equals(mockClient2)).Returns(true);
ConfigurationClientWrapper cw1 = new ConfigurationClientWrapper(TestHelpers.PrimaryConfigStoreEndpoint, mockClient1.Object);
@@ -340,9 +340,7 @@ public void FailOverTests_GetNoDynamicClient()
[Fact]
public void FailOverTests_NetworkTimeout()
{
- // Arrange
- IConfigurationRefresher refresher = null;
- var mockResponse = new Mock();
+ var mockResponse = new MockResponse(200);
var client1 = new ConfigurationClient(TestHelpers.CreateMockEndpointString(),
new ConfigurationClientOptions()
@@ -357,9 +355,9 @@ public void FailOverTests_NetworkTimeout()
mockClient2.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns(new MockAsyncPageable(Enumerable.Empty().ToList()));
mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)));
+ .Returns(Task.FromResult(Response.FromValue(kv, mockResponse)));
mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)));
+ .Returns(Task.FromResult(Response.FromValue(kv, mockResponse)));
mockClient2.Setup(c => c.Equals(mockClient2)).Returns(true);
ConfigurationClientWrapper cw1 = new ConfigurationClientWrapper(TestHelpers.PrimaryConfigStoreEndpoint, client1);
@@ -420,7 +418,7 @@ ae.InnerException is AggregateException ae2 &&
public async Task FailOverTests_AllClientsBackedOffAfterNonFailoverableException()
{
IConfigurationRefresher refresher = null;
- var mockResponse = new Mock();
+ var mockResponse = new MockResponse(200);
// Setup first client - succeeds on startup, fails with 404 (non-failoverable) on first refresh
var mockClient1 = new Mock();
@@ -429,7 +427,7 @@ public async Task FailOverTests_AllClientsBackedOffAfterNonFailoverableException
.Throws(new RequestFailedException(412, "Request failed."))
.Throws(new RequestFailedException(412, "Request failed."));
mockClient1.SetupSequence(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)))
+ .Returns(Task.FromResult(Response.FromValue(kv, mockResponse)))
.Throws(new RequestFailedException(412, "Request failed."))
.Throws(new RequestFailedException(412, "Request failed."));
mockClient1.SetupSequence(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
@@ -442,9 +440,9 @@ public async Task FailOverTests_AllClientsBackedOffAfterNonFailoverableException
mockClient2.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns(new MockAsyncPageable(Enumerable.Empty().ToList()));
mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)));
+ .Returns(Task.FromResult(Response.FromValue(kv, mockResponse)));
mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object)));
+ .Returns(Task.FromResult(Response.FromValue(kv, mockResponse)));
mockClient2.Setup(c => c.Equals(mockClient2)).Returns(true);
ConfigurationClientWrapper cw1 = new ConfigurationClientWrapper(TestHelpers.PrimaryConfigStoreEndpoint, mockClient1.Object);
diff --git a/tests/Tests.AzureAppConfiguration/Unit/FeatureManagementTests.cs b/tests/Tests.AzureAppConfiguration/Unit/FeatureManagementTests.cs
index 439c63bb6..05642fe8a 100644
--- a/tests/Tests.AzureAppConfiguration/Unit/FeatureManagementTests.cs
+++ b/tests/Tests.AzureAppConfiguration/Unit/FeatureManagementTests.cs
@@ -777,6 +777,10 @@ public async Task WatchesFeatureFlags()
.Callback(() => mockAsyncPageable.UpdateCollection(featureFlags))
.Returns(mockAsyncPageable);
+ mockClient.Setup(c => c.CheckConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Callback(() => mockAsyncPageable.UpdateCollection(featureFlags))
+ .Returns(mockAsyncPageable);
+
IConfigurationRefresher refresher = null;
var config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
@@ -849,6 +853,10 @@ public async Task WatchesFeatureFlagsUsingCacheExpirationInterval()
.Callback(() => mockAsyncPageable.UpdateCollection(featureFlags))
.Returns(mockAsyncPageable);
+ mockClient.Setup(c => c.CheckConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Callback(() => mockAsyncPageable.UpdateCollection(featureFlags))
+ .Returns(mockAsyncPageable);
+
var cacheExpirationInterval = TimeSpan.FromSeconds(1);
IConfigurationRefresher refresher = null;
@@ -923,6 +931,10 @@ public async Task SkipRefreshIfRefreshIntervalHasNotElapsed()
.Callback(() => mockAsyncPageable.UpdateCollection(featureFlags))
.Returns(mockAsyncPageable);
+ mockClient.Setup(c => c.CheckConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Callback(() => mockAsyncPageable.UpdateCollection(featureFlags))
+ .Returns(mockAsyncPageable);
+
IConfigurationRefresher refresher = null;
var config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
@@ -994,6 +1006,10 @@ public async Task SkipRefreshIfCacheNotExpired()
.Callback(() => mockAsyncPageable.UpdateCollection(featureFlags))
.Returns(mockAsyncPageable);
+ mockClient.Setup(c => c.CheckConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Callback(() => mockAsyncPageable.UpdateCollection(featureFlags))
+ .Returns(mockAsyncPageable);
+
IConfigurationRefresher refresher = null;
var config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
@@ -1118,6 +1134,10 @@ public async Task DoesNotUseEtagForFeatureFlagRefresh()
.Callback(() => mockAsyncPageable.UpdateCollection(new List { _kv }))
.Returns(mockAsyncPageable);
+ mockClient.Setup(c => c.CheckConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Callback(() => mockAsyncPageable.UpdateCollection(new List { _kv }))
+ .Returns(mockAsyncPageable);
+
IConfigurationRefresher refresher = null;
var config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
@@ -1134,7 +1154,8 @@ public async Task DoesNotUseEtagForFeatureFlagRefresh()
Thread.Sleep(RefreshInterval);
await refresher.TryRefreshAsync();
- mockClient.Verify(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Exactly(3));
+ mockClient.Verify(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Exactly(2));
+ mockClient.Verify(c => c.CheckConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Once());
}
[Fact]
@@ -1569,6 +1590,12 @@ public async Task DifferentCacheExpirationsForMultipleFeatureFlagRegistrations()
(s.Key.StartsWith(FeatureManagementConstants.FeatureFlagMarker + prefix2) && s.Label == label2 && s.Key != FeatureManagementConstants.FeatureFlagMarker + "App2_Feature3")).ToList()))
.Returns(mockAsyncPageable);
+ mockClient.Setup(c => c.CheckConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Callback(() => mockAsyncPageable.UpdateCollection(featureFlagCollection.Where(s =>
+ (s.Key.StartsWith(FeatureManagementConstants.FeatureFlagMarker + prefix1) && s.Label == label1) ||
+ (s.Key.StartsWith(FeatureManagementConstants.FeatureFlagMarker + prefix2) && s.Label == label2 && s.Key != FeatureManagementConstants.FeatureFlagMarker + "App2_Feature3")).ToList()))
+ .Returns(mockAsyncPageable);
+
var config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
@@ -1739,6 +1766,11 @@ public async Task SelectAndRefreshSingleFeatureFlag()
s.Key.Equals(FeatureManagementConstants.FeatureFlagMarker + prefix1) && s.Label == label1).ToList()))
.Returns(mockAsyncPageable);
+ mockClient.Setup(c => c.CheckConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Callback(() => mockAsyncPageable.UpdateCollection(featureFlagCollection.Where(s =>
+ s.Key.Equals(FeatureManagementConstants.FeatureFlagMarker + prefix1) && s.Label == label1).ToList()))
+ .Returns(mockAsyncPageable);
+
var config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
@@ -1802,6 +1834,10 @@ public async Task ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefre
.Callback(() => mockAsyncPageable.UpdateCollection(featureFlags))
.Returns(mockAsyncPageable);
+ mockClient.Setup(c => c.CheckConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Callback(() => mockAsyncPageable.UpdateCollection(featureFlags))
+ .Returns(mockAsyncPageable);
+
mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync((Func>)GetIfChanged);
@@ -1886,6 +1922,10 @@ public async Task ValidateFeatureFlagsUnchangedLogged()
.Callback(() => mockAsyncPageable.UpdateCollection(featureFlags))
.Returns(mockAsyncPageable);
+ mockClient.Setup(c => c.CheckConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Callback(() => mockAsyncPageable.UpdateCollection(featureFlags))
+ .Returns(mockAsyncPageable);
+
mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync((Func>)GetIfChanged);
@@ -1964,6 +2004,10 @@ public async Task MapTransformFeatureFlagWithRefresh()
.Callback(() => mockAsyncPageable.UpdateCollection(featureFlags))
.Returns(mockAsyncPageable);
+ mockClient.Setup(c => c.CheckConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Callback(() => mockAsyncPageable.UpdateCollection(featureFlags))
+ .Returns(mockAsyncPageable);
+
mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync((Func>)GetIfChanged);
@@ -2367,7 +2411,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
Response GetTestKey(string key, string label, CancellationToken cancellationToken)
{
- return Response.FromValue(TestHelpers.CloneSetting(FirstKeyValue), new Mock().Object);
+ return Response.FromValue(TestHelpers.CloneSetting(FirstKeyValue), new MockResponse(200));
}
private ConfigurationSetting CreateFeatureFlag(string featureId,
diff --git a/tests/Tests.AzureAppConfiguration/Unit/HealthCheckTest.cs b/tests/Tests.AzureAppConfiguration/Unit/HealthCheckTest.cs
index 4484def0f..d99a6d28e 100644
--- a/tests/Tests.AzureAppConfiguration/Unit/HealthCheckTest.cs
+++ b/tests/Tests.AzureAppConfiguration/Unit/HealthCheckTest.cs
@@ -68,6 +68,10 @@ public async Task HealthCheckTests_ReturnsUnhealthyWhenRefreshFailed()
mockClient.SetupSequence(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns(new MockAsyncPageable(kvCollection))
+ .Returns(new MockAsyncPageable(Enumerable.Empty().ToList()))
+ .Returns(new MockAsyncPageable(Enumerable.Empty().ToList()));
+
+ mockClient.SetupSequence(c => c.CheckConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Throws(new RequestFailedException(503, "Request failed."))
.Returns(new MockAsyncPageable(Enumerable.Empty().ToList()))
.Returns(new MockAsyncPageable(Enumerable.Empty().ToList()));
@@ -145,7 +149,9 @@ public async Task HealthCheckTests_ShouldRespectHealthCheckRegistration()
var mockClient = new Mock(MockBehavior.Strict);
mockClient.SetupSequence(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
- .Returns(new MockAsyncPageable(kvCollection))
+ .Returns(new MockAsyncPageable(kvCollection));
+
+ mockClient.SetupSequence(c => c.CheckConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Throws(new RequestFailedException(503, "Request failed."));
var config = new ConfigurationBuilder()
diff --git a/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs b/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs
index 3e856a1b0..a671300eb 100644
--- a/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs
+++ b/tests/Tests.AzureAppConfiguration/Unit/KeyVaultReferenceTests.cs
@@ -171,7 +171,6 @@ public class KeyVaultReferenceTests
[Fact]
public void NotSecretIdentifierURI()
{
- var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns(new MockAsyncPageable(new List { _kvNoUrl }));
@@ -198,7 +197,6 @@ public void NotSecretIdentifierURI()
[Fact]
public void UseSecret()
{
- var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns(new MockAsyncPageable(new List { _kv }));
@@ -223,7 +221,6 @@ public void UseSecret()
[Fact]
public void UseCertificate()
{
- var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns(new MockAsyncPageable(new List { _kvCertRef }));
@@ -248,7 +245,6 @@ public void UseCertificate()
[Fact]
public void ThrowsWhenSecretNotFound()
{
- var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns(new MockAsyncPageable(new List { _kv }));
@@ -273,7 +269,6 @@ public void ThrowsWhenSecretNotFound()
[Fact]
public void DisabledSecretIdentifier()
{
- var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns(new MockAsyncPageable(new List { _kv }));
@@ -298,7 +293,6 @@ public void DisabledSecretIdentifier()
[Fact]
public void WrongContentType()
{
- var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns(new MockAsyncPageable(new List { _kvWrongContentType }));
@@ -320,7 +314,6 @@ public void WrongContentType()
[Fact]
public void MultipleKeys()
{
- var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns(new MockAsyncPageable(_kvCollectionPageOne));
@@ -346,7 +339,6 @@ public void MultipleKeys()
[Fact]
public void CancellationToken()
{
- var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns(new MockAsyncPageable(_kvCollectionPageOne));
@@ -374,7 +366,6 @@ public void CancellationToken()
[Fact]
public void HasNoAccessToKeyVault()
{
- var mockResponse = new Mock