diff --git a/build/install-dotnet.ps1 b/build/install-dotnet.ps1
index 11552a78..eeb52b70 100644
--- a/build/install-dotnet.ps1
+++ b/build/install-dotnet.ps1
@@ -1,8 +1,8 @@
-# Installs .NET 6 and .NET 7 for CI/CD environment
+# Installs .NET 6 and .NET 8 for CI/CD environment
# see: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script#examples
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://dot.net/v1/dotnet-install.ps1'))) -Channel 6.0
-&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://dot.net/v1/dotnet-install.ps1'))) -Channel 7.0
\ No newline at end of file
+&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://dot.net/v1/dotnet-install.ps1'))) -Channel 8.0
diff --git a/examples/ConfigStoreDemo/ConfigStoreDemo.csproj b/examples/ConfigStoreDemo/ConfigStoreDemo.csproj
index 3b11aa18..caab5885 100644
--- a/examples/ConfigStoreDemo/ConfigStoreDemo.csproj
+++ b/examples/ConfigStoreDemo/ConfigStoreDemo.csproj
@@ -1,7 +1,7 @@
false
- net7.0
+ net8.0
diff --git a/examples/ConfigStoreDemo/Program.cs b/examples/ConfigStoreDemo/Program.cs
index 90838b59..40d0b9ed 100644
--- a/examples/ConfigStoreDemo/Program.cs
+++ b/examples/ConfigStoreDemo/Program.cs
@@ -30,7 +30,7 @@ public static IWebHost BuildWebHost(string[] args)
.ConfigureRefresh(refresh =>
{
refresh.Register("Settings:BackgroundColor")
- .SetCacheExpiration(TimeSpan.FromSeconds(10));
+ .SetRefreshInterval(TimeSpan.FromSeconds(10));
});
});
})
diff --git a/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj b/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
index dcff4691..ddcb2b93 100644
--- a/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
+++ b/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
@@ -2,13 +2,13 @@
Exe
- net7.0
+ net8.0
-
-
-
+
+
+
diff --git a/examples/ConsoleApplication/ConsoleApplication.csproj b/examples/ConsoleApplication/ConsoleApplication.csproj
index 2bc2e9ec..bd4756fa 100644
--- a/examples/ConsoleApplication/ConsoleApplication.csproj
+++ b/examples/ConsoleApplication/ConsoleApplication.csproj
@@ -3,12 +3,12 @@
false
Exe
- net7.0
+ net8.0
-
-
+
+
diff --git a/examples/ConsoleApplication/Program.cs b/examples/ConsoleApplication/Program.cs
index a3e371b0..c3d52ac8 100644
--- a/examples/ConsoleApplication/Program.cs
+++ b/examples/ConsoleApplication/Program.cs
@@ -57,7 +57,7 @@ private static void Configure()
{
refresh.Register("AppName")
.Register("Language", refreshAll: true)
- .SetCacheExpiration(TimeSpan.FromSeconds(10));
+ .SetRefreshInterval(TimeSpan.FromSeconds(10));
});
// Get an instance of the refresher that can be used to refresh data
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 c8b8b0a9..cb429e03 100644
--- a/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
+++ b/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
@@ -2,7 +2,7 @@
- net6.0;net7.0
+ net6.0;net8.0
Microsoft.Azure.AppConfiguration.AspNetCore allows developers to use Microsoft Azure App Configuration service as a configuration source in their applications. This package adds additional features for ASP.NET Core applications to the existing package Microsoft.Extensions.Configuration.AzureAppConfiguration.
true
false
@@ -21,7 +21,7 @@
- 8.0.0-preview.2
+ 8.0.0-preview.3
@@ -34,7 +34,8 @@
..\..\AzureAppConfigurationRules.ruleset
- True
+ true
+ true
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 55c32e2d..67df4997 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
@@ -2,7 +2,7 @@
- net6.0;net7.0
+ net6.0;net8.0
Microsoft.Azure.AppConfiguration.Functions.Worker allows developers to use the Microsoft Azure App Configuration service as a configuration source in their applications. This package adds additional features to the existing package Microsoft.Extensions.Configuration.AzureAppConfiguration for .NET Azure Functions running in an isolated process.
true
false
@@ -24,7 +24,7 @@
- 8.0.0-preview.2
+ 8.0.0-preview.3
@@ -37,7 +37,8 @@
..\..\AzureAppConfigurationRules.ruleset
- True
+ true
+ true
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
index d79838da..7d9a9cad 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
@@ -35,10 +35,15 @@ public class AzureAppConfigurationOptions
private SortedSet _keyPrefixes = new SortedSet(Comparer.Create((k1, k2) => -string.Compare(k1, k2, StringComparison.OrdinalIgnoreCase)));
///
- /// Flag to indicate whether enable replica discovery.
+ /// Flag to indicate whether replica discovery is enabled.
///
public bool ReplicaDiscoveryEnabled { get; set; } = true;
+ ///
+ /// Flag to indicate whether load balancing is enabled.
+ ///
+ public bool LoadBalancingEnabled { get; set; }
+
///
/// The list of connection strings used to connect to an Azure App Configuration store and its replicas.
///
@@ -119,7 +124,7 @@ internal IEnumerable Adapters
///
/// Indicates all types of feature filters used by the application.
///
- internal FeatureFilterTracing FeatureFilterTracing { get; set; } = new FeatureFilterTracing();
+ internal FeatureFlagTracing FeatureFlagTracing { get; set; } = new FeatureFlagTracing();
///
/// Options used to configure provider startup.
@@ -135,7 +140,7 @@ public AzureAppConfigurationOptions()
{
new AzureKeyVaultKeyValueAdapter(new AzureKeyVaultSecretProvider()),
new JsonKeyValueAdapter(),
- new FeatureManagementKeyValueAdapter(FeatureFilterTracing)
+ new FeatureManagementKeyValueAdapter(FeatureFlagTracing)
};
}
@@ -215,10 +220,10 @@ public AzureAppConfigurationOptions UseFeatureFlags(Action c
FeatureFlagOptions options = new FeatureFlagOptions();
configure?.Invoke(options);
- if (options.CacheExpirationInterval < RefreshConstants.MinimumFeatureFlagsCacheExpirationInterval)
+ if (options.RefreshInterval < RefreshConstants.MinimumFeatureFlagRefreshInterval)
{
- throw new ArgumentOutOfRangeException(nameof(options.CacheExpirationInterval), options.CacheExpirationInterval.TotalMilliseconds,
- string.Format(ErrorMessages.CacheExpirationTimeTooShort, RefreshConstants.MinimumFeatureFlagsCacheExpirationInterval.TotalMilliseconds));
+ throw new ArgumentOutOfRangeException(nameof(options.RefreshInterval), options.RefreshInterval.TotalMilliseconds,
+ string.Format(ErrorMessages.RefreshIntervalTooShort, RefreshConstants.MinimumFeatureFlagRefreshInterval.TotalMilliseconds));
}
if (options.FeatureFlagSelectors.Count() != 0 && options.Label != null)
@@ -247,8 +252,8 @@ public AzureAppConfigurationOptions UseFeatureFlags(Action c
{
Key = featureFlagFilter,
Label = labelFilter,
- // If UseFeatureFlags is called multiple times for the same key and label filters, last cache expiration time wins
- CacheExpirationInterval = options.CacheExpirationInterval
+ // If UseFeatureFlags is called multiple times for the same key and label filters, last refresh interval wins
+ RefreshInterval = options.RefreshInterval
});
}
@@ -381,7 +386,7 @@ public AzureAppConfigurationOptions ConfigureRefresh(Action _mappedData;
private Dictionary _watchedSettings = new Dictionary();
private RequestTracingOptions _requestTracingOptions;
private Dictionary _configClientBackoffs = new Dictionary();
- private readonly TimeSpan MinCacheExpirationInterval;
+ private readonly TimeSpan MinRefreshInterval;
// The most-recent time when the refresh operation attempted to load the initial configuration
private DateTimeOffset InitializationCacheExpires = default;
@@ -111,11 +112,11 @@ public AzureAppConfigurationProvider(IConfigurationClientManager configClientMan
if (watchers.Any())
{
- MinCacheExpirationInterval = watchers.Min(w => w.CacheExpirationInterval);
+ MinRefreshInterval = watchers.Min(w => w.RefreshInterval);
}
else
{
- MinCacheExpirationInterval = RefreshConstants.DefaultCacheExpirationInterval;
+ MinRefreshInterval = RefreshConstants.DefaultRefreshInterval;
}
// Enable request tracing if not opt-out
@@ -189,16 +190,16 @@ public async Task RefreshAsync(CancellationToken cancellationToken)
try
{
// FeatureManagement assemblies may not be loaded on provider startup, so version information is gathered upon first refresh for tracing
- EnsureFeatureManagementVersionInspected();
+ EnsureAssemblyInspected();
var utcNow = DateTimeOffset.UtcNow;
- IEnumerable cacheExpiredWatchers = _options.ChangeWatchers.Where(changeWatcher => utcNow >= changeWatcher.CacheExpires);
- IEnumerable cacheExpiredMultiKeyWatchers = _options.MultiKeyWatchers.Where(changeWatcher => utcNow >= changeWatcher.CacheExpires);
+ IEnumerable refreshableWatchers = _options.ChangeWatchers.Where(changeWatcher => utcNow >= changeWatcher.NextRefreshTime);
+ IEnumerable refreshableMultiKeyWatchers = _options.MultiKeyWatchers.Where(changeWatcher => utcNow >= changeWatcher.NextRefreshTime);
- // Skip refresh if mappedData is loaded, but none of the watchers or adapters cache is expired.
+ // Skip refresh if mappedData is loaded, but none of the watchers or adapters are refreshable.
if (_mappedData != null &&
- !cacheExpiredWatchers.Any() &&
- !cacheExpiredMultiKeyWatchers.Any() &&
+ !refreshableWatchers.Any() &&
+ !refreshableMultiKeyWatchers.Any() &&
!_options.Adapters.Any(adapter => adapter.NeedsRefresh()))
{
return;
@@ -237,7 +238,7 @@ public async Task RefreshAsync(CancellationToken cancellationToken)
{
if (InitializationCacheExpires < utcNow)
{
- InitializationCacheExpires = utcNow.Add(MinCacheExpirationInterval);
+ InitializationCacheExpires = utcNow.Add(MinRefreshInterval);
await InitializeAsync(clients, cancellationToken).ConfigureAwait(false);
}
@@ -266,7 +267,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) =>
logDebugBuilder.Clear();
logInfoBuilder.Clear();
- foreach (KeyValueWatcher changeWatcher in cacheExpiredWatchers)
+ foreach (KeyValueWatcher changeWatcher in refreshableWatchers)
{
string watchedKey = changeWatcher.Key;
string watchedLabel = changeWatcher.Label;
@@ -336,7 +337,7 @@ await CallWithRequestTracing(
return;
}
- changedKeyValuesCollection = await GetRefreshedKeyValueCollections(cacheExpiredMultiKeyWatchers, client, logDebugBuilder, logInfoBuilder, endpoint, cancellationToken).ConfigureAwait(false);
+ changedKeyValuesCollection = await GetRefreshedKeyValueCollections(refreshableMultiKeyWatchers, client, logDebugBuilder, logInfoBuilder, endpoint, cancellationToken).ConfigureAwait(false);
if (!changedKeyValuesCollection.Any())
{
@@ -350,9 +351,9 @@ await CallWithRequestTracing(
{
watchedSettings = new Dictionary(_watchedSettings);
- foreach (KeyValueWatcher changeWatcher in cacheExpiredWatchers.Concat(cacheExpiredMultiKeyWatchers))
+ foreach (KeyValueWatcher changeWatcher in refreshableWatchers.Concat(refreshableMultiKeyWatchers))
{
- UpdateCacheExpirationTime(changeWatcher);
+ UpdateNextRefreshTime(changeWatcher);
}
foreach (KeyValueChange change in keyValueChanges.Concat(changedKeyValuesCollection))
@@ -387,7 +388,7 @@ await CallWithRequestTracing(
// Invalidate the cached Key Vault secret (if any) for this ConfigurationSetting
foreach (IKeyValueAdapter adapter in _options.Adapters)
{
- adapter.InvalidateCache(change.Current);
+ adapter.OnChangeDetected(change.Current);
}
}
}
@@ -398,13 +399,13 @@ await CallWithRequestTracing(
// Invalidate all the cached KeyVault secrets
foreach (IKeyValueAdapter adapter in _options.Adapters)
{
- adapter.InvalidateCache();
+ adapter.OnChangeDetected();
}
- // Update the cache expiration time for all refresh registered settings and feature flags
+ // Update the next refresh time for all refresh registered settings and feature flags
foreach (KeyValueWatcher changeWatcher in _options.ChangeWatchers.Concat(_options.MultiKeyWatchers))
{
- UpdateCacheExpirationTime(changeWatcher);
+ UpdateNextRefreshTime(changeWatcher);
}
}
@@ -492,6 +493,12 @@ public async Task TryRefreshAsync(CancellationToken cancellationToken)
return false;
}
+ catch (FormatException fe)
+ {
+ _logger.LogWarning(LogHelper.BuildRefreshFailedDueToFormattingErrorMessage(fe.Message));
+
+ return false;
+ }
return true;
}
@@ -536,16 +543,16 @@ public void ProcessPushNotification(PushNotification pushNotification, TimeSpan?
private void SetDirty(TimeSpan? maxDelay)
{
- DateTimeOffset cacheExpires = AddRandomDelay(DateTimeOffset.UtcNow, maxDelay ?? DefaultMaxSetDirtyDelay);
+ DateTimeOffset nextRefreshTime = AddRandomDelay(DateTimeOffset.UtcNow, maxDelay ?? DefaultMaxSetDirtyDelay);
foreach (KeyValueWatcher changeWatcher in _options.ChangeWatchers)
{
- changeWatcher.CacheExpires = cacheExpires;
+ changeWatcher.NextRefreshTime = nextRefreshTime;
}
foreach (KeyValueWatcher changeWatcher in _options.MultiKeyWatchers)
{
- changeWatcher.CacheExpires = cacheExpires;
+ changeWatcher.NextRefreshTime = nextRefreshTime;
}
}
@@ -553,8 +560,8 @@ private async Task> PrepareData(Dictionary(StringComparer.OrdinalIgnoreCase);
- // Reset old filter tracing in order to track the filter types present in the current response from server.
- _options.FeatureFilterTracing.ResetFeatureFilterTracing();
+ // Reset old feature flag tracing in order to track the information present in the current response from server.
+ _options.FeatureFlagTracing.ResetFeatureFlagTracing();
foreach (KeyValuePair kvp in data)
{
@@ -634,6 +641,7 @@ exception is KeyVaultReferenceException ||
exception is TimeoutException ||
exception is OperationCanceledException ||
exception is InvalidOperationException ||
+ exception is FormatException ||
((exception as AggregateException)?.InnerExceptions?.Any(e =>
e is RequestFailedException ||
e is OperationCanceledException) ?? false)))
@@ -722,10 +730,10 @@ await ExecuteWithFailOverPolicyAsync(
cancellationToken)
.ConfigureAwait(false);
- // Update the cache expiration time for all refresh registered settings and feature flags
+ // Update the next refresh time for all refresh registered settings and feature flags
foreach (KeyValueWatcher changeWatcher in _options.ChangeWatchers.Concat(_options.MultiKeyWatchers))
{
- UpdateCacheExpirationTime(changeWatcher);
+ UpdateNextRefreshTime(changeWatcher);
}
if (data != null)
@@ -733,7 +741,7 @@ await ExecuteWithFailOverPolicyAsync(
// Invalidate all the cached KeyVault secrets
foreach (IKeyValueAdapter adapter in _options.Adapters)
{
- adapter.InvalidateCache();
+ adapter.OnChangeDetected();
}
Dictionary mappedData = await MapConfigurationSettings(data).ConfigureAwait(false);
@@ -912,6 +920,11 @@ private void SetData(IDictionary data)
// Set the application data for the configuration provider
Data = data;
+ foreach (IKeyValueAdapter adapter in _options.Adapters)
+ {
+ adapter.OnConfigUpdated();
+ }
+
// Notify that the configuration has been updated
OnReload();
}
@@ -955,7 +968,7 @@ private void SetRequestTracingOptions()
IsKeyVaultConfigured = _options.IsKeyVaultConfigured,
IsKeyVaultRefreshConfigured = _options.IsKeyVaultRefreshConfigured,
ReplicaCount = _options.Endpoints?.Count() - 1 ?? _options.ConnectionStrings?.Count() - 1 ?? 0,
- FilterTracing = _options.FeatureFilterTracing
+ FeatureFlagTracing = _options.FeatureFlagTracing
};
}
@@ -980,10 +993,9 @@ private bool IsAuthenticationError(Exception ex)
return false;
}
- private void UpdateCacheExpirationTime(KeyValueWatcher changeWatcher)
+ private void UpdateNextRefreshTime(KeyValueWatcher changeWatcher)
{
- TimeSpan cacheExpirationTime = changeWatcher.CacheExpirationInterval;
- changeWatcher.CacheExpires = DateTimeOffset.UtcNow.Add(cacheExpirationTime);
+ changeWatcher.NextRefreshTime = DateTimeOffset.UtcNow.Add(changeWatcher.RefreshInterval);
}
private async Task ExecuteWithFailOverPolicyAsync(
@@ -991,6 +1003,27 @@ private async Task ExecuteWithFailOverPolicyAsync(
Func> funcToExecute,
CancellationToken cancellationToken = default)
{
+ if (_options.LoadBalancingEnabled && _lastSuccessfulEndpoint != null && clients.Count() > 1)
+ {
+ int nextClientIndex = 0;
+
+ foreach (ConfigurationClient client in clients)
+ {
+ nextClientIndex++;
+
+ if (_configClientManager.GetEndpointForClient(client) == _lastSuccessfulEndpoint)
+ {
+ break;
+ }
+ }
+
+ // If we found the last successful client, we'll rotate the list so that the next client is at the beginning
+ if (nextClientIndex < clients.Count())
+ {
+ clients = clients.Skip(nextClientIndex).Concat(clients.Take(nextClientIndex));
+ }
+ }
+
using IEnumerator clientEnumerator = clients.GetEnumerator();
clientEnumerator.MoveNext();
@@ -1011,6 +1044,8 @@ private async Task ExecuteWithFailOverPolicyAsync(
T result = await funcToExecute(currentClient).ConfigureAwait(false);
success = true;
+ _lastSuccessfulEndpoint = _configClientManager.GetEndpointForClient(currentClient);
+
return result;
}
catch (RequestFailedException rfe)
@@ -1179,17 +1214,22 @@ private IEnumerable GetCurrentKeyValueCollection(string ke
return currentKeyValues;
}
- private void EnsureFeatureManagementVersionInspected()
+ private void EnsureAssemblyInspected()
{
- if (!_isFeatureManagementVersionInspected)
+ if (!_isAssemblyInspected)
{
- _isFeatureManagementVersionInspected = true;
+ _isAssemblyInspected = true;
if (_requestTracingEnabled && _requestTracingOptions != null)
{
_requestTracingOptions.FeatureManagementVersion = TracingUtils.GetAssemblyVersion(RequestTracingConstants.FeatureManagementAssemblyName);
_requestTracingOptions.FeatureManagementAspNetCoreVersion = TracingUtils.GetAssemblyVersion(RequestTracingConstants.FeatureManagementAspNetCoreAssemblyName);
+
+ if (TracingUtils.GetAssemblyVersion(RequestTracingConstants.SignalRAssemblyName) != null)
+ {
+ _requestTracingOptions.IsSignalRUsed = true;
+ }
}
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationRefreshOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationRefreshOptions.cs
index 5297507c..32ff2291 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationRefreshOptions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationRefreshOptions.cs
@@ -12,7 +12,7 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
///
public class AzureAppConfigurationRefreshOptions
{
- internal TimeSpan CacheExpirationInterval { get; private set; } = RefreshConstants.DefaultCacheExpirationInterval;
+ internal TimeSpan RefreshInterval { get; private set; } = RefreshConstants.DefaultRefreshInterval;
internal ISet RefreshRegistrations = new HashSet();
///
@@ -55,15 +55,28 @@ public AzureAppConfigurationRefreshOptions Register(string key, string label = L
/// Any refresh operation triggered using will not update the value for a key until the cached value for that key has expired.
///
/// Minimum time that must elapse before the cache is expired.
+ [Obsolete("The " + nameof(SetCacheExpiration) + " method is deprecated and will be removed in a future release. " +
+ "Please use the " + nameof(SetRefreshInterval) + " method instead. " +
+ "Note that only the name of the method has changed, and the functionality remains the same.")]
public AzureAppConfigurationRefreshOptions SetCacheExpiration(TimeSpan cacheExpiration)
{
- if (cacheExpiration < RefreshConstants.MinimumCacheExpirationInterval)
+ return SetRefreshInterval(cacheExpiration);
+ }
+
+ ///
+ /// Sets the minimum time interval between consecutive refresh operations for the registered key-values. Default value is 30 seconds. Must be greater than 1 second.
+ /// Refresh operations triggered using will not make any server requests unless the refresh interval has elapsed since the key was last refreshed.
+ ///
+ /// Minimum time that must elapse between each refresh for a specific key.
+ public AzureAppConfigurationRefreshOptions SetRefreshInterval(TimeSpan refreshInterval)
+ {
+ if (refreshInterval < RefreshConstants.MinimumRefreshInterval)
{
- throw new ArgumentOutOfRangeException(nameof(cacheExpiration), cacheExpiration.TotalMilliseconds,
- string.Format(ErrorMessages.CacheExpirationTimeTooShort, RefreshConstants.MinimumCacheExpirationInterval.TotalMilliseconds));
+ throw new ArgumentOutOfRangeException(nameof(refreshInterval), refreshInterval.TotalMilliseconds,
+ string.Format(ErrorMessages.RefreshIntervalTooShort, RefreshConstants.MinimumRefreshInterval.TotalMilliseconds));
}
- CacheExpirationInterval = cacheExpiration;
+ RefreshInterval = refreshInterval;
return this;
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs
index 71a5f480..446fa714 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs
@@ -36,11 +36,20 @@ public IConfigurationProvider Build(IConfigurationBuilder builder)
}
else if (options.ConnectionStrings != null)
{
- clientManager = new ConfigurationClientManager(options.ConnectionStrings, options.ClientOptions, options.ReplicaDiscoveryEnabled);
+ clientManager = new ConfigurationClientManager(
+ options.ConnectionStrings,
+ options.ClientOptions,
+ options.ReplicaDiscoveryEnabled,
+ options.LoadBalancingEnabled);
}
else if (options.Endpoints != null && options.Credential != null)
{
- clientManager = new ConfigurationClientManager(options.Endpoints, options.Credential, options.ClientOptions, options.ReplicaDiscoveryEnabled);
+ clientManager = new ConfigurationClientManager(
+ options.Endpoints,
+ options.Credential,
+ options.ClientOptions,
+ options.ReplicaDiscoveryEnabled,
+ options.LoadBalancingEnabled);
}
else
{
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs
index e49deb2c..8d0ec3b5 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs
@@ -29,22 +29,12 @@ public AzureKeyVaultKeyValueAdapter(AzureKeyVaultSecretProvider secretProvider)
/// returns the keyname and actual value
public async Task>> ProcessKeyValue(ConfigurationSetting setting, Uri endpoint, Logger logger, CancellationToken cancellationToken)
{
- KeyVaultSecretReference secretRef;
-
- // Content validation
- try
- {
- secretRef = JsonSerializer.Deserialize(setting.Value);
- }
- catch (JsonException e)
- {
- throw CreateKeyVaultReferenceException("Invalid Key Vault reference.", setting, e, null);
- }
+ string secretRefUri = ParseSecretReferenceUri(setting);
// Uri validation
- if (string.IsNullOrEmpty(secretRef.Uri) || !Uri.TryCreate(secretRef.Uri, UriKind.Absolute, out Uri secretUri) || !KeyVaultSecretIdentifier.TryCreate(secretUri, out KeyVaultSecretIdentifier secretIdentifier))
+ 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, secretRef);
+ throw CreateKeyVaultReferenceException("Invalid Key vault secret identifier.", setting, null, secretRefUri);
}
string secret;
@@ -55,11 +45,11 @@ public async Task>> ProcessKeyValue(Con
}
catch (Exception e) when (e is UnauthorizedAccessException || (e.Source?.Equals(AzureIdentityAssemblyName, StringComparison.OrdinalIgnoreCase) ?? false))
{
- throw CreateKeyVaultReferenceException(e.Message, setting, e, secretRef);
+ 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, secretRef);
+ throw CreateKeyVaultReferenceException("Key vault error.", setting, e, secretRefUri);
}
return new KeyValuePair[]
@@ -68,7 +58,7 @@ public async Task>> ProcessKeyValue(Con
};
}
- KeyVaultReferenceException CreateKeyVaultReferenceException(string message, ConfigurationSetting setting, Exception inner, KeyVaultSecretReference secretRef = null)
+ KeyVaultReferenceException CreateKeyVaultReferenceException(string message, ConfigurationSetting setting, Exception inner, string secretRefUri = null)
{
return new KeyVaultReferenceException(message, inner)
{
@@ -76,7 +66,7 @@ KeyVaultReferenceException CreateKeyVaultReferenceException(string message, Conf
Label = setting.Label,
Etag = setting.ETag.ToString(),
ErrorCode = (inner as RequestFailedException)?.ErrorCode,
- SecretIdentifier = secretRef?.Uri
+ SecretIdentifier = secretRefUri
};
}
@@ -86,7 +76,7 @@ public bool CanProcess(ConfigurationSetting setting)
return string.Equals(contentType, KeyVaultConstants.ContentType);
}
- public void InvalidateCache(ConfigurationSetting setting = null)
+ public void OnChangeDetected(ConfigurationSetting setting = null)
{
if (setting == null)
{
@@ -98,9 +88,59 @@ public void InvalidateCache(ConfigurationSetting setting = null)
}
}
+ public void OnConfigUpdated()
+ {
+ return;
+ }
+
public bool NeedsRefresh()
{
return _secretProvider.ShouldRefreshKeyVaultSecrets();
}
+
+ private string ParseSecretReferenceUri(ConfigurationSetting setting)
+ {
+ string secretRefUri = null;
+
+ try
+ {
+ var reader = new Utf8JsonReader(System.Text.Encoding.UTF8.GetBytes(setting.Value));
+
+ if (reader.Read() && reader.TokenType != JsonTokenType.StartObject)
+ {
+ throw CreateKeyVaultReferenceException(ErrorMessages.InvalidKeyVaultReference, setting, null, null);
+ }
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ if (reader.GetString() == KeyVaultConstants.SecretReferenceUriJsonPropertyName)
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ secretRefUri = reader.GetString();
+ }
+ else
+ {
+ throw CreateKeyVaultReferenceException(ErrorMessages.InvalidKeyVaultReference, setting, null, null);
+ }
+ }
+ else
+ {
+ reader.Skip();
+ }
+ }
+ }
+ catch (JsonException e)
+ {
+ throw CreateKeyVaultReferenceException(ErrorMessages.InvalidKeyVaultReference, setting, e, null);
+ }
+
+ return secretRefUri;
+ }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultConstants.cs
index ec55e115..1309e58c 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultConstants.cs
@@ -6,5 +6,7 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault
internal class KeyVaultConstants
{
public const string ContentType = "application/vnd.microsoft.appconfig.keyvaultref+json";
+
+ public const string SecretReferenceUriJsonPropertyName = "uri";
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultSecretReference.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultSecretReference.cs
deleted file mode 100644
index 40efc2b1..00000000
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultSecretReference.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT license.
-//
-using System.Text.Json.Serialization;
-
-namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault
-{
- internal class KeyVaultSecretReference
- {
- [JsonPropertyName("uri")]
- public string Uri { get; set; }
- }
-}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ConfigurationClientManager.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ConfigurationClientManager.cs
index 9ae71d2a..0a80932c 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ConfigurationClientManager.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ConfigurationClientManager.cs
@@ -54,7 +54,8 @@ internal class ConfigurationClientManager : IConfigurationClientManager, IDispos
public ConfigurationClientManager(
IEnumerable connectionStrings,
ConfigurationClientOptions clientOptions,
- bool replicaDiscoveryEnabled)
+ bool replicaDiscoveryEnabled,
+ bool loadBalancingEnabled)
{
if (connectionStrings == null || !connectionStrings.Any())
{
@@ -68,6 +69,12 @@ public ConfigurationClientManager(
_clientOptions = clientOptions;
_replicaDiscoveryEnabled = replicaDiscoveryEnabled;
+ // If load balancing is enabled, shuffle the passed in connection strings to randomize the endpoint used on startup
+ if (loadBalancingEnabled)
+ {
+ connectionStrings = connectionStrings.ToList().Shuffle();
+ }
+
_validDomain = GetValidDomain(_endpoint);
_srvLookupClient = new SrvLookupClient();
@@ -84,7 +91,8 @@ public ConfigurationClientManager(
IEnumerable endpoints,
TokenCredential credential,
ConfigurationClientOptions clientOptions,
- bool replicaDiscoveryEnabled)
+ bool replicaDiscoveryEnabled,
+ bool loadBalancingEnabled)
{
if (endpoints == null || !endpoints.Any())
{
@@ -101,6 +109,12 @@ public ConfigurationClientManager(
_clientOptions = clientOptions;
_replicaDiscoveryEnabled = replicaDiscoveryEnabled;
+ // If load balancing is enabled, shuffle the passed in endpoints to randomize the endpoint used on startup
+ if (loadBalancingEnabled)
+ {
+ endpoints = endpoints.ToList().Shuffle();
+ }
+
_validDomain = GetValidDomain(_endpoint);
_srvLookupClient = new SrvLookupClient();
@@ -132,6 +146,7 @@ public IEnumerable GetClients()
_ = DiscoverFallbackClients();
}
+ // Treat the passed in endpoints as the highest priority clients
IEnumerable clients = _clients.Select(c => c.Client);
if (_dynamicClients != null && _dynamicClients.Any())
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
index 0c19cccf..c7974736 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
@@ -5,7 +5,10 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
{
internal class ErrorMessages
{
- public const string CacheExpirationTimeTooShort = "The cache expiration time cannot be less than {0} milliseconds.";
+ public const string RefreshIntervalTooShort = "The refresh interval cannot be less than {0} milliseconds.";
public const string SecretRefreshIntervalTooShort = "The secret refresh interval cannot be less than {0} milliseconds.";
+ public const string FeatureFlagInvalidJsonProperty = "Invalid property '{0}' for feature flag. Key: '{1}'. Found type: '{2}'. Expected type: '{3}'.";
+ public const string FeatureFlagInvalidFormat = "Invalid json format for feature flag. Key: '{0}'.";
+ public const string InvalidKeyVaultReference = "Invalid Key Vault reference.";
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/LoggingConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/LoggingConstants.cs
index 41c56ca3..86576a48 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/LoggingConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/LoggingConstants.cs
@@ -15,6 +15,7 @@ internal class LoggingConstants
public const string RefreshFailedDueToKeyVaultError = "A refresh operation failed while resolving a Key Vault reference.";
public const string PushNotificationUnregisteredEndpoint = "Ignoring the push notification received for the unregistered endpoint";
public const string FallbackClientLookupError = "Failed to perform fallback client lookup.";
+ public const string RefreshFailedDueToFormattingError = "A refresh operation failed due to a formatting error.";
// Successful update, debug log level
public const string RefreshKeyValueRead = "Key-value read from App Configuration.";
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RefreshConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RefreshConstants.cs
index 5805191a..965e380a 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RefreshConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RefreshConstants.cs
@@ -8,12 +8,12 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
internal class RefreshConstants
{
// Key-values
- public static readonly TimeSpan DefaultCacheExpirationInterval = TimeSpan.FromSeconds(30);
- public static readonly TimeSpan MinimumCacheExpirationInterval = TimeSpan.FromSeconds(1);
+ public static readonly TimeSpan DefaultRefreshInterval = TimeSpan.FromSeconds(30);
+ public static readonly TimeSpan MinimumRefreshInterval = TimeSpan.FromSeconds(1);
// Feature flags
- public static readonly TimeSpan DefaultFeatureFlagsCacheExpirationInterval = TimeSpan.FromSeconds(30);
- public static readonly TimeSpan MinimumFeatureFlagsCacheExpirationInterval = TimeSpan.FromSeconds(1);
+ public static readonly TimeSpan DefaultFeatureFlagRefreshInterval = TimeSpan.FromSeconds(30);
+ public static readonly TimeSpan MinimumFeatureFlagRefreshInterval = TimeSpan.FromSeconds(1);
// Key Vault secrets
public static readonly TimeSpan MinimumSecretRefreshInterval = TimeSpan.FromSeconds(1);
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs
index 9ab29637..9e542785 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs
@@ -30,6 +30,12 @@ internal class RequestTracingConstants
public const string KeyVaultConfiguredTag = "UsesKeyVault";
public const string KeyVaultRefreshConfiguredTag = "RefreshesKeyVault";
public const string ReplicaCountKey = "ReplicaCount";
+ public const string FeatureFlagUsesTelemetryTag = "Telemetry";
+ public const string FeatureFlagUsesSeedTag = "Seed";
+ public const string FeatureFlagMaxVariantsKey = "MaxVariants";
+ public const string FeatureFlagUsesVariantConfigurationReferenceTag = "ConfigRef";
+ public const string FeatureFlagFeaturesKey = "FFFeatures";
+ public const string SignalRUsedTag = "UsesSignalR";
public const string DiagnosticHeaderActivityName = "Azure.CustomDiagnosticHeaders";
public const string CorrelationContextHeader = "Correlation-Context";
@@ -37,5 +43,8 @@ internal class RequestTracingConstants
public const string FeatureManagementAssemblyName = "Microsoft.FeatureManagement";
public const string FeatureManagementAspNetCoreAssemblyName = "Microsoft.FeatureManagement.AspNetCore";
+ public const string SignalRAssemblyName = "Microsoft.AspNetCore.SignalR";
+
+ public const string Delimiter = "+";
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/EventGridEventExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/EventGridEventExtensions.cs
index bc91de1e..7ac04ca9 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/EventGridEventExtensions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/EventGridEventExtensions.cs
@@ -31,29 +31,59 @@ public static bool TryCreatePushNotification(this EventGridEvent eventGridEvent,
if (Uri.TryCreate(eventGridEvent.Subject, UriKind.Absolute, out Uri resourceUri))
{
- JsonElement eventGridEventData;
+ string syncToken = null;
try
{
- eventGridEventData = JsonDocument.Parse(eventGridEvent.Data.ToString()).RootElement;
+ var reader = new Utf8JsonReader(System.Text.Encoding.UTF8.GetBytes(eventGridEvent.Data.ToString()));
+
+ if (reader.Read() && reader.TokenType != JsonTokenType.StartObject)
+ {
+ return false;
+ }
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ if (reader.GetString() == SyncTokenPropertyName)
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ syncToken = reader.GetString();
+ }
+ else
+ {
+ return false;
+ }
+ }
+ else
+ {
+ reader.Skip();
+ }
+ }
}
catch (JsonException)
{
return false;
}
- if (eventGridEventData.ValueKind == JsonValueKind.Object &&
- eventGridEventData.TryGetProperty(SyncTokenPropertyName, out JsonElement syncTokenJson) &&
- syncTokenJson.ValueKind == JsonValueKind.String)
+ if (syncToken == null)
{
- pushNotification = new PushNotification()
- {
- SyncToken = syncTokenJson.GetString(),
- EventType = eventGridEvent.EventType,
- ResourceUri = resourceUri
- };
- return true;
+ return false;
}
+
+ pushNotification = new PushNotification()
+ {
+ SyncToken = syncToken,
+ EventType = eventGridEvent.EventType,
+ ResourceUri = resourceUri
+ };
+
+ return true;
}
return false;
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/Utf8JsonReaderExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/Utf8JsonReaderExtensions.cs
deleted file mode 100644
index f5e2a01e..00000000
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/Utf8JsonReaderExtensions.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT license.
-//
-using System.Text.Json;
-
-namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions
-{
- internal static class Utf8JsonReaderExtensions
- {
- public static string ReadAsString(this Utf8JsonReader reader)
- {
- if (reader.Read())
- {
- return reader.GetString();
- }
-
- return null;
- }
- }
-}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/ClientFilter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/ClientFilter.cs
index efd4023d..80aed990 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/ClientFilter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/ClientFilter.cs
@@ -2,16 +2,13 @@
// Licensed under the MIT license.
//
using System.Text.Json;
-using System.Text.Json.Serialization;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement
{
internal class ClientFilter
{
- [JsonPropertyName("name")]
public string Name { get; set; }
- [JsonPropertyName("parameters")]
public JsonElement Parameters { get; set; }
}
-}
+}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureAllocation.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureAllocation.cs
index f3817c97..0b2877e6 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureAllocation.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureAllocation.cs
@@ -2,28 +2,21 @@
// Licensed under the MIT license.
//
using System.Collections.Generic;
-using System.Text.Json.Serialization;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement
{
internal class FeatureAllocation
{
- [JsonPropertyName("default_when_disabled")]
public string DefaultWhenDisabled { get; set; }
- [JsonPropertyName("default_when_enabled")]
public string DefaultWhenEnabled { get; set; }
- [JsonPropertyName("user")]
public IEnumerable User { get; set; }
- [JsonPropertyName("group")]
public IEnumerable Group { get; set; }
- [JsonPropertyName("percentile")]
public IEnumerable Percentile { get; set; }
- [JsonPropertyName("seed")]
public string Seed { get; set; }
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureConditions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureConditions.cs
index 6927d310..ec29c199 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureConditions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureConditions.cs
@@ -2,16 +2,13 @@
// Licensed under the MIT license.
//
using System.Collections.Generic;
-using System.Text.Json.Serialization;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement
{
internal class FeatureConditions
{
- [JsonPropertyName("client_filters")]
public List ClientFilters { get; set; } = new List();
- [JsonPropertyName("requirement_type")]
public string RequirementType { get; set; }
}
-}
+}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlag.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlag.cs
index dddf52c3..7fe6245e 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlag.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlag.cs
@@ -2,28 +2,21 @@
// Licensed under the MIT license.
//
using System.Collections.Generic;
-using System.Text.Json.Serialization;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement
{
internal class FeatureFlag
{
- [JsonPropertyName("id")]
public string Id { get; set; }
- [JsonPropertyName("enabled")]
public bool Enabled { get; set; }
- [JsonPropertyName("conditions")]
public FeatureConditions Conditions { get; set; }
- [JsonPropertyName("variants")]
public IEnumerable Variants { get; set; }
- [JsonPropertyName("allocation")]
public FeatureAllocation Allocation { get; set; }
- [JsonPropertyName("telemetry")]
public FeatureTelemetry Telemetry { get; set; }
}
-}
+}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagOptions.cs
index 202f4055..11f2fde1 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagOptions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagOptions.cs
@@ -14,11 +14,22 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManage
///
public class FeatureFlagOptions
{
+ private TimeSpan _refreshInterval = RefreshConstants.DefaultFeatureFlagRefreshInterval;
+
///
/// A collection of .
///
internal List FeatureFlagSelectors = new List();
+ ///
+ /// The time after which feature flags can be refreshed. Must be greater than or equal to 1 second.
+ ///
+ internal TimeSpan RefreshInterval
+ {
+ get { return _refreshInterval; }
+ set { _refreshInterval = value; }
+ }
+
///
/// The label that feature flags will be selected from.
///
@@ -27,7 +38,27 @@ public class FeatureFlagOptions
///
/// The time after which the cached values of the feature flags expire. Must be greater than or equal to 1 second.
///
- public TimeSpan CacheExpirationInterval { get; set; } = RefreshConstants.DefaultFeatureFlagsCacheExpirationInterval;
+ [Obsolete("The " + nameof(CacheExpirationInterval) + " property is deprecated and will be removed in a future release. " +
+ "Please use the new " + nameof(SetRefreshInterval) + " method instead. " +
+ "Note that the usage has changed, but the functionality remains the same.")]
+ public TimeSpan CacheExpirationInterval
+ {
+ get { return _refreshInterval; }
+ set { _refreshInterval = value; }
+ }
+
+ ///
+ /// Sets the time after which feature flags can be refreshed.
+ ///
+ ///
+ /// Sets the minimum time interval between consecutive refresh operations for feature flags. Default value is 30 seconds. Must be greater than or equal to 1 second.
+ ///
+ public FeatureFlagOptions SetRefreshInterval(TimeSpan refreshInterval)
+ {
+ RefreshInterval = refreshInterval;
+
+ return this;
+ }
///
/// Specify what feature flags to include in the configuration provider.
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFilterTracing.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagTracing.cs
similarity index 65%
rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFilterTracing.cs
rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagTracing.cs
index 27b59205..c1e4aaa5 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFilterTracing.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagTracing.cs
@@ -12,13 +12,12 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManage
///
/// Tracing for tracking built-in feature filter usage.
///
- internal class FeatureFilterTracing
+ internal class FeatureFlagTracing
{
private const string CustomFilter = "CSTM";
private const string PercentageFilter = "PRCNT";
private const string TimeWindowFilter = "TIME";
private const string TargetingFilter = "TRGT";
- private const string FilterTypeDelimiter = "+";
// Built-in Feature Filter Names
private readonly List PercentageFilterNames = new List { "Percentage", "Microsoft.Percentage", "PercentageFilter", "Microsoft.PercentageFilter" };
@@ -29,18 +28,31 @@ internal class FeatureFilterTracing
public bool UsesPercentageFilter { get; set; } = false;
public bool UsesTimeWindowFilter { get; set; } = false;
public bool UsesTargetingFilter { get; set; } = false;
-
+ public bool UsesSeed { get; set; } = false;
+ public bool UsesTelemetry { get; set; } = false;
+ public bool UsesVariantConfigurationReference { get; set; } = false;
+ public int MaxVariants { get; set; }
+
public bool UsesAnyFeatureFilter()
{
return UsesCustomFilter || UsesPercentageFilter || UsesTimeWindowFilter || UsesTargetingFilter;
}
- public void ResetFeatureFilterTracing()
+ public bool UsesAnyTracingFeature()
+ {
+ return UsesSeed || UsesTelemetry || UsesVariantConfigurationReference;
+ }
+
+ public void ResetFeatureFlagTracing()
{
UsesCustomFilter = false;
UsesPercentageFilter = false;
UsesTimeWindowFilter = false;
UsesTargetingFilter = false;
+ UsesSeed = false;
+ UsesTelemetry = false;
+ UsesVariantConfigurationReference = false;
+ MaxVariants = 0;
}
public void UpdateFeatureFilterTracing(string filterName)
@@ -63,11 +75,19 @@ public void UpdateFeatureFilterTracing(string filterName)
}
}
+ public void NotifyMaxVariants(int currentFlagTotalVariants)
+ {
+ if (currentFlagTotalVariants > MaxVariants)
+ {
+ MaxVariants = currentFlagTotalVariants;
+ }
+ }
+
///
/// Returns a formatted string containing code names, indicating which feature filters are used by the application.
///
/// Formatted string like: "CSTM+PRCNT+TIME+TRGT", "PRCNT+TRGT", etc. If no filters are used, empty string will be returned.
- public override string ToString()
+ public string CreateFiltersString()
{
if (!UsesAnyFeatureFilter())
{
@@ -85,7 +105,7 @@ public override string ToString()
{
if (sb.Length > 0)
{
- sb.Append(FilterTypeDelimiter);
+ sb.Append(RequestTracingConstants.Delimiter);
}
sb.Append(PercentageFilter);
@@ -95,7 +115,7 @@ public override string ToString()
{
if (sb.Length > 0)
{
- sb.Append(FilterTypeDelimiter);
+ sb.Append(RequestTracingConstants.Delimiter);
}
sb.Append(TimeWindowFilter);
@@ -105,7 +125,7 @@ public override string ToString()
{
if (sb.Length > 0)
{
- sb.Append(FilterTypeDelimiter);
+ sb.Append(RequestTracingConstants.Delimiter);
}
sb.Append(TargetingFilter);
@@ -113,5 +133,37 @@ public override string ToString()
return sb.ToString();
}
+
+ public string CreateFeaturesString()
+ {
+ var sb = new StringBuilder();
+
+ if (UsesSeed)
+ {
+ sb.Append(RequestTracingConstants.FeatureFlagUsesSeedTag);
+ }
+
+ if (UsesVariantConfigurationReference)
+ {
+ if (sb.Length > 0)
+ {
+ sb.Append(RequestTracingConstants.Delimiter);
+ }
+
+ sb.Append(RequestTracingConstants.FeatureFlagUsesVariantConfigurationReferenceTag);
+ }
+
+ if (UsesTelemetry)
+ {
+ if (sb.Length > 0)
+ {
+ sb.Append(RequestTracingConstants.Delimiter);
+ }
+
+ sb.Append(RequestTracingConstants.FeatureFlagUsesTelemetryTag);
+ }
+
+ return sb.ToString();
+ }
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureGroupAllocation.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureGroupAllocation.cs
index 3a9e6663..f39ca8cd 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureGroupAllocation.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureGroupAllocation.cs
@@ -2,16 +2,13 @@
// Licensed under the MIT license.
//
using System.Collections.Generic;
-using System.Text.Json.Serialization;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement
{
internal class FeatureGroupAllocation
{
- [JsonPropertyName("variant")]
public string Variant { get; set; }
- [JsonPropertyName("groups")]
public IEnumerable Groups { get; set; }
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
index 7d53c234..c6d86d84 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
@@ -1,42 +1,53 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
+
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement
{
internal class FeatureManagementConstants
{
public const string FeatureFlagMarker = ".appconfig.featureflag/";
public const string ContentType = "application/vnd.microsoft.appconfig.ff+json";
- public const string SectionName = "FeatureManagement";
- public const string EnabledFor = "EnabledFor";
- public const string Variants = "Variants";
- public const string Allocation = "Allocation";
- public const string User = "User";
- public const string Group = "Group";
- public const string Percentile = "Percentile";
- public const string Telemetry = "Telemetry";
- public const string Enabled = "Enabled";
- public const string Metadata = "Metadata";
- public const string RequirementType = "RequirementType";
- public const string Name = "Name";
- public const string Parameters = "Parameters";
- public const string Variant = "Variant";
- public const string ConfigurationValue = "ConfigurationValue";
- public const string ConfigurationReference = "ConfigurationReference";
- public const string StatusOverride = "StatusOverride";
- public const string DefaultWhenDisabled = "DefaultWhenDisabled";
- public const string DefaultWhenEnabled = "DefaultWhenEnabled";
- public const string Users = "Users";
- public const string Groups = "Groups";
- public const string From = "From";
- public const string To = "To";
- public const string Seed = "Seed";
+
+ // Feature management section keys
+ public const string FeatureManagementSectionName = "feature_management";
+ public const string FeatureFlagsSectionName = "feature_flags";
+
+ // Feature flag properties
+ public const string Id = "id";
+ public const string Enabled = "enabled";
+ public const string Conditions = "conditions";
+ public const string ClientFilters = "client_filters";
+ public const string Variants = "variants";
+ public const string Allocation = "allocation";
+ public const string UserAllocation = "user";
+ public const string GroupAllocation = "group";
+ public const string PercentileAllocation = "percentile";
+ public const string Telemetry = "telemetry";
+ public const string Metadata = "metadata";
+ public const string RequirementType = "requirement_type";
+ public const string Name = "name";
+ public const string Parameters = "parameters";
+ public const string Variant = "variant";
+ public const string ConfigurationValue = "configuration_value";
+ public const string ConfigurationReference = "configuration_reference";
+ public const string StatusOverride = "status_override";
+ public const string DefaultWhenDisabled = "default_when_disabled";
+ public const string DefaultWhenEnabled = "default_when_enabled";
+ public const string Users = "users";
+ public const string Groups = "groups";
+ public const string From = "from";
+ public const string To = "to";
+ public const string Seed = "seed";
+
+ // Telemetry metadata keys
public const string ETag = "ETag";
public const string FeatureFlagId = "FeatureFlagId";
public const string FeatureFlagReference = "FeatureFlagReference";
- public const string Status = "Status";
- public const string AlwaysOnFilter = "AlwaysOn";
- public const string Conditional = "Conditional";
- public const string Disabled = "Disabled";
+
+ // Dotnet schema keys
+ public const string DotnetSchemaSectionName = "FeatureManagement";
+ public const string DotnetSchemaEnabledFor = "EnabledFor";
+ public const string DotnetSchemaRequirementType = "RequirementType";
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
index 7b15f881..b562a904 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
@@ -16,55 +16,90 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManage
{
internal class FeatureManagementKeyValueAdapter : IKeyValueAdapter
{
- private FeatureFilterTracing _featureFilterTracing;
+ private FeatureFlagTracing _featureFlagTracing;
+ private int _featureFlagIndex = 0;
- public FeatureManagementKeyValueAdapter(FeatureFilterTracing featureFilterTracing)
+ public FeatureManagementKeyValueAdapter(FeatureFlagTracing featureFlagTracing)
{
- _featureFilterTracing = featureFilterTracing ?? throw new ArgumentNullException(nameof(featureFilterTracing));
+ _featureFlagTracing = featureFlagTracing ?? throw new ArgumentNullException(nameof(featureFlagTracing));
}
public Task>> ProcessKeyValue(ConfigurationSetting setting, Uri endpoint, Logger logger, CancellationToken cancellationToken)
{
- FeatureFlag featureFlag;
- try
+ FeatureFlag featureFlag = ParseFeatureFlag(setting.Key, setting.Value);
+
+ var keyValues = new List>();
+
+ // Check if we need to process the feature flag using the microsoft schema
+ if ((featureFlag.Variants != null && featureFlag.Variants.Any()) || featureFlag.Allocation != null || featureFlag.Telemetry != null)
{
- featureFlag = JsonSerializer.Deserialize(setting.Value);
+ keyValues = ProcessMicrosoftSchemaFeatureFlag(featureFlag, setting, endpoint);
}
- catch (JsonException e)
+ else
{
- throw new FormatException(setting.Key, e);
+ keyValues = ProcessDotnetSchemaFeatureFlag(featureFlag, setting, endpoint);
}
+ return Task.FromResult>>(keyValues);
+ }
+
+ public bool CanProcess(ConfigurationSetting setting)
+ {
+ string contentType = setting?.ContentType?.Split(';')[0].Trim();
+
+ return string.Equals(contentType, FeatureManagementConstants.ContentType) ||
+ setting.Key.StartsWith(FeatureManagementConstants.FeatureFlagMarker);
+ }
+
+ public bool NeedsRefresh()
+ {
+ return false;
+ }
+
+ public void OnChangeDetected(ConfigurationSetting setting = null)
+ {
+ return;
+ }
+
+ public void OnConfigUpdated()
+ {
+ _featureFlagIndex = 0;
+
+ return;
+ }
+
+ private List> ProcessDotnetSchemaFeatureFlag(FeatureFlag featureFlag, ConfigurationSetting setting, Uri endpoint)
+ {
var keyValues = new List>();
- string featureFlagPath = $"{FeatureManagementConstants.SectionName}:{featureFlag.Id}";
+ if (string.IsNullOrEmpty(featureFlag.Id))
+ {
+ return keyValues;
+ }
+
+ string featureFlagPath = $"{FeatureManagementConstants.DotnetSchemaSectionName}:{featureFlag.Id}";
if (featureFlag.Enabled)
{
- keyValues.Add(new KeyValuePair($"{featureFlagPath}:{FeatureManagementConstants.Status}", FeatureManagementConstants.Conditional));
-
- //if (featureFlag.Conditions?.ClientFilters == null)
- if (featureFlag.Conditions?.ClientFilters == null || !featureFlag.Conditions.ClientFilters.Any()) // workaround since we are not yet setting client filters to null
+ if (featureFlag.Conditions?.ClientFilters == null || !featureFlag.Conditions.ClientFilters.Any())
{
- keyValues.Add(new KeyValuePair($"{featureFlagPath}:{FeatureManagementConstants.EnabledFor}:{0}:{FeatureManagementConstants.Name}", FeatureManagementConstants.AlwaysOnFilter));
+ keyValues.Add(new KeyValuePair(featureFlagPath, true.ToString()));
}
else
{
- //
- // Conditionally on based on feature filters
for (int i = 0; i < featureFlag.Conditions.ClientFilters.Count; i++)
{
ClientFilter clientFilter = featureFlag.Conditions.ClientFilters[i];
- _featureFilterTracing.UpdateFeatureFilterTracing(clientFilter.Name);
+ _featureFlagTracing.UpdateFeatureFilterTracing(clientFilter.Name);
- string clientFiltersPath = $"{featureFlagPath}:{FeatureManagementConstants.EnabledFor}:{i}";
+ string clientFiltersPath = $"{featureFlagPath}:{FeatureManagementConstants.DotnetSchemaEnabledFor}:{i}";
- keyValues.Add(new KeyValuePair($"{clientFiltersPath}:{FeatureManagementConstants.Name}", clientFilter.Name));
+ keyValues.Add(new KeyValuePair($"{clientFiltersPath}:Name", clientFilter.Name));
foreach (KeyValuePair kvp in new JsonFlattener().FlattenJson(clientFilter.Parameters))
{
- keyValues.Add(new KeyValuePair($"{clientFiltersPath}:{FeatureManagementConstants.Parameters}:{kvp.Key}", kvp.Value));
+ keyValues.Add(new KeyValuePair($"{clientFiltersPath}:Parameters:{kvp.Key}", kvp.Value));
}
}
@@ -73,14 +108,67 @@ public Task>> ProcessKeyValue(Configura
if (featureFlag.Conditions.RequirementType != null)
{
keyValues.Add(new KeyValuePair(
- $"{featureFlagPath}:{FeatureManagementConstants.RequirementType}",
+ $"{featureFlagPath}:{FeatureManagementConstants.DotnetSchemaRequirementType}",
featureFlag.Conditions.RequirementType));
}
}
}
else
{
- keyValues.Add(new KeyValuePair($"{featureFlagPath}:{FeatureManagementConstants.Status}", FeatureManagementConstants.Disabled));
+ keyValues.Add(new KeyValuePair($"{featureFlagPath}", false.ToString()));
+ }
+
+ return keyValues;
+ }
+
+ private List> ProcessMicrosoftSchemaFeatureFlag(FeatureFlag featureFlag, ConfigurationSetting setting, Uri endpoint)
+ {
+ var keyValues = new List>();
+
+ if (string.IsNullOrEmpty(featureFlag.Id))
+ {
+ return keyValues;
+ }
+
+ string featureFlagPath = $"{FeatureManagementConstants.FeatureManagementSectionName}:{FeatureManagementConstants.FeatureFlagsSectionName}:{_featureFlagIndex}";
+
+ _featureFlagIndex++;
+
+ keyValues.Add(new KeyValuePair($"{featureFlagPath}:{FeatureManagementConstants.Id}", featureFlag.Id));
+
+ keyValues.Add(new KeyValuePair($"{featureFlagPath}:{FeatureManagementConstants.Enabled}", featureFlag.Enabled.ToString()));
+
+ if (featureFlag.Enabled)
+ {
+ if (featureFlag.Conditions?.ClientFilters != null && featureFlag.Conditions.ClientFilters.Any()) // workaround since we are not yet setting client filters to null
+ {
+ //
+ // Conditionally based on feature filters
+ for (int i = 0; i < featureFlag.Conditions.ClientFilters.Count; i++)
+ {
+ ClientFilter clientFilter = featureFlag.Conditions.ClientFilters[i];
+
+ _featureFlagTracing.UpdateFeatureFilterTracing(clientFilter.Name);
+
+ string clientFiltersPath = $"{featureFlagPath}:{FeatureManagementConstants.Conditions}:{FeatureManagementConstants.ClientFilters}:{i}";
+
+ keyValues.Add(new KeyValuePair($"{clientFiltersPath}:{FeatureManagementConstants.Name}", clientFilter.Name));
+
+ foreach (KeyValuePair kvp in new JsonFlattener().FlattenJson(clientFilter.Parameters))
+ {
+ keyValues.Add(new KeyValuePair($"{clientFiltersPath}:{FeatureManagementConstants.Parameters}:{kvp.Key}", kvp.Value));
+ }
+ }
+
+ //
+ // process RequirementType only when filters are not empty
+ if (featureFlag.Conditions.RequirementType != null)
+ {
+ keyValues.Add(new KeyValuePair(
+ $"{featureFlagPath}:{FeatureManagementConstants.Conditions}:{FeatureManagementConstants.RequirementType}",
+ featureFlag.Conditions.RequirementType));
+ }
+ }
}
if (featureFlag.Variants != null)
@@ -101,6 +189,8 @@ public Task>> ProcessKeyValue(Configura
if (featureVariant.ConfigurationReference != null)
{
+ _featureFlagTracing.UsesVariantConfigurationReference = true;
+
keyValues.Add(new KeyValuePair($"{variantsPath}:{FeatureManagementConstants.ConfigurationReference}", featureVariant.ConfigurationReference));
}
@@ -111,6 +201,8 @@ public Task>> ProcessKeyValue(Configura
i++;
}
+
+ _featureFlagTracing.NotifyMaxVariants(i);
}
if (featureFlag.Allocation != null)
@@ -135,13 +227,13 @@ public Task>> ProcessKeyValue(Configura
foreach (FeatureUserAllocation userAllocation in allocation.User)
{
- keyValues.Add(new KeyValuePair($"{allocationPath}:{FeatureManagementConstants.User}:{i}:{FeatureManagementConstants.Variant}", userAllocation.Variant));
+ keyValues.Add(new KeyValuePair($"{allocationPath}:{FeatureManagementConstants.UserAllocation}:{i}:{FeatureManagementConstants.Variant}", userAllocation.Variant));
int j = 0;
foreach (string user in userAllocation.Users)
{
- keyValues.Add(new KeyValuePair($"{allocationPath}:{FeatureManagementConstants.User}:{i}:{FeatureManagementConstants.Users}:{j}", user));
+ keyValues.Add(new KeyValuePair($"{allocationPath}:{FeatureManagementConstants.UserAllocation}:{i}:{FeatureManagementConstants.Users}:{j}", user));
j++;
}
@@ -156,13 +248,13 @@ public Task>> ProcessKeyValue(Configura
foreach (FeatureGroupAllocation groupAllocation in allocation.Group)
{
- keyValues.Add(new KeyValuePair($"{allocationPath}:{FeatureManagementConstants.Group}:{i}:{FeatureManagementConstants.Variant}", groupAllocation.Variant));
+ keyValues.Add(new KeyValuePair($"{allocationPath}:{FeatureManagementConstants.GroupAllocation}:{i}:{FeatureManagementConstants.Variant}", groupAllocation.Variant));
int j = 0;
foreach (string group in groupAllocation.Groups)
{
- keyValues.Add(new KeyValuePair($"{allocationPath}:{FeatureManagementConstants.Group}:{i}:{FeatureManagementConstants.Groups}:{j}", group));
+ keyValues.Add(new KeyValuePair($"{allocationPath}:{FeatureManagementConstants.GroupAllocation}:{i}:{FeatureManagementConstants.Groups}:{j}", group));
j++;
}
@@ -177,11 +269,11 @@ public Task>> ProcessKeyValue(Configura
foreach (FeaturePercentileAllocation percentileAllocation in allocation.Percentile)
{
- keyValues.Add(new KeyValuePair($"{allocationPath}:{FeatureManagementConstants.Percentile}:{i}:{FeatureManagementConstants.Variant}", percentileAllocation.Variant));
+ keyValues.Add(new KeyValuePair($"{allocationPath}:{FeatureManagementConstants.PercentileAllocation}:{i}:{FeatureManagementConstants.Variant}", percentileAllocation.Variant));
- keyValues.Add(new KeyValuePair($"{allocationPath}:{FeatureManagementConstants.Percentile}:{i}:{FeatureManagementConstants.From}", percentileAllocation.From.ToString()));
+ keyValues.Add(new KeyValuePair($"{allocationPath}:{FeatureManagementConstants.PercentileAllocation}:{i}:{FeatureManagementConstants.From}", percentileAllocation.From.ToString()));
- keyValues.Add(new KeyValuePair($"{allocationPath}:{FeatureManagementConstants.Percentile}:{i}:{FeatureManagementConstants.To}", percentileAllocation.To.ToString()));
+ keyValues.Add(new KeyValuePair($"{allocationPath}:{FeatureManagementConstants.PercentileAllocation}:{i}:{FeatureManagementConstants.To}", percentileAllocation.To.ToString()));
i++;
}
@@ -189,6 +281,8 @@ public Task>> ProcessKeyValue(Configura
if (allocation.Seed != null)
{
+ _featureFlagTracing.UsesSeed = true;
+
keyValues.Add(new KeyValuePair($"{allocationPath}:{FeatureManagementConstants.Seed}", allocation.Seed));
}
}
@@ -201,6 +295,8 @@ public Task>> ProcessKeyValue(Configura
if (telemetry.Enabled)
{
+ _featureFlagTracing.UsesTelemetry = true;
+
if (telemetry.Metadata != null)
{
foreach (KeyValuePair kvp in telemetry.Metadata)
@@ -226,25 +322,984 @@ public Task>> ProcessKeyValue(Configura
}
}
- return Task.FromResult>>(keyValues);
+ return keyValues;
}
- public bool CanProcess(ConfigurationSetting setting)
+ private FormatException CreateFeatureFlagFormatException(string jsonPropertyName, string settingKey, string foundJsonValueKind, string expectedJsonValueKind)
{
- string contentType = setting?.ContentType?.Split(';')[0].Trim();
+ return new FormatException(string.Format(
+ ErrorMessages.FeatureFlagInvalidJsonProperty,
+ jsonPropertyName,
+ settingKey,
+ foundJsonValueKind,
+ expectedJsonValueKind));
+ }
- return string.Equals(contentType, FeatureManagementConstants.ContentType) ||
- setting.Key.StartsWith(FeatureManagementConstants.FeatureFlagMarker);
+ private FeatureFlag ParseFeatureFlag(string settingKey, string settingValue)
+ {
+ var featureFlag = new FeatureFlag();
+
+ var reader = new Utf8JsonReader(System.Text.Encoding.UTF8.GetBytes(settingValue));
+
+ try
+ {
+ if (reader.Read() && reader.TokenType != JsonTokenType.StartObject)
+ {
+ throw new FormatException(string.Format(ErrorMessages.FeatureFlagInvalidFormat, settingKey));
+ }
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ string propertyName = reader.GetString();
+
+ switch (propertyName)
+ {
+ case FeatureManagementConstants.Id:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ featureFlag.Id = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Id,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.Enabled:
+ {
+ if (reader.Read() && (reader.TokenType == JsonTokenType.False || reader.TokenType == JsonTokenType.True))
+ {
+ featureFlag.Enabled = reader.GetBoolean();
+ }
+ else if (reader.TokenType == JsonTokenType.String && bool.TryParse(reader.GetString(), out bool enabled))
+ {
+ featureFlag.Enabled = enabled;
+ }
+ else
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Enabled,
+ settingKey,
+ reader.TokenType.ToString(),
+ $"{JsonTokenType.True}' or '{JsonTokenType.False}");
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.Conditions:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.StartObject)
+ {
+ featureFlag.Conditions = ParseFeatureConditions(ref reader, settingKey);
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Conditions,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartObject.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.Allocation:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.StartObject)
+ {
+ featureFlag.Allocation = ParseFeatureAllocation(ref reader, settingKey);
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Allocation,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartObject.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.Variants:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.StartArray)
+ {
+ List variants = new List();
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
+ {
+ int i = 0;
+
+ if (reader.TokenType == JsonTokenType.StartObject)
+ {
+ FeatureVariant featureVariant = ParseFeatureVariant(ref reader, settingKey);
+
+ if (featureVariant.Name != null)
+ {
+ variants.Add(featureVariant);
+ }
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ $"{FeatureManagementConstants.Variants}[{i}]",
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartObject.ToString());
+ }
+
+ i++;
+ }
+
+ featureFlag.Variants = variants;
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Variants,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartArray.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.Telemetry:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.StartObject)
+ {
+ featureFlag.Telemetry = ParseFeatureTelemetry(ref reader, settingKey);
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Telemetry,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartObject.ToString());
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+
+ break;
+ }
+ }
+ }
+ catch (JsonException e)
+ {
+ throw new FormatException(settingKey, e);
+ }
+
+ return featureFlag;
}
- public void InvalidateCache(ConfigurationSetting setting = null)
+ private FeatureConditions ParseFeatureConditions(ref Utf8JsonReader reader, string settingKey)
{
- return;
+ var featureConditions = new FeatureConditions();
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ string conditionsPropertyName = reader.GetString();
+
+ switch (conditionsPropertyName)
+ {
+ case FeatureManagementConstants.ClientFilters:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.StartArray)
+ {
+ int i = 0;
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
+ {
+ if (reader.TokenType == JsonTokenType.StartObject)
+ {
+ ClientFilter clientFilter = ParseClientFilter(ref reader, settingKey);
+
+ if (clientFilter.Name != null ||
+ (clientFilter.Parameters.ValueKind == JsonValueKind.Object &&
+ clientFilter.Parameters.EnumerateObject().Any()))
+ {
+ featureConditions.ClientFilters.Add(clientFilter);
+ }
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ $"{FeatureManagementConstants.ClientFilters}[{i}]",
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartObject.ToString());
+ }
+
+ i++;
+ }
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.ClientFilters,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartArray.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.RequirementType:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ featureConditions.RequirementType = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.RequirementType,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+
+ break;
+ }
+ }
+
+ return featureConditions;
}
- public bool NeedsRefresh()
+ private ClientFilter ParseClientFilter(ref Utf8JsonReader reader, string settingKey)
{
- return false;
+ var clientFilter = new ClientFilter();
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ string clientFiltersPropertyName = reader.GetString();
+
+ switch (clientFiltersPropertyName)
+ {
+ case FeatureManagementConstants.Name:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ clientFilter.Name = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Name,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.Parameters:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.StartObject)
+ {
+ clientFilter.Parameters = JsonDocument.ParseValue(ref reader).RootElement;
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Parameters,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartObject.ToString());
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+
+ break;
+ }
+ }
+
+ return clientFilter;
+ }
+
+ private FeatureAllocation ParseFeatureAllocation(ref Utf8JsonReader reader, string settingKey)
+ {
+ var featureAllocation = new FeatureAllocation();
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ string allocationPropertyName = reader.GetString();
+
+ switch (allocationPropertyName)
+ {
+ case FeatureManagementConstants.DefaultWhenDisabled:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ featureAllocation.DefaultWhenDisabled = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.DefaultWhenDisabled,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.DefaultWhenEnabled:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ featureAllocation.DefaultWhenEnabled = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.DefaultWhenEnabled,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.UserAllocation:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.StartArray)
+ {
+ List userAllocations = new List();
+
+ int i = 0;
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
+ {
+ if (reader.TokenType == JsonTokenType.StartObject)
+ {
+ FeatureUserAllocation featureUserAllocation = ParseFeatureUserAllocation(ref reader, settingKey);
+
+ if (featureUserAllocation.Variant != null ||
+ (featureUserAllocation.Users != null &&
+ featureUserAllocation.Users.Any()))
+ {
+ userAllocations.Add(featureUserAllocation);
+ }
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ $"{FeatureManagementConstants.UserAllocation}[{i}]",
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartObject.ToString());
+ }
+
+ i++;
+ }
+
+ featureAllocation.User = userAllocations;
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.UserAllocation,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartArray.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.GroupAllocation:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.StartArray)
+ {
+ List groupAllocations = new List();
+
+ int i = 0;
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
+ {
+ if (reader.TokenType == JsonTokenType.StartObject)
+ {
+ FeatureGroupAllocation featureGroupAllocation = ParseFeatureGroupAllocation(ref reader, settingKey);
+
+ if (featureGroupAllocation.Variant != null ||
+ (featureGroupAllocation.Groups != null &&
+ featureGroupAllocation.Groups.Any()))
+ {
+ groupAllocations.Add(featureGroupAllocation);
+ }
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ $"{FeatureManagementConstants.GroupAllocation}[{i}]",
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartObject.ToString());
+ }
+
+ i++;
+ }
+
+ featureAllocation.Group = groupAllocations;
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.GroupAllocation,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartArray.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.PercentileAllocation:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.StartArray)
+ {
+ List percentileAllocations = new List();
+
+ int i = 0;
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
+ {
+ if (reader.TokenType == JsonTokenType.StartObject)
+ {
+ FeaturePercentileAllocation featurePercentileAllocation = ParseFeaturePercentileAllocation(ref reader, settingKey);
+
+ percentileAllocations.Add(featurePercentileAllocation);
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ $"{FeatureManagementConstants.PercentileAllocation}[{i}]",
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartObject.ToString());
+ }
+
+ i++;
+ }
+
+ featureAllocation.Percentile = percentileAllocations;
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.PercentileAllocation,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartArray.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.Seed:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ featureAllocation.Seed = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Seed,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+
+ break;
+ }
+ }
+
+ return featureAllocation;
+ }
+
+ private FeatureUserAllocation ParseFeatureUserAllocation(ref Utf8JsonReader reader, string settingKey)
+ {
+ var featureUserAllocation = new FeatureUserAllocation();
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ string userAllocationPropertyName = reader.GetString();
+
+ switch (userAllocationPropertyName)
+ {
+ case FeatureManagementConstants.Variant:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ featureUserAllocation.Variant = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Variant,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.Users:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.StartArray)
+ {
+ List users = new List();
+
+ int i = 0;
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ users.Add(reader.GetString());
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ $"{FeatureManagementConstants.Users}[{i}]",
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ i++;
+ }
+
+ featureUserAllocation.Users = users;
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Users,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartArray.ToString());
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+
+ break;
+ }
+ }
+
+ return featureUserAllocation;
+ }
+
+ private FeatureGroupAllocation ParseFeatureGroupAllocation(ref Utf8JsonReader reader, string settingKey)
+ {
+ var featureGroupAllocation = new FeatureGroupAllocation();
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ string groupAllocationPropertyName = reader.GetString();
+
+ switch (groupAllocationPropertyName)
+ {
+ case FeatureManagementConstants.Variant:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ featureGroupAllocation.Variant = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Variant,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.Groups:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.StartArray)
+ {
+ List groups = new List();
+
+ int i = 0;
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ groups.Add(reader.GetString());
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ $"{FeatureManagementConstants.Groups}[{i}]",
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ i++;
+ }
+
+ featureGroupAllocation.Groups = groups;
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Groups,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartArray.ToString());
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+
+ break;
+ }
+ }
+
+ return featureGroupAllocation;
+ }
+
+ private FeaturePercentileAllocation ParseFeaturePercentileAllocation(ref Utf8JsonReader reader, string settingKey)
+ {
+ var featurePercentileAllocation = new FeaturePercentileAllocation();
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ string percentileAllocationPropertyName = reader.GetString();
+
+ switch (percentileAllocationPropertyName)
+ {
+ case FeatureManagementConstants.Variant:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ featurePercentileAllocation.Variant = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Variant,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.From:
+ {
+ if (reader.Read() &&
+ ((reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out int from)) ||
+ (reader.TokenType == JsonTokenType.String && int.TryParse(reader.GetString(), out from))))
+ {
+ featurePercentileAllocation.From = from;
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.From,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.Number.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.To:
+ {
+ if (reader.Read() &&
+ ((reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out int to)) ||
+ (reader.TokenType == JsonTokenType.String && int.TryParse(reader.GetString(), out to))))
+ {
+ featurePercentileAllocation.To = to;
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.To,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.Number.ToString());
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+
+ break;
+ }
+ }
+
+ return featurePercentileAllocation;
+ }
+
+ private FeatureVariant ParseFeatureVariant(ref Utf8JsonReader reader, string settingKey)
+ {
+ var featureVariant = new FeatureVariant();
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ string variantPropertyName = reader.GetString();
+
+ switch (variantPropertyName)
+ {
+ case FeatureManagementConstants.Name:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ featureVariant.Name = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Name,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.ConfigurationReference:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ featureVariant.ConfigurationReference = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.ConfigurationReference,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.ConfigurationValue:
+ {
+ if (reader.Read())
+ {
+ featureVariant.ConfigurationValue = JsonDocument.ParseValue(ref reader).RootElement;
+ }
+
+ break;
+ }
+
+
+ case FeatureManagementConstants.StatusOverride:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ featureVariant.StatusOverride = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.StatusOverride,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+
+ break;
+ }
+ }
+
+ return featureVariant;
+ }
+
+ private FeatureTelemetry ParseFeatureTelemetry(ref Utf8JsonReader reader, string settingKey)
+ {
+ var featureTelemetry = new FeatureTelemetry();
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ string telemetryPropertyName = reader.GetString();
+
+ switch (telemetryPropertyName)
+ {
+ case FeatureManagementConstants.Enabled:
+ {
+ if (reader.Read() && (reader.TokenType == JsonTokenType.False || reader.TokenType == JsonTokenType.True))
+ {
+ featureTelemetry.Enabled = reader.GetBoolean();
+ }
+ else if (reader.TokenType == JsonTokenType.String && bool.TryParse(reader.GetString(), out bool enabled))
+ {
+ featureTelemetry.Enabled = enabled;
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Enabled,
+ settingKey,
+ reader.TokenType.ToString(),
+ $"{JsonTokenType.True}' or '{JsonTokenType.False}");
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.Metadata:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.StartObject)
+ {
+ featureTelemetry.Metadata = new Dictionary();
+
+ while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
+ {
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ {
+ continue;
+ }
+
+ string metadataPropertyName = reader.GetString();
+
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ featureTelemetry.Metadata[metadataPropertyName] = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ metadataPropertyName,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+ }
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.Metadata,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartObject.ToString());
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+
+ break;
+ }
+ }
+
+ return featureTelemetry;
}
private static string CalculateFeatureFlagId(string key, string label)
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeaturePercentileAllocation.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeaturePercentileAllocation.cs
index 8cb7e474..1d107dba 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeaturePercentileAllocation.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeaturePercentileAllocation.cs
@@ -1,20 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
-using System.Collections.Generic;
-using System.Text.Json.Serialization;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement
{
internal class FeaturePercentileAllocation
{
- [JsonPropertyName("variant")]
public string Variant { get; set; }
- [JsonPropertyName("from")]
public double From { get; set; }
- [JsonPropertyName("to")]
public double To { get; set; }
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureTelemetry.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureTelemetry.cs
index c95178dd..09d8dd71 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureTelemetry.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureTelemetry.cs
@@ -3,16 +3,13 @@
//
using System.Collections.Generic;
-using System.Text.Json.Serialization;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement
{
internal class FeatureTelemetry
{
- [JsonPropertyName("enabled")]
public bool Enabled { get; set; }
- [JsonPropertyName("metadata")]
- public IReadOnlyDictionary Metadata { get; set; }
+ public IDictionary Metadata { get; set; }
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureUserAllocation.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureUserAllocation.cs
index e781a5c6..fc403d6e 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureUserAllocation.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureUserAllocation.cs
@@ -2,16 +2,13 @@
// Licensed under the MIT license.
//
using System.Collections.Generic;
-using System.Text.Json.Serialization;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement
{
internal class FeatureUserAllocation
{
- [JsonPropertyName("variant")]
public string Variant { get; set; }
- [JsonPropertyName("users")]
public IEnumerable Users { get; set; }
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureVariant.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureVariant.cs
index c590f56b..87c5c0b1 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureVariant.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureVariant.cs
@@ -2,22 +2,17 @@
// Licensed under the MIT license.
//
using System.Text.Json;
-using System.Text.Json.Serialization;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement
{
internal class FeatureVariant
{
- [JsonPropertyName("name")]
public string Name { get; set; }
- [JsonPropertyName("configuration_value")]
public JsonElement ConfigurationValue { get; set; }
- [JsonPropertyName("configuration_reference")]
public string ConfigurationReference { get; set; }
- [JsonPropertyName("status_override")]
public string StatusOverride { get; set; }
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IKeyValueAdapter.cs
index 8da0ee55..de13314e 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IKeyValueAdapter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/IKeyValueAdapter.cs
@@ -15,7 +15,9 @@ internal interface IKeyValueAdapter
bool CanProcess(ConfigurationSetting setting);
- void InvalidateCache(ConfigurationSetting setting = null);
+ void OnChangeDetected(ConfigurationSetting setting = null);
+
+ void OnConfigUpdated();
bool NeedsRefresh();
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs
index fed06f11..574f64fa 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs
@@ -30,10 +30,15 @@ public Task>> ProcessKeyValue(Configura
}
string rootJson = $"{{\"{setting.Key}\":{setting.Value}}}";
- JsonElement jsonData;
+
+ List> keyValuePairs = new List>();
+
try
{
- jsonData = JsonSerializer.Deserialize(rootJson);
+ using (JsonDocument document = JsonDocument.Parse(rootJson))
+ {
+ keyValuePairs = new JsonFlattener().FlattenJson(document.RootElement);
+ }
}
catch (JsonException)
{
@@ -41,7 +46,7 @@ public Task>> ProcessKeyValue(Configura
return Task.FromResult>>(new[] { new KeyValuePair(setting.Key, setting.Value) });
}
- return Task.FromResult>>(new JsonFlattener().FlattenJson(jsonData));
+ return Task.FromResult>>(keyValuePairs);
}
public bool CanProcess(ConfigurationSetting setting)
@@ -90,7 +95,12 @@ public bool CanProcess(ConfigurationSetting setting)
return false;
}
- public void InvalidateCache(ConfigurationSetting setting = null)
+ public void OnChangeDetected(ConfigurationSetting setting = null)
+ {
+ return;
+ }
+
+ public void OnConfigUpdated()
{
return;
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/LogHelper.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/LogHelper.cs
index 113a1499..11442dcb 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/LogHelper.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/LogHelper.cs
@@ -88,6 +88,10 @@ public static string BuildLastEndpointFailedMessage(string endpoint)
public static string BuildFallbackClientLookupFailMessage(string exceptionMessage)
{
return $"{LoggingConstants.FallbackClientLookupError}\n{exceptionMessage}";
+ }
+ public static string BuildRefreshFailedDueToFormattingErrorMessage(string exceptionMessage)
+ {
+ return $"{LoggingConstants.RefreshFailedDueToFormattingError}\n{exceptionMessage}";
}
}
}
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 d5b85ce4..31e1e854 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
@@ -14,9 +14,9 @@
-
+
-
+
@@ -34,7 +34,7 @@
- 8.0.0-preview.2
+ 8.0.0-preview.3
@@ -47,7 +47,8 @@
..\..\AzureAppConfigurationRules.ruleset
- True
+ true
+ true
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Models/KeyValueWatcher.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Models/KeyValueWatcher.cs
index 121a35be..616f8bcd 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Models/KeyValueWatcher.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Models/KeyValueWatcher.cs
@@ -26,12 +26,12 @@ internal class KeyValueWatcher
///
/// The minimum time that must elapse before the key-value is refreshed.
///
- public TimeSpan CacheExpirationInterval { get; set; }
+ public TimeSpan RefreshInterval { get; set; }
///
- /// The cache expiration time for the key-value.
+ /// The next time when this key-value can be refreshed.
///
- public DateTimeOffset CacheExpires { get; set; }
+ public DateTimeOffset NextRefreshTime { get; set; }
public override bool Equals(object obj)
{
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs
index 27af8fae..bbf9f667 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs
@@ -33,9 +33,9 @@ internal class RequestTracingOptions
public int ReplicaCount { get; set; } = 0;
///
- /// Type of feature filters used by the application.
+ /// Information about feature flags in the application, like filter and variant usage.
///
- public FeatureFilterTracing FilterTracing { get; set; } = new FeatureFilterTracing();
+ public FeatureFlagTracing FeatureFlagTracing { get; set; } = new FeatureFlagTracing();
///
/// Version of the Microsoft.FeatureManagement assembly, if present in the application.
@@ -46,5 +46,10 @@ internal class RequestTracingOptions
/// Version of the Microsoft.FeatureManagement.AspNetCore assembly, if present in the application.
///
public string FeatureManagementAspNetCoreVersion { get; set; }
+
+ ///
+ /// Flag to indicate whether Microsoft.AspNetCore.SignalR assembly is present in the application.
+ ///
+ public bool IsSignalRUsed { get; set; } = false;
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs
index 3ca5271f..6d5ae208 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Reflection;
using System.Security;
using System.Text;
using System.Threading.Tasks;
@@ -79,8 +80,28 @@ public static string GetAssemblyVersion(string assemblyName)
{
if (!string.IsNullOrEmpty(assemblyName))
{
- // Return the version using only the first 3 fields and remove additional characters
- return AppDomain.CurrentDomain.GetAssemblies().SingleOrDefault(assembly => assembly.GetName().Name == assemblyName)?.GetName().Version?.ToString(3).Trim('{', '}');
+ Assembly infoVersionAttribute = AppDomain.CurrentDomain.GetAssemblies().SingleOrDefault(assembly => assembly.GetName().Name == assemblyName);
+
+ if (infoVersionAttribute != null)
+ {
+ string informationalVersion = infoVersionAttribute.GetCustomAttribute()?.InformationalVersion;
+
+ if (string.IsNullOrEmpty(informationalVersion))
+ {
+ return null;
+ }
+
+ // Commit information is appended to the informational version starting with a '+', so we remove
+ // the commit information to get just the full name of the version.
+ int plusIndex = informationalVersion.IndexOf('+');
+
+ if (plusIndex != -1)
+ {
+ informationalVersion = informationalVersion.Substring(0, plusIndex);
+ }
+
+ return informationalVersion;
+ }
}
return null;
@@ -135,9 +156,19 @@ private static string CreateCorrelationContextHeader(RequestType requestType, Re
correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.EnvironmentKey, RequestTracingConstants.DevEnvironmentValue));
}
- if (requestTracingOptions.FilterTracing.UsesAnyFeatureFilter())
+ if (requestTracingOptions.FeatureFlagTracing.UsesAnyFeatureFilter())
+ {
+ correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.FilterTypeKey, requestTracingOptions.FeatureFlagTracing.CreateFiltersString()));
+ }
+
+ if (requestTracingOptions.FeatureFlagTracing.MaxVariants > 0)
+ {
+ correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.FeatureFlagMaxVariantsKey, requestTracingOptions.FeatureFlagTracing.MaxVariants.ToString()));
+ }
+
+ if (requestTracingOptions.FeatureFlagTracing.UsesAnyTracingFeature())
{
- correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.FilterTypeKey, requestTracingOptions.FilterTracing.ToString()));
+ correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.FeatureFlagFeaturesKey, requestTracingOptions.FeatureFlagTracing.CreateFeaturesString()));
}
if (requestTracingOptions.FeatureManagementVersion != null)
@@ -160,6 +191,11 @@ private static string CreateCorrelationContextHeader(RequestType requestType, Re
correlationContextTags.Add(RequestTracingConstants.KeyVaultRefreshConfiguredTag);
}
+ if (requestTracingOptions.IsSignalRUsed)
+ {
+ correlationContextTags.Add(RequestTracingConstants.SignalRUsedTag);
+ }
+
var sb = new StringBuilder();
foreach (KeyValuePair kvp in correlationContextKeyValues)
diff --git a/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj b/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj
index ef89659c..5d0a8d25 100644
--- a/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj
+++ b/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj
@@ -1,7 +1,7 @@
- net6.0;net7.0
+ net6.0;net8.0
8.0
false
true
@@ -10,11 +10,14 @@
-
-
-
-
-
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
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 0340afd5..6ad45820 100644
--- a/tests/Tests.AzureAppConfiguration.Functions.Worker/Tests.AzureAppConfiguration.Functions.Worker.csproj
+++ b/tests/Tests.AzureAppConfiguration.Functions.Worker/Tests.AzureAppConfiguration.Functions.Worker.csproj
@@ -1,7 +1,7 @@
- net6.0;net7.0
+ net6.0;net8.0
8.0
false
true
@@ -10,11 +10,14 @@
-
-
-
-
-
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/tests/Tests.AzureAppConfiguration/FailoverTests.cs b/tests/Tests.AzureAppConfiguration/FailoverTests.cs
index d302e407..4ddbd58f 100644
--- a/tests/Tests.AzureAppConfiguration/FailoverTests.cs
+++ b/tests/Tests.AzureAppConfiguration/FailoverTests.cs
@@ -23,7 +23,7 @@ public class FailOverTests
contentType: "text");
[Fact]
- public void FailOverTests_ReturnsAllClientsIfAllBackedOff()
+ public async Task FailOverTests_ReturnsAllClientsIfAllBackedOff()
{
// Arrange
IConfigurationRefresher refresher = null;
@@ -68,7 +68,7 @@ public void FailOverTests_ReturnsAllClientsIfAllBackedOff()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
options.ReplicaDiscoveryEnabled = false;
@@ -85,7 +85,7 @@ public void FailOverTests_ReturnsAllClientsIfAllBackedOff()
// Assert the inner request failed exceptions
Assert.True((exception.InnerException as AggregateException)?.InnerExceptions?.All(e => e is RequestFailedException) ?? false);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
// The client manager should have called RefreshClients when all clients were backed off
Assert.Equal(1, configClientManager.RefreshClientsCalled);
@@ -133,7 +133,7 @@ public void FailOverTests_PropagatesNonFailOverableExceptions()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -144,7 +144,7 @@ public void FailOverTests_PropagatesNonFailOverableExceptions()
}
[Fact]
- public void FailOverTests_BackoffStateIsUpdatedOnSuccessfulRequest()
+ public async Task FailOverTests_BackoffStateIsUpdatedOnSuccessfulRequest()
{
// Arrange
IConfigurationRefresher refresher = null;
@@ -193,13 +193,13 @@ public void FailOverTests_BackoffStateIsUpdatedOnSuccessfulRequest()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
}).Build();
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
// The first client should not have been called during refresh
mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(0));
@@ -211,7 +211,7 @@ public void FailOverTests_BackoffStateIsUpdatedOnSuccessfulRequest()
// Wait for client 1 backoff to end
Thread.Sleep(2500);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
// The first client should have been called now with refresh after the backoff time ends
mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1));
@@ -258,7 +258,7 @@ public void FailOverTests_AutoFailover()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
})
@@ -272,7 +272,8 @@ public void FailOverTests_ValidateEndpoints()
new[] { new Uri("https://foobar.azconfig.io") },
new DefaultAzureCredential(),
new ConfigurationClientOptions(),
- true);
+ true,
+ false);
Assert.True(configClientManager.IsValidEndpoint("azure.azconfig.io"));
Assert.True(configClientManager.IsValidEndpoint("appconfig.azconfig.io"));
@@ -287,7 +288,8 @@ public void FailOverTests_ValidateEndpoints()
new[] { new Uri("https://foobar.appconfig.azure.com") },
new DefaultAzureCredential(),
new ConfigurationClientOptions(),
- true);
+ true,
+ false);
Assert.True(configClientManager2.IsValidEndpoint("azure.appconfig.azure.com"));
Assert.True(configClientManager2.IsValidEndpoint("azure.z1.appconfig.azure.com"));
@@ -302,7 +304,8 @@ public void FailOverTests_ValidateEndpoints()
new[] { new Uri("https://foobar.azconfig-test.io") },
new DefaultAzureCredential(),
new ConfigurationClientOptions(),
- true);
+ true,
+ false);
Assert.False(configClientManager3.IsValidEndpoint("azure.azconfig-test.io"));
Assert.False(configClientManager3.IsValidEndpoint("azure.azconfig.io"));
@@ -311,7 +314,8 @@ public void FailOverTests_ValidateEndpoints()
new[] { new Uri("https://foobar.z1.appconfig-test.azure.com") },
new DefaultAzureCredential(),
new ConfigurationClientOptions(),
- true);
+ true,
+ false);
Assert.False(configClientManager4.IsValidEndpoint("foobar.z2.appconfig-test.azure.com"));
Assert.False(configClientManager4.IsValidEndpoint("foobar.appconfig-test.azure.com"));
@@ -325,7 +329,8 @@ public void FailOverTests_GetNoDynamicClient()
new[] { new Uri("https://azure.azconfig.io") },
new DefaultAzureCredential(),
new ConfigurationClientOptions(),
- true);
+ true,
+ false);
var clients = configClientManager.GetClients();
@@ -372,7 +377,7 @@ public void FailOverTests_FailOverOnKeyVaultReferenceException()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
diff --git a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
index 8c2d2bbb..8e871ef2 100644
--- a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
+++ b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
@@ -15,9 +15,9 @@
using System.Collections.Generic;
using System.Diagnostics.Tracing;
using System.Linq;
-using System.Net;
using System.Security.Cryptography;
using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
@@ -88,7 +88,239 @@ public class FeatureManagementTests
contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"));
- List _featureFlagCollection = new List
+ List _nullOrMissingConditionsFeatureFlagCollection = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "NullParameters",
+ value: @"
+ {
+ ""id"": ""NullParameters"",
+ ""description"": """",
+ ""display_name"": ""Null Parameters"",
+ ""enabled"": true,
+ ""conditions"": {
+ ""client_filters"": [
+ {
+ ""name"": ""Filter"",
+ ""parameters"": null
+ }
+ ]
+ }
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "NullConditions",
+ value: @"
+ {
+ ""id"": ""NullConditions"",
+ ""description"": """",
+ ""display_name"": ""Null Conditions"",
+ ""enabled"": true,
+ ""conditions"": null
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "NullClientFilters",
+ value: @"
+ {
+ ""id"": ""NullClientFilters"",
+ ""description"": """",
+ ""display_name"": ""Null Client Filters"",
+ ""enabled"": true,
+ ""conditions"": {
+ ""client_filters"": null
+ }
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "NoConditions",
+ value: @"
+ {
+ ""id"": ""NoConditions"",
+ ""description"": """",
+ ""display_name"": ""No Conditions"",
+ ""enabled"": true
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "EmptyConditions",
+ value: @"
+ {
+ ""id"": ""EmptyConditions"",
+ ""description"": """",
+ ""display_name"": ""Empty Conditions"",
+ ""conditions"": {},
+ ""enabled"": true
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "EmptyClientFilter",
+ value: @"
+ {
+ ""id"": ""EmptyClientFilter"",
+ ""description"": """",
+ ""display_name"": ""Empty Client Filter"",
+ ""conditions"": {
+ ""client_filters"": [
+ {}
+ ]
+ },
+ ""enabled"": true
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"))
+ };
+
+ List _validFormatFeatureFlagCollection = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "AdditionalProperty",
+ value: @"
+ {
+ ""id"": ""AdditionalProperty"",
+ ""description"": ""Should not throw an exception, additional properties are skipped."",
+ ""ignored_object"": {
+ ""id"": false
+ },
+ ""enabled"": true,
+ ""conditions"": {}
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "DuplicateProperty",
+ value: @"
+ {
+ ""id"": ""DuplicateProperty"",
+ ""description"": ""Should not throw an exception, last of duplicate properties will win."",
+ ""enabled"": false,
+ ""enabled"": true,
+ ""conditions"": {}
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "AllowNullRequirementType",
+ value: @"
+ {
+ ""id"": ""AllowNullRequirementType"",
+ ""description"": ""Should not throw an exception, requirement type is allowed as null."",
+ ""enabled"": true,
+ ""conditions"": {
+ ""requirement_type"": null
+ }
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"))
+ };
+
+ List _invalidFormatFeatureFlagCollection = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "MissingClosingBracket1",
+ value: @"
+ {
+ ""id"": ""MissingClosingBracket1"",
+ ""description"": ""Should throw an exception, invalid end of json."",
+ ""enabled"": true,
+ ""conditions"": {}
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "MissingClosingBracket2",
+ value: @"
+ {
+ ""id"": ""MissingClosingBracket2"",
+ ""description"": ""Should throw an exception, invalid end of conditions object."",
+ ""conditions"": {,
+ ""enabled"": true
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "MissingClosingBracket3",
+ value: @"
+ {
+ ""id"": ""MissingClosingBracket3"",
+ ""description"": ""Should throw an exception, no closing bracket on client filters array."",
+ ""conditions"": {
+ ""client_filters"": [
+ },
+ ""enabled"": true
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "MissingOpeningBracket1",
+ value: @"
+ {
+ ""id"": ""MissingOpeningBracket1"",
+ ""description"": ""Should throw an exception, no opening bracket on conditions object."",
+ ""conditions"": },
+ ""enabled"": true
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "MissingOpeningBracket2",
+ value: @"
+ {
+ ""id"": ""MissingOpeningBracket2"",
+ ""description"": ""Should throw an exception, no opening bracket on client filters array."",
+ ""conditions"": {
+ ""client_filters"": ]
+ },
+ ""enabled"": true
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"))
+ };
+
+ List _featureFlagCollection = new List
{
ConfigurationModelFactory.ConfigurationSetting(
key: FeatureManagementConstants.FeatureFlagMarker + "App1_Feature1",
@@ -188,95 +420,85 @@ public class FeatureManagementTests
eTag: new ETag("0a76e3d7-7ec1-4e37-883c-9ea6d0d89e63"),
contentType: "text");
- private ConfigurationSetting _variantsKv1 = ConfigurationModelFactory.ConfigurationSetting(
- key: FeatureManagementConstants.FeatureFlagMarker + "VariantsFeature1",
- value: @"
- {
- ""id"": ""VariantsFeature1"",
- ""description"": """",
- ""display_name"": ""Variants Feature 1"",
- ""enabled"": true,
- ""conditions"": {
- ""client_filters"": [
- ]
- },
- ""variants"": [
- {
- ""name"": ""Big"",
- ""configuration_value"": ""600px""
- },
- {
- ""name"": ""Small"",
- ""configuration_reference"": ""ShoppingCart:Small"",
- ""status_override"": ""Disabled""
- }
- ],
- ""allocation"": {
- ""seed"": ""13992821"",
- ""default_when_disabled"": ""Small"",
- ""default_when_enabled"": ""Small"",
- ""user"": [
- {
- ""variant"": ""Big"",
- ""users"": [
- ""Marsha"",
- ""John""
- ]
- },
- {
- ""variant"": ""Small"",
- ""users"": [
- ""Alice"",
- ""Bob""
- ]
- }
- ],
- ""group"": [
- {
- ""variant"": ""Big"",
- ""groups"": [
- ""Ring1""
- ]
- },
- {
- ""variant"": ""Small"",
- ""groups"": [
- ""Ring2"",
- ""Ring3""
- ]
- }
- ],
- ""percentile"": [
- {
- ""variant"": ""Big"",
- ""from"": 0,
- ""to"": 50
- },
- {
- ""variant"": ""Small"",
- ""from"": 50,
- ""to"": 100
- }
- ]
- }
- }
- ",
- label: default,
- contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
- eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"));
+ List _variantFeatureFlagCollection = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "VariantsFeature1",
+ value: @"
+ {
+ ""id"": ""VariantsFeature1"",
+ ""enabled"": true,
+ ""variants"": [
+ {
+ ""name"": ""Big"",
+ ""configuration_value"": ""600px""
+ },
+ {
+ ""name"": ""Small"",
+ ""configuration_reference"": ""ShoppingCart:Small"",
+ ""status_override"": ""Disabled""
+ }
+ ],
+ ""allocation"": {
+ ""seed"": ""13992821"",
+ ""default_when_disabled"": ""Small"",
+ ""default_when_enabled"": ""Small"",
+ ""user"": [
+ {
+ ""variant"": ""Big"",
+ ""users"": [
+ ""Marsha"",
+ ""John""
+ ]
+ },
+ {
+ ""variant"": ""Small"",
+ ""users"": [
+ ""Alice"",
+ ""Bob""
+ ]
+ }
+ ],
+ ""group"": [
+ {
+ ""variant"": ""Big"",
+ ""groups"": [
+ ""Ring1""
+ ]
+ },
+ {
+ ""variant"": ""Small"",
+ ""groups"": [
+ ""Ring2"",
+ ""Ring3""
+ ]
+ }
+ ],
+ ""percentile"": [
+ {
+ ""variant"": ""Big"",
+ ""from"": 0,
+ ""to"": 50
+ },
+ {
+ ""variant"": ""Small"",
+ ""from"": 50,
+ ""to"": 100
+ }
+ ]
+ }
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
- private ConfigurationSetting _variantsKv2 = ConfigurationModelFactory.ConfigurationSetting(
- key: FeatureManagementConstants.FeatureFlagMarker + "VariantsFeature2",
- value: @"
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "VariantsFeature2",
+ value: @"
{
""id"": ""VariantsFeature2"",
- ""description"": """",
- ""display_name"": ""Variants Feature 2"",
""enabled"": false,
- ""conditions"": {
- ""client_filters"": [
- ]
- },
""variants"": [
{
""name"": ""ObjectVariant"",
@@ -309,36 +531,98 @@ public class FeatureManagementTests
}
}
",
- label: default,
- contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
- eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"));
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
- private ConfigurationSetting _telemetryKv = ConfigurationModelFactory.ConfigurationSetting(
- key: FeatureManagementConstants.FeatureFlagMarker + "TelemetryFeature",
- value: @"
- {
- ""id"": ""TelemetryFeature"",
- ""description"": """",
- ""display_name"": ""Telemetry Feature"",
- ""enabled"": true,
- ""conditions"": {
- ""client_filters"": [
- ]
- },
- ""telemetry"": {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "VariantsFeature3",
+ value: @"
+ {
+ ""id"": ""VariantsFeature3"",
+ ""enabled"": ""true"",
+ ""variants"": [
+ {
+ ""name"": ""NumberVariant"",
+ ""configuration_value"": 1
+ },
+ {
+ ""name"": ""NumberVariant"",
+ ""configuration_value"": 2
+ },
+ {
+ ""name"": ""OtherVariant"",
+ ""configuration_value"": ""Other""
+ }
+ ],
+ ""allocation"": {
+ ""default_when_enabled"": ""OtherVariant"",
+ ""default_when_enabled"": ""NumberVariant""
+ }
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "VariantsFeature4",
+ value: @"
+ {
+ ""id"": ""VariantsFeature4"",
+ ""enabled"": true,
+ ""variants"": null,
+ ""allocation"": null
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"))
+ };
+
+ List _telemetryFeatureFlagCollection = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "TelemetryFeature1",
+ value: @"
+ {
+ ""id"": ""TelemetryFeature1"",
""enabled"": true,
- ""metadata"": {
- ""Tags.Tag1"": ""Tag1Value"",
- ""Tags.Tag2"": ""Tag2Value""
- }
+ ""telemetry"": {
+ ""enabled"": ""true"",
+ ""metadata"": {
+ ""Tags.Tag1"": ""Tag1Value"",
+ ""Tags.Tag2"": ""Tag2Value""
+ }
+ }
}
- }
- ",
- label: "label",
- contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
- eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"));
+ ",
+ label: "label",
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1")),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "TelemetryFeature2",
+ value: @"
+ {
+ ""id"": ""TelemetryFeature2"",
+ ""enabled"": true,
+ ""telemetry"": {
+ ""enabled"": false,
+ ""enabled"": true,
+ ""metadata"": {
+ ""Tags.Tag1"": ""Tag1Value"",
+ ""Tags.Tag1"": ""Tag2Value""
+ }
+ }
+ }
+ ",
+ label: "label",
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"))
+ };
- TimeSpan CacheExpirationTime = TimeSpan.FromSeconds(1);
+ TimeSpan RefreshInterval = TimeSpan.FromSeconds(1);
[Fact]
public void UsesFeatureFlags()
@@ -374,7 +658,76 @@ public void UsesFeatureFlags()
}
[Fact]
- public void WatchesFeatureFlags()
+ public async Task WatchesFeatureFlags()
+ {
+ var featureFlags = new List { _kv };
+
+ var mockResponse = new Mock();
+ var mockClient = new Mock(MockBehavior.Strict);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns(new MockAsyncPageable(featureFlags));
+
+ IConfigurationRefresher refresher = null;
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.UseFeatureFlags(o => o.SetRefreshInterval(RefreshInterval));
+
+ refresher = options.GetRefresher();
+ })
+ .Build();
+
+ Assert.Equal("Browser", config["FeatureManagement:Beta:EnabledFor:0:Name"]);
+ Assert.Equal("Firefox", config["FeatureManagement:Beta:EnabledFor:0:Parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Safari", config["FeatureManagement:Beta:EnabledFor:0:Parameters:AllowedBrowsers:1"]);
+ Assert.Equal("RollOut", config["FeatureManagement:Beta:EnabledFor:1:Name"]);
+ Assert.Equal("20", config["FeatureManagement:Beta:EnabledFor:1:Parameters:Percentage"]);
+ Assert.Equal("US", config["FeatureManagement:Beta:EnabledFor:1:Parameters:Region"]);
+ Assert.Equal("SuperUsers", config["FeatureManagement:Beta:EnabledFor:2:Name"]);
+ Assert.Equal("TimeWindow", config["FeatureManagement:Beta:EnabledFor:3:Name"]);
+ Assert.Equal("/Date(1578643200000)/", config["FeatureManagement:Beta:EnabledFor:3:Parameters:Start"]);
+ Assert.Equal("/Date(1578686400000)/", config["FeatureManagement:Beta:EnabledFor:3:Parameters:End"]);
+
+ featureFlags[0] = ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "myFeature",
+ value: @"
+ {
+ ""id"": ""Beta"",
+ ""description"": ""The new beta version of our web site."",
+ ""display_name"": ""Beta Feature"",
+ ""enabled"": true,
+ ""conditions"": {
+ ""client_filters"": [
+ {
+ ""name"": ""Browser"",
+ ""parameters"": {
+ ""AllowedBrowsers"": [ ""Chrome"", ""Edge"" ]
+ }
+ }
+ ]
+ }
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1" + "f"));
+
+ featureFlags.Add(_kv2);
+
+ // Sleep to let the refresh interval elapse
+ Thread.Sleep(RefreshInterval);
+ await refresher.RefreshAsync();
+
+ Assert.Equal("Browser", config["FeatureManagement:Beta:EnabledFor:0:Name"]);
+ Assert.Equal("Chrome", config["FeatureManagement:Beta:EnabledFor:0:Parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Edge", config["FeatureManagement:Beta:EnabledFor:0:Parameters:AllowedBrowsers:1"]);
+ Assert.Equal("SuperUsers", config["FeatureManagement:MyFeature2:EnabledFor:0:Name"]);
+ }
+
+ [Fact]
+ public async Task WatchesFeatureFlagsUsingCacheExpirationInterval()
{
var featureFlags = new List { _kv };
@@ -384,13 +737,14 @@ public void WatchesFeatureFlags()
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns(new MockAsyncPageable(featureFlags));
+ var cacheExpirationInterval = TimeSpan.FromSeconds(1);
+
IConfigurationRefresher refresher = null;
- var cacheExpirationTimeSpan = TimeSpan.FromSeconds(1);
var config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
- options.UseFeatureFlags(o => o.CacheExpirationInterval = cacheExpirationTimeSpan);
+ options.UseFeatureFlags(o => o.CacheExpirationInterval = cacheExpirationInterval);
refresher = options.GetRefresher();
})
@@ -434,8 +788,8 @@ public void WatchesFeatureFlags()
featureFlags.Add(_kv2);
// Sleep to let the cache expire
- Thread.Sleep(cacheExpirationTimeSpan);
- refresher.RefreshAsync().Wait();
+ Thread.Sleep(cacheExpirationInterval);
+ await refresher.RefreshAsync();
Assert.Equal("Browser", config["FeatureManagement:Beta:EnabledFor:0:Name"]);
Assert.Equal("Chrome", config["FeatureManagement:Beta:EnabledFor:0:Parameters:AllowedBrowsers:0"]);
@@ -443,9 +797,75 @@ public void WatchesFeatureFlags()
Assert.Equal("SuperUsers", config["FeatureManagement:MyFeature2:EnabledFor:0:Name"]);
}
+ [Fact]
+ public async Task SkipRefreshIfRefreshIntervalHasNotElapsed()
+ {
+ var featureFlags = new List { _kv };
+
+ var mockResponse = new Mock();
+ var mockClient = new Mock(MockBehavior.Strict);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns(new MockAsyncPageable(featureFlags));
+
+ IConfigurationRefresher refresher = null;
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.UseFeatureFlags(o => o.SetRefreshInterval(TimeSpan.FromSeconds(10)));
+
+ refresher = options.GetRefresher();
+ })
+ .Build();
+
+ Assert.Equal("Browser", config["FeatureManagement:Beta:EnabledFor:0:Name"]);
+ Assert.Equal("Firefox", config["FeatureManagement:Beta:EnabledFor:0:Parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Safari", config["FeatureManagement:Beta:EnabledFor:0:Parameters:AllowedBrowsers:1"]);
+ Assert.Equal("RollOut", config["FeatureManagement:Beta:EnabledFor:1:Name"]);
+ Assert.Equal("20", config["FeatureManagement:Beta:EnabledFor:1:Parameters:Percentage"]);
+ Assert.Equal("US", config["FeatureManagement:Beta:EnabledFor:1:Parameters:Region"]);
+ Assert.Equal("SuperUsers", config["FeatureManagement:Beta:EnabledFor:2:Name"]);
+ Assert.Equal("TimeWindow", config["FeatureManagement:Beta:EnabledFor:3:Name"]);
+ Assert.Equal("/Date(1578643200000)/", config["FeatureManagement:Beta:EnabledFor:3:Parameters:Start"]);
+ Assert.Equal("/Date(1578686400000)/", config["FeatureManagement:Beta:EnabledFor:3:Parameters:End"]);
+
+ featureFlags[0] = ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + "myFeature",
+ value: @"
+ {
+ ""id"": ""Beta"",
+ ""description"": ""The new beta version of our web site."",
+ ""display_name"": ""Beta Feature"",
+ ""enabled"": true,
+ ""conditions"": {
+ ""client_filters"": [
+ {
+ ""name"": ""Browser"",
+ ""parameters"": {
+ ""AllowedBrowsers"": [ ""Chrome"", ""Edge"" ]
+ }
+ }
+ ]
+ }
+ }
+ ",
+ label: default,
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1" + "f"));
+
+ featureFlags.Add(_kv2);
+
+ await refresher.RefreshAsync();
+
+ Assert.Equal("Browser", config["FeatureManagement:Beta:EnabledFor:0:Name"]);
+ Assert.Equal("Firefox", config["FeatureManagement:Beta:EnabledFor:0:Parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Safari", config["FeatureManagement:Beta:EnabledFor:0:Parameters:AllowedBrowsers:1"]);
+ Assert.Null(config["FeatureManagement:MyFeature2:EnabledFor:0:Name"]);
+ }
[Fact]
- public void SkipRefreshIfCacheNotExpired()
+ public async Task SkipRefreshIfCacheNotExpired()
{
var featureFlags = new List { _kv };
@@ -503,7 +923,7 @@ public void SkipRefreshIfCacheNotExpired()
featureFlags.Add(_kv2);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
Assert.Equal("Browser", config["FeatureManagement:Beta:EnabledFor:0:Name"]);
Assert.Equal("Firefox", config["FeatureManagement:Beta:EnabledFor:0:Parameters:AllowedBrowsers:0"]);
@@ -568,28 +988,27 @@ public void QueriesFeatureFlags()
}
[Fact]
- public void UsesEtagForFeatureFlagRefresh()
+ public async Task UsesEtagForFeatureFlagRefresh()
{
var mockClient = new Mock(MockBehavior.Strict);
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns(new MockAsyncPageable(new List { _kv }));
IConfigurationRefresher refresher = null;
- var cacheExpirationTimeSpan = TimeSpan.FromSeconds(1);
var config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
{
options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
- options.UseFeatureFlags(o => o.CacheExpirationInterval = cacheExpirationTimeSpan);
+ options.UseFeatureFlags(o => o.SetRefreshInterval(RefreshInterval));
refresher = options.GetRefresher();
})
.Build();
- // Sleep to let the cache expire
- Thread.Sleep(cacheExpirationTimeSpan);
+ // Sleep to wait for refresh interval to elapse
+ Thread.Sleep(RefreshInterval);
- refresher.TryRefreshAsync().Wait();
+ await refresher.TryRefreshAsync();
mockClient.Verify(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Exactly(3));
}
@@ -600,7 +1019,6 @@ public void SelectFeatureFlags()
var mockClient = new Mock(MockBehavior.Strict);
var featureFlagPrefix = "App1";
var labelFilter = "App1_Label";
- var cacheExpiration = TimeSpan.FromSeconds(1);
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
.Returns(new MockAsyncPageable(_featureFlagCollection.Where(s => s.Key.StartsWith(FeatureManagementConstants.FeatureFlagMarker + featureFlagPrefix) && s.Label == labelFilter).ToList()));
@@ -613,14 +1031,14 @@ public void SelectFeatureFlags()
options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(testClient);
options.UseFeatureFlags(ff =>
{
- ff.CacheExpirationInterval = cacheExpiration;
+ ff.SetRefreshInterval(RefreshInterval);
ff.Select(featureFlagPrefix + "*", labelFilter);
});
})
.Build();
- Assert.Equal("AlwaysOn", config["FeatureManagement:App1_Feature1:EnabledFor:0:Name"]);
- Assert.Equal("Disabled", config["FeatureManagement:App1_Feature2:Status"]);
+ Assert.Equal("True", config["FeatureManagement:App1_Feature1"]);
+ Assert.Equal("False", config["FeatureManagement:App1_Feature2"]);
// Verify that the feature flag that did not start with the specified prefix was not loaded
Assert.Null(config["FeatureManagement:Feature1"]);
@@ -630,6 +1048,124 @@ public void SelectFeatureFlags()
Assert.Null(config["FeatureManagement:App2_Feature2"]);
}
+ [Fact]
+ public void TestNullAndMissingValuesForConditions()
+ {
+ var mockResponse = new Mock();
+ var mockClient = new Mock(MockBehavior.Strict);
+ var refreshInterval = TimeSpan.FromSeconds(1);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns(new MockAsyncPageable(_nullOrMissingConditionsFeatureFlagCollection));
+
+ var testClient = mockClient.Object;
+
+ // Makes sure that adapter properly processes values and doesn't throw an exception
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(testClient);
+ options.UseFeatureFlags(ff =>
+ {
+ ff.SetRefreshInterval(refreshInterval);
+ ff.Select(KeyFilter.Any);
+ });
+ })
+ .Build();
+
+ Assert.Null(config["FeatureManagement:NullConditions:EnabledFor"]);
+ Assert.Equal("Filter", config["FeatureManagement:NullParameters:EnabledFor:0:Name"]);
+ Assert.Null(config["FeatureManagement:NullParameters:EnabledFor:0:Parameters"]);
+ Assert.Null(config["FeatureManagement:NullClientFilters:EnabledFor"]);
+ Assert.Null(config["FeatureManagement:NoConditions:EnabledFor"]);
+ Assert.Null(config["FeatureManagement:EmptyConditions:EnabledFor"]);
+ Assert.Null(config["FeatureManagement:EmptyClientFilter:EnabledFor"]);
+ }
+
+ [Fact]
+ public void InvalidFeatureFlagFormatsThrowFormatException()
+ {
+ var mockResponse = new Mock();
+ var mockClient = new Mock(MockBehavior.Strict);
+ var refreshInterval = TimeSpan.FromSeconds(1);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns((Func)GetTestKeys);
+
+ MockAsyncPageable GetTestKeys(SettingSelector selector, CancellationToken ct)
+ {
+ var copy = new List();
+ var newSetting = _invalidFormatFeatureFlagCollection.FirstOrDefault(s => s.Key == selector.KeyFilter);
+ if (newSetting != null)
+ copy.Add(TestHelpers.CloneSetting(newSetting));
+ return new MockAsyncPageable(copy);
+ };
+
+ var testClient = mockClient.Object;
+
+ foreach (ConfigurationSetting setting in _invalidFormatFeatureFlagCollection)
+ {
+ void action() => new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.Select("_");
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(testClient);
+ options.UseFeatureFlags(ff =>
+ {
+ ff.SetRefreshInterval(refreshInterval);
+ ff.Select(setting.Key.Substring(FeatureManagementConstants.FeatureFlagMarker.Length));
+ });
+ })
+ .Build();
+
+ // Each of the feature flags should throw an exception
+ Assert.Throws(action);
+ }
+ }
+
+ [Fact]
+ public void AlternateValidFeatureFlagFormats()
+ {
+ var mockResponse = new Mock();
+ var mockClient = new Mock(MockBehavior.Strict);
+ var cacheExpiration = TimeSpan.FromSeconds(1);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns((Func)GetTestKeys);
+
+ MockAsyncPageable GetTestKeys(SettingSelector selector, CancellationToken ct)
+ {
+ var copy = new List();
+ var newSetting = _validFormatFeatureFlagCollection.FirstOrDefault(s => s.Key == selector.KeyFilter);
+ if (newSetting != null)
+ copy.Add(TestHelpers.CloneSetting(newSetting));
+ return new MockAsyncPageable(copy);
+ };
+
+ var testClient = mockClient.Object;
+
+ foreach (ConfigurationSetting setting in _validFormatFeatureFlagCollection)
+ {
+ string flagKey = setting.Key.Substring(FeatureManagementConstants.FeatureFlagMarker.Length);
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.Select("_");
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(testClient);
+ options.UseFeatureFlags(ff =>
+ {
+ ff.CacheExpirationInterval = cacheExpiration;
+ ff.Select(flagKey);
+ });
+ })
+ .Build();
+
+ // None of the feature flags should throw an exception, and the flag should be loaded like normal
+ Assert.Equal("True", config[$"FeatureManagement:{flagKey}"]);
+ }
+ }
+
[Fact]
public void MultipleSelectsInSameUseFeatureFlags()
{
@@ -662,10 +1198,10 @@ public void MultipleSelectsInSameUseFeatureFlags()
})
.Build();
- Assert.Equal("AlwaysOn", config["FeatureManagement:App1_Feature1:EnabledFor:0:Name"]);
- Assert.Equal("Disabled", config["FeatureManagement:App1_Feature2:Status"]);
- Assert.Equal("Disabled", config["FeatureManagement:App2_Feature1:Status"]);
- Assert.Equal("AlwaysOn", config["FeatureManagement:App2_Feature2:EnabledFor:0:Name"]);
+ Assert.Equal("True", config["FeatureManagement:App1_Feature1"]);
+ Assert.Equal("False", config["FeatureManagement:App1_Feature2"]);
+ Assert.Equal("False", config["FeatureManagement:App2_Feature1"]);
+ Assert.Equal("True", config["FeatureManagement:App2_Feature2"]);
// Verify that the feature flag that did not start with the specified prefix was not loaded
Assert.Null(config["FeatureManagement:Feature1"]);
@@ -703,7 +1239,7 @@ public void KeepSelectorPrecedenceAfterDedup()
})
.Build();
// label: App1_Label has higher precedence
- Assert.Equal("AlwaysOn", config["FeatureManagement:Feature1:EnabledFor:0:Name"]);
+ Assert.Equal("True", config["FeatureManagement:Feature1"]);
}
[Fact]
@@ -754,7 +1290,7 @@ public void MultipleCallsToUseFeatureFlags()
.Returns(() =>
{
return new MockAsyncPageable(_featureFlagCollection.Where(s =>
- (s.Key.StartsWith(FeatureManagementConstants.FeatureFlagMarker + prefix1) && s.Label == label1) ||
+ (s.Key.StartsWith(FeatureManagementConstants.FeatureFlagMarker + prefix1) && s.Label == label1) ||
(s.Key.StartsWith(FeatureManagementConstants.FeatureFlagMarker + prefix2) && s.Label == label2)).ToList());
});
@@ -775,10 +1311,10 @@ public void MultipleCallsToUseFeatureFlags()
})
.Build();
- Assert.Equal("AlwaysOn", config["FeatureManagement:App1_Feature1:EnabledFor:0:Name"]);
- Assert.Equal("Disabled", config["FeatureManagement:App1_Feature2:Status"]);
- Assert.Equal("Disabled", config["FeatureManagement:App2_Feature1:Status"]);
- Assert.Equal("AlwaysOn", config["FeatureManagement:App2_Feature2:EnabledFor:0:Name"]);
+ Assert.Equal("True", config["FeatureManagement:App1_Feature1"]);
+ Assert.Equal("False", config["FeatureManagement:App1_Feature2"]);
+ Assert.Equal("False", config["FeatureManagement:App2_Feature1"]);
+ Assert.Equal("True", config["FeatureManagement:App2_Feature2"]);
// Verify that the feature flag that did not start with the specified prefix was not loaded
Assert.Null(config["FeatureManagement:Feature1"]);
@@ -819,17 +1355,17 @@ public void MultipleCallsToUseFeatureFlagsWithSelectAndLabel()
.Build();
// Loaded from prefix1 and label1
- Assert.Equal("AlwaysOn", config["FeatureManagement:App1_Feature1:EnabledFor:0:Name"]);
- Assert.Equal("Disabled", config["FeatureManagement:App1_Feature2:Status"]);
+ Assert.Equal("True", config["FeatureManagement:App1_Feature1"]);
+ Assert.Equal("False", config["FeatureManagement:App1_Feature2"]);
// Loaded from label2
- Assert.Equal("Disabled", config["FeatureManagement:App2_Feature1:Status"]);
- Assert.Equal("AlwaysOn", config["FeatureManagement:App2_Feature2:EnabledFor:0:Name"]);
- Assert.Equal("AlwaysOn", config["FeatureManagement:Feature1:EnabledFor:0:Name"]);
+ Assert.Equal("False", config["FeatureManagement:App2_Feature1"]);
+ Assert.Equal("True", config["FeatureManagement:App2_Feature2"]);
+ Assert.Equal("True", config["FeatureManagement:Feature1"]);
}
[Fact]
- public void DifferentCacheExpirationsForMultipleFeatureFlagRegistrations()
+ public async Task DifferentCacheExpirationsForMultipleFeatureFlagRegistrations()
{
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
@@ -837,8 +1373,8 @@ public void DifferentCacheExpirationsForMultipleFeatureFlagRegistrations()
var prefix2 = "App2";
var label1 = "App1_Label";
var label2 = "App2_Label";
- var cacheExpiration1 = TimeSpan.FromSeconds(1);
- var cacheExpiration2 = TimeSpan.FromSeconds(60);
+ var refreshInterval1 = TimeSpan.FromSeconds(1);
+ var refreshInterval2 = TimeSpan.FromSeconds(60);
IConfigurationRefresher refresher = null;
var featureFlagCollection = new List(_featureFlagCollection);
@@ -856,12 +1392,12 @@ public void DifferentCacheExpirationsForMultipleFeatureFlagRegistrations()
options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.UseFeatureFlags(ff =>
{
- ff.CacheExpirationInterval = cacheExpiration1;
+ ff.SetRefreshInterval(refreshInterval1);
ff.Select(prefix1 + "*", label1);
});
options.UseFeatureFlags(ff =>
{
- ff.CacheExpirationInterval = cacheExpiration2;
+ ff.SetRefreshInterval(refreshInterval2);
ff.Select(prefix2 + "*", label2);
});
@@ -869,10 +1405,10 @@ public void DifferentCacheExpirationsForMultipleFeatureFlagRegistrations()
})
.Build();
- Assert.Equal("AlwaysOn", config["FeatureManagement:App1_Feature1:EnabledFor:0:Name"]);
- Assert.Equal("Disabled", config["FeatureManagement:App1_Feature2:Status"]);
- Assert.Equal("Disabled", config["FeatureManagement:App2_Feature1:Status"]);
- Assert.Equal("AlwaysOn", config["FeatureManagement:App2_Feature2:EnabledFor:0:Name"]);
+ Assert.Equal("True", config["FeatureManagement:App1_Feature1"]);
+ Assert.Equal("False", config["FeatureManagement:App1_Feature2"]);
+ Assert.Equal("False", config["FeatureManagement:App2_Feature1"]);
+ Assert.Equal("True", config["FeatureManagement:App2_Feature2"]);
// update the value of App1_Feature1 feature flag with label1
featureFlagCollection[0] = ConfigurationModelFactory.ConfigurationSetting(
@@ -913,28 +1449,28 @@ public void DifferentCacheExpirationsForMultipleFeatureFlagRegistrations()
contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1" + "f")));
- // Sleep to let the cache for feature flag with label1 expire
- Thread.Sleep(cacheExpiration1);
- refresher.RefreshAsync().Wait();
+ // Sleep to let the refresh interval for feature flag with label1 elapse
+ Thread.Sleep(refreshInterval1);
+ await refresher.RefreshAsync();
Assert.Equal("Browser", config["FeatureManagement:App1_Feature1:EnabledFor:0:Name"]);
Assert.Equal("Chrome", config["FeatureManagement:App1_Feature1:EnabledFor:0:Parameters:AllowedBrowsers:0"]);
Assert.Equal("Edge", config["FeatureManagement:App1_Feature1:EnabledFor:0:Parameters:AllowedBrowsers:1"]);
- Assert.Equal("Disabled", config["FeatureManagement:App1_Feature2:Status"]);
- Assert.Equal("Disabled", config["FeatureManagement:App2_Feature1:Status"]);
- Assert.Equal("AlwaysOn", config["FeatureManagement:App2_Feature2:EnabledFor:0:Name"]);
+ Assert.Equal("False", config["FeatureManagement:App1_Feature2"]);
+ Assert.Equal("False", config["FeatureManagement:App2_Feature1"]);
+ Assert.Equal("True", config["FeatureManagement:App2_Feature2"]);
// even though App2_Feature3 feature flag has been added, its value should not be loaded in config because label2 cache has not expired
Assert.Null(config["FeatureManagement:App2_Feature3"]);
}
[Fact]
- public void OverwrittenCacheExpirationForSameFeatureFlagRegistrations()
+ public async Task OverwrittenRefreshIntervalForSameFeatureFlagRegistrations()
{
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
- var cacheExpiration1 = TimeSpan.FromSeconds(1);
- var cacheExpiration2 = TimeSpan.FromSeconds(60);
+ var refreshInterval1 = TimeSpan.FromSeconds(1);
+ var refreshInterval2 = TimeSpan.FromSeconds(60);
IConfigurationRefresher refresher = null;
var featureFlagCollection = new List(_featureFlagCollection);
@@ -949,24 +1485,24 @@ public void OverwrittenCacheExpirationForSameFeatureFlagRegistrations()
{
ff.Select("*", "App1_Label");
ff.Select("*", "App2_Label");
- ff.CacheExpirationInterval = cacheExpiration1;
+ ff.SetRefreshInterval(refreshInterval1);
});
options.UseFeatureFlags(ff =>
{
ff.Select("*", "App1_Label");
ff.Select("*", "App2_Label");
- ff.CacheExpirationInterval = cacheExpiration2;
+ ff.SetRefreshInterval(refreshInterval2);
});
refresher = options.GetRefresher();
})
.Build();
- Assert.Equal("AlwaysOn", config["FeatureManagement:App1_Feature1:EnabledFor:0:Name"]);
- Assert.Equal("Disabled", config["FeatureManagement:App1_Feature2:Status"]);
- Assert.Equal("Disabled", config["FeatureManagement:App2_Feature1:Status"]);
- Assert.Equal("AlwaysOn", config["FeatureManagement:App2_Feature2:EnabledFor:0:Name"]);
- Assert.Equal("AlwaysOn", config["FeatureManagement:Feature1:EnabledFor:0:Name"]);
+ Assert.Equal("True", config["FeatureManagement:App1_Feature1"]);
+ Assert.Equal("False", config["FeatureManagement:App1_Feature2"]);
+ Assert.Equal("False", config["FeatureManagement:App2_Feature1"]);
+ Assert.Equal("True", config["FeatureManagement:App2_Feature2"]);
+ Assert.Equal("True", config["FeatureManagement:Feature1"]);
// update the value of App1_Feature1 feature flag with label1
featureFlagCollection[0] = ConfigurationModelFactory.ConfigurationSetting(
@@ -991,26 +1527,25 @@ public void OverwrittenCacheExpirationForSameFeatureFlagRegistrations()
contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1" + "f"));
- Thread.Sleep(cacheExpiration1);
- refresher.RefreshAsync().Wait();
+ Thread.Sleep(refreshInterval1);
+ await refresher.RefreshAsync();
- // The cache expiration time for feature flags was overwritten by second call to UseFeatureFlags.
- // Sleeping for cacheExpiration1 time should not update feature flags.
- Assert.Equal("AlwaysOn", config["FeatureManagement:App1_Feature1:EnabledFor:0:Name"]);
- Assert.Equal("Disabled", config["FeatureManagement:App1_Feature2:Status"]);
- Assert.Equal("Disabled", config["FeatureManagement:App2_Feature1:Status"]);
- Assert.Equal("AlwaysOn", config["FeatureManagement:App2_Feature2:EnabledFor:0:Name"]);
- Assert.Equal("AlwaysOn", config["FeatureManagement:Feature1:EnabledFor:0:Name"]);
+ // The refresh interval time for feature flags was overwritten by second call to UseFeatureFlags.
+ // Sleeping for refreshInterval1 time should not update feature flags.
+ Assert.Equal("True", config["FeatureManagement:App1_Feature1"]);
+ Assert.Equal("False", config["FeatureManagement:App1_Feature2"]);
+ Assert.Equal("False", config["FeatureManagement:App2_Feature1"]);
+ Assert.Equal("True", config["FeatureManagement:App2_Feature2"]);
+ Assert.Equal("True", config["FeatureManagement:Feature1"]);
}
[Fact]
- public void SelectAndRefreshSingleFeatureFlag()
+ public async Task SelectAndRefreshSingleFeatureFlag()
{
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
var prefix1 = "Feature1";
var label1 = "App1_Label";
- var cacheExpiration = TimeSpan.FromSeconds(1);
IConfigurationRefresher refresher = null;
var featureFlagCollection = new List(_featureFlagCollection);
@@ -1027,7 +1562,7 @@ public void SelectAndRefreshSingleFeatureFlag()
options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.UseFeatureFlags(ff =>
{
- ff.CacheExpirationInterval = cacheExpiration;
+ ff.SetRefreshInterval(RefreshInterval);
ff.Select(prefix1, label1);
});
@@ -1035,7 +1570,7 @@ public void SelectAndRefreshSingleFeatureFlag()
})
.Build();
- Assert.Equal("Disabled", config["FeatureManagement:Feature1:Status"]);
+ Assert.Equal("False", config["FeatureManagement:Feature1"]);
// update the value of Feature1 feature flag with App1_Label
featureFlagCollection[2] = ConfigurationModelFactory.ConfigurationSetting(
@@ -1060,9 +1595,9 @@ public void SelectAndRefreshSingleFeatureFlag()
contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1" + "f"));
- // Sleep to let the cache for feature flag with label1 expire
- Thread.Sleep(cacheExpiration);
- refresher.RefreshAsync().Wait();
+ // Sleep to let the refresh interval for feature flag with label1 elapse
+ Thread.Sleep(RefreshInterval);
+ await refresher.RefreshAsync();
Assert.Equal("Browser", config["FeatureManagement:Feature1:EnabledFor:0:Name"]);
Assert.Equal("Chrome", config["FeatureManagement:Feature1:EnabledFor:0:Parameters:AllowedBrowsers:0"]);
@@ -1070,7 +1605,7 @@ public void SelectAndRefreshSingleFeatureFlag()
}
[Fact]
- public void ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefresh()
+ public async Task ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefresh()
{
IConfigurationRefresher refresher = null;
var featureFlags = new List { _kv2 };
@@ -1101,7 +1636,7 @@ public void ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefresh()
.AddAzureAppConfiguration(options =>
{
options.ClientManager = mockClientManager;
- options.UseFeatureFlags(o => o.CacheExpirationInterval = CacheExpirationTime);
+ options.UseFeatureFlags(o => o.SetRefreshInterval(RefreshInterval));
refresher = options.GetRefresher();
})
.Build();
@@ -1129,15 +1664,15 @@ public void ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefresh()
contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1" + "f"));
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.Equal("AllUsers", config["FeatureManagement:MyFeature:EnabledFor:0:Name"]);
Assert.Contains(LogHelper.BuildFeatureFlagReadMessage("myFeature1", null, TestHelpers.PrimaryConfigStoreEndpoint.ToString().TrimEnd('/')), verboseInvocation);
Assert.Contains(LogHelper.BuildFeatureFlagUpdatedMessage("myFeature1"), informationalInvocation);
featureFlags.RemoveAt(0);
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.Null(config["FeatureManagement:MyFeature:EnabledFor:0:Name"]);
Assert.Contains(LogHelper.BuildFeatureFlagReadMessage("myFeature1", null, TestHelpers.PrimaryConfigStoreEndpoint.ToString().TrimEnd('/')), verboseInvocation);
@@ -1145,7 +1680,7 @@ public void ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefresh()
}
[Fact]
- public void ValidateFeatureFlagsUnchangedLogged()
+ public async Task ValidateFeatureFlagsUnchangedLogged()
{
IConfigurationRefresher refresher = null;
var featureFlags = new List { _kv2 };
@@ -1177,11 +1712,11 @@ public void ValidateFeatureFlagsUnchangedLogged()
.AddAzureAppConfiguration(options =>
{
options.ClientManager = mockClientManager;
- options.UseFeatureFlags(o => o.CacheExpirationInterval = CacheExpirationTime);
+ options.UseFeatureFlags(o => o.SetRefreshInterval(RefreshInterval));
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
})
@@ -1190,14 +1725,14 @@ public void ValidateFeatureFlagsUnchangedLogged()
Assert.Equal("SuperUsers", config["FeatureManagement:MyFeature2:EnabledFor:0:Name"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.Equal("SuperUsers", config["FeatureManagement:MyFeature2:EnabledFor:0:Name"]);
Assert.Contains(LogHelper.BuildFeatureFlagsUnchangedMessage(TestHelpers.PrimaryConfigStoreEndpoint.ToString().TrimEnd('/')), verboseInvocation);
}
[Fact]
- public void MapTransformFeatureFlagWithRefresh()
+ public async Task MapTransformFeatureFlagWithRefresh()
{
ConfigurationSetting _kv = ConfigurationModelFactory.ConfigurationSetting(
key: FeatureManagementConstants.FeatureFlagMarker + "myFeature",
@@ -1245,9 +1780,9 @@ public void MapTransformFeatureFlagWithRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
- options.UseFeatureFlags(o => o.CacheExpirationInterval = CacheExpirationTime);
+ options.UseFeatureFlags(o => o.SetRefreshInterval(RefreshInterval));
options.Map((setting) =>
{
if (setting.ContentType == FeatureManagementConstants.ContentType + ";charset=utf-8")
@@ -1302,8 +1837,8 @@ public void MapTransformFeatureFlagWithRefresh()
contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1" + "f"));
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.Equal("newValue1", config["TestKey1"]);
Assert.Equal("NoUsers", config["FeatureManagement:MyFeature:EnabledFor:0:Name"]);
@@ -1312,17 +1847,11 @@ public void MapTransformFeatureFlagWithRefresh()
[Fact]
public void WithVariants()
{
- var featureFlags = new List()
- {
- _variantsKv1,
- _variantsKv2
- };
-
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
- .Returns(new MockAsyncPageable(featureFlags));
+ .Returns(new MockAsyncPageable(_variantFeatureFlagCollection));
var config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
@@ -1332,93 +1861,79 @@ public void WithVariants()
})
.Build();
- Assert.Equal("AlwaysOn", config["FeatureManagement:VariantsFeature1:EnabledFor:0:Name"]);
- Assert.Equal("Big", config["FeatureManagement:VariantsFeature1:Variants:0:Name"]);
- Assert.Equal("600px", config["FeatureManagement:VariantsFeature1:Variants:0:ConfigurationValue"]);
- Assert.Equal("Small", config["FeatureManagement:VariantsFeature1:Variants:1:Name"]);
- Assert.Equal("ShoppingCart:Small", config["FeatureManagement:VariantsFeature1:Variants:1:ConfigurationReference"]);
- Assert.Equal("Disabled", config["FeatureManagement:VariantsFeature1:Variants:1:StatusOverride"]);
- Assert.Equal("Small", config["FeatureManagement:VariantsFeature1:Allocation:DefaultWhenDisabled"]);
- Assert.Equal("Small", config["FeatureManagement:VariantsFeature1:Allocation:DefaultWhenEnabled"]);
- Assert.Equal("Big", config["FeatureManagement:VariantsFeature1:Allocation:User:0:Variant"]);
- Assert.Equal("Marsha", config["FeatureManagement:VariantsFeature1:Allocation:User:0:Users:0"]);
- Assert.Equal("John", config["FeatureManagement:VariantsFeature1:Allocation:User:0:Users:1"]);
- Assert.Equal("Small", config["FeatureManagement:VariantsFeature1:Allocation:User:1:Variant"]);
- Assert.Equal("Alice", config["FeatureManagement:VariantsFeature1:Allocation:User:1:Users:0"]);
- Assert.Equal("Bob", config["FeatureManagement:VariantsFeature1:Allocation:User:1:Users:1"]);
- Assert.Equal("Big", config["FeatureManagement:VariantsFeature1:Allocation:Group:0:Variant"]);
- Assert.Equal("Ring1", config["FeatureManagement:VariantsFeature1:Allocation:Group:0:Groups:0"]);
- Assert.Equal("Small", config["FeatureManagement:VariantsFeature1:Allocation:Group:1:Variant"]);
- Assert.Equal("Ring2", config["FeatureManagement:VariantsFeature1:Allocation:Group:1:Groups:0"]);
- Assert.Equal("Ring3", config["FeatureManagement:VariantsFeature1:Allocation:Group:1:Groups:1"]);
- Assert.Equal("Big", config["FeatureManagement:VariantsFeature1:Allocation:Percentile:0:Variant"]);
- Assert.Equal("0", config["FeatureManagement:VariantsFeature1:Allocation:Percentile:0:From"]);
- Assert.Equal("50", config["FeatureManagement:VariantsFeature1:Allocation:Percentile:0:To"]);
- Assert.Equal("Small", config["FeatureManagement:VariantsFeature1:Allocation:Percentile:1:Variant"]);
- Assert.Equal("50", config["FeatureManagement:VariantsFeature1:Allocation:Percentile:1:From"]);
- Assert.Equal("100", config["FeatureManagement:VariantsFeature1:Allocation:Percentile:1:To"]);
- Assert.Equal("13992821", config["FeatureManagement:VariantsFeature1:Allocation:Seed"]);
-
- Assert.Equal("Disabled", config["FeatureManagement:VariantsFeature2:Status"]);
- Assert.Equal("ObjectVariant", config["FeatureManagement:VariantsFeature2:Variants:0:Name"]);
- Assert.Equal("Value1", config["FeatureManagement:VariantsFeature2:Variants:0:ConfigurationValue:Key1"]);
- Assert.Equal("Value2", config["FeatureManagement:VariantsFeature2:Variants:0:ConfigurationValue:Key2:InsideKey2"]);
- Assert.Equal("NumberVariant", config["FeatureManagement:VariantsFeature2:Variants:1:Name"]);
- Assert.Equal("100", config["FeatureManagement:VariantsFeature2:Variants:1:ConfigurationValue"]);
- Assert.Equal("NullVariant", config["FeatureManagement:VariantsFeature2:Variants:2:Name"]);
- Assert.Equal("", config["FeatureManagement:VariantsFeature2:Variants:2:ConfigurationValue"]);
+ Assert.Equal("VariantsFeature1", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("Big", config["feature_management:feature_flags:0:variants:0:name"]);
+ Assert.Equal("600px", config["feature_management:feature_flags:0:variants:0:configuration_value"]);
+ Assert.Equal("Small", config["feature_management:feature_flags:0:variants:1:name"]);
+ Assert.Equal("ShoppingCart:Small", config["feature_management:feature_flags:0:variants:1:configuration_reference"]);
+ Assert.Equal("Disabled", config["feature_management:feature_flags:0:variants:1:status_override"]);
+ Assert.Equal("Small", config["feature_management:feature_flags:0:allocation:default_when_disabled"]);
+ Assert.Equal("Small", config["feature_management:feature_flags:0:allocation:default_when_enabled"]);
+ Assert.Equal("Big", config["feature_management:feature_flags:0:allocation:user:0:variant"]);
+ Assert.Equal("Marsha", config["feature_management:feature_flags:0:allocation:user:0:users:0"]);
+ Assert.Equal("John", config["feature_management:feature_flags:0:allocation:user:0:users:1"]);
+ Assert.Equal("Small", config["feature_management:feature_flags:0:allocation:user:1:variant"]);
+ Assert.Equal("Alice", config["feature_management:feature_flags:0:allocation:user:1:users:0"]);
+ Assert.Equal("Bob", config["feature_management:feature_flags:0:allocation:user:1:users:1"]);
+ Assert.Equal("Big", config["feature_management:feature_flags:0:allocation:group:0:variant"]);
+ Assert.Equal("Ring1", config["feature_management:feature_flags:0:allocation:group:0:groups:0"]);
+ Assert.Equal("Small", config["feature_management:feature_flags:0:allocation:group:1:variant"]);
+ Assert.Equal("Ring2", config["feature_management:feature_flags:0:allocation:group:1:groups:0"]);
+ Assert.Equal("Ring3", config["feature_management:feature_flags:0:allocation:group:1:groups:1"]);
+ Assert.Equal("Big", config["feature_management:feature_flags:0:allocation:percentile:0:variant"]);
+ Assert.Equal("0", config["feature_management:feature_flags:0:allocation:percentile:0:from"]);
+ Assert.Equal("50", config["feature_management:feature_flags:0:allocation:percentile:0:to"]);
+ Assert.Equal("Small", config["feature_management:feature_flags:0:allocation:percentile:1:variant"]);
+ Assert.Equal("50", config["feature_management:feature_flags:0:allocation:percentile:1:from"]);
+ Assert.Equal("100", config["feature_management:feature_flags:0:allocation:percentile:1:to"]);
+ Assert.Equal("13992821", config["feature_management:feature_flags:0:allocation:seed"]);
+
+ Assert.Equal("VariantsFeature2", config["feature_management:feature_flags:1:id"]);
+ Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
+ Assert.Equal("ObjectVariant", config["feature_management:feature_flags:1:variants:0:name"]);
+ Assert.Equal("Value1", config["feature_management:feature_flags:1:variants:0:configuration_value:Key1"]);
+ Assert.Equal("Value2", config["feature_management:feature_flags:1:variants:0:configuration_value:Key2:InsideKey2"]);
+ Assert.Equal("NumberVariant", config["feature_management:feature_flags:1:variants:1:name"]);
+ Assert.Equal("100", config["feature_management:feature_flags:1:variants:1:configuration_value"]);
+ Assert.Equal("NullVariant", config["feature_management:feature_flags:1:variants:2:name"]);
+ Assert.Equal("", config["feature_management:feature_flags:1:variants:2:configuration_value"]);
Assert.True(config
- .GetSection("FeatureManagement:VariantsFeature2:Variants:2")
+ .GetSection("feature_management:feature_flags:1:variants:2")
.AsEnumerable()
.ToDictionary(x => x.Key, x => x.Value)
- .ContainsKey("FeatureManagement:VariantsFeature2:Variants:2:ConfigurationValue"));
- Assert.Equal("MissingValueVariant", config["FeatureManagement:VariantsFeature2:Variants:3:Name"]);
- Assert.Null(config["FeatureManagement:VariantsFeature2:Variants:3:ConfigurationValue"]);
+ .ContainsKey("feature_management:feature_flags:1:variants:2:configuration_value"));
+ Assert.Equal("MissingValueVariant", config["feature_management:feature_flags:1:variants:3:name"]);
+ Assert.Null(config["feature_management:feature_flags:1:variants:3:configuration_value"]);
Assert.False(config
- .GetSection("FeatureManagement:VariantsFeature2:Variants:3")
+ .GetSection("feature_management:feature_flags:1:variants:3")
.AsEnumerable()
.ToDictionary(x => x.Key, x => x.Value)
- .ContainsKey("FeatureManagement:VariantsFeature2:Variants:3:ConfigurationValue"));
- Assert.Equal("BooleanVariant", config["FeatureManagement:VariantsFeature2:Variants:4:Name"]);
- Assert.Equal("True", config["FeatureManagement:VariantsFeature2:Variants:4:ConfigurationValue"]);
- Assert.Equal("ObjectVariant", config["FeatureManagement:VariantsFeature2:Allocation:DefaultWhenDisabled"]);
- Assert.Equal("ObjectVariant", config["FeatureManagement:VariantsFeature2:Allocation:DefaultWhenEnabled"]);
- }
-
- [Fact]
- public void WithStatus()
- {
- var mockResponse = new Mock();
- var mockClient = new Mock(MockBehavior.Strict);
-
- mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
- .Returns(new MockAsyncPageable(_featureFlagCollection));
-
- var config = new ConfigurationBuilder()
- .AddAzureAppConfiguration(options =>
- {
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
- options.UseFeatureFlags();
- })
- .Build();
-
- Assert.Equal("Disabled", config["FeatureManagement:App1_Feature2:Status"]);
- Assert.Equal("Conditional", config["FeatureManagement:Feature1:Status"]);
+ .ContainsKey("feature_management:feature_flags:1:variants:3:configuration_value"));
+ Assert.Equal("BooleanVariant", config["feature_management:feature_flags:1:variants:4:name"]);
+ Assert.Equal("True", config["feature_management:feature_flags:1:variants:4:configuration_value"]);
+ Assert.Equal("ObjectVariant", config["feature_management:feature_flags:1:allocation:default_when_disabled"]);
+ Assert.Equal("ObjectVariant", config["feature_management:feature_flags:1:allocation:default_when_enabled"]);
+
+ Assert.Equal("VariantsFeature3", config["feature_management:feature_flags:2:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:2:enabled"]);
+ Assert.Equal("NumberVariant", config["feature_management:feature_flags:2:allocation:default_when_enabled"]);
+ Assert.Equal("1", config["feature_management:feature_flags:2:variants:0:configuration_value"]);
+ Assert.Equal("2", config["feature_management:feature_flags:2:variants:1:configuration_value"]);
+ Assert.Equal("Other", config["feature_management:feature_flags:2:variants:2:configuration_value"]);
+ Assert.Equal("NumberVariant", config["feature_management:feature_flags:2:allocation:default_when_enabled"]);
+
+ Assert.Equal("True", config["FeatureManagement:VariantsFeature4"]);
}
[Fact]
public void WithTelemetry()
{
- var featureFlags = new List()
- {
- _telemetryKv
- };
-
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
- .Returns(new MockAsyncPageable(featureFlags));
+ .Returns(new MockAsyncPageable(_telemetryFeatureFlagCollection));
var config = new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
@@ -1429,16 +1944,17 @@ public void WithTelemetry()
})
.Build();
- Assert.Equal("True", config["FeatureManagement:TelemetryFeature:Telemetry:Enabled"]);
- Assert.Equal("Tag1Value", config["FeatureManagement:TelemetryFeature:Telemetry:Metadata:Tags.Tag1"]);
- Assert.Equal("Tag2Value", config["FeatureManagement:TelemetryFeature:Telemetry:Metadata:Tags.Tag2"]);
- Assert.Equal("c3c231fd-39a0-4cb6-3237-4614474b92c1", config["FeatureManagement:TelemetryFeature:Telemetry:Metadata:ETag"]);
+ Assert.Equal("True", config["feature_management:feature_flags:0:telemetry:enabled"]);
+ Assert.Equal("TelemetryFeature1", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("Tag1Value", config["feature_management:feature_flags:0:telemetry:metadata:Tags.Tag1"]);
+ Assert.Equal("Tag2Value", config["feature_management:feature_flags:0:telemetry:metadata:Tags.Tag2"]);
+ Assert.Equal("c3c231fd-39a0-4cb6-3237-4614474b92c1", config["feature_management:feature_flags:0:telemetry:metadata:ETag"]);
byte[] featureFlagIdHash;
using (HashAlgorithm hashAlgorithm = SHA256.Create())
{
- featureFlagIdHash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes($"{FeatureManagementConstants.FeatureFlagMarker}TelemetryFeature\nlabel"));
+ featureFlagIdHash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes($"{FeatureManagementConstants.FeatureFlagMarker}TelemetryFeature1\nlabel"));
}
string featureFlagId = Convert.ToBase64String(featureFlagIdHash)
@@ -1446,8 +1962,12 @@ public void WithTelemetry()
.Replace('+', '-')
.Replace('/', '_');
- Assert.Equal(featureFlagId, config["FeatureManagement:TelemetryFeature:Telemetry:Metadata:FeatureFlagId"]);
- Assert.Equal($"{TestHelpers.PrimaryConfigStoreEndpoint}kv/{FeatureManagementConstants.FeatureFlagMarker}TelemetryFeature?label=label", config["FeatureManagement:TelemetryFeature:Telemetry:Metadata:FeatureFlagReference"]);
+ Assert.Equal(featureFlagId, config["feature_management:feature_flags:0:telemetry:metadata:FeatureFlagId"]);
+ Assert.Equal($"{TestHelpers.PrimaryConfigStoreEndpoint}kv/{FeatureManagementConstants.FeatureFlagMarker}TelemetryFeature1?label=label", config["feature_management:feature_flags:0:telemetry:metadata:FeatureFlagReference"]);
+
+ Assert.Equal("True", config["feature_management:feature_flags:1:telemetry:enabled"]);
+ Assert.Equal("TelemetryFeature2", config["feature_management:feature_flags:1:id"]);
+ Assert.Equal("Tag2Value", config["feature_management:feature_flags:1:telemetry:metadata:Tags.Tag1"]);
}
@@ -1469,9 +1989,9 @@ public void WithRequirementType()
var featureFlags = new List()
{
_kv2,
- FeatureWithRequirementType("Feature_NoFilters", "All", emptyFilters),
- FeatureWithRequirementType("Feature_RequireAll", "All", nonEmptyFilters),
- FeatureWithRequirementType("Feature_RequireAny", "Any", nonEmptyFilters)
+ CreateFeatureFlag("Feature_NoFilters", requirementType: "\"All\"", clientFiltersJsonString: emptyFilters),
+ CreateFeatureFlag("Feature_RequireAll", requirementType: "\"All\"", clientFiltersJsonString: nonEmptyFilters),
+ CreateFeatureFlag("Feature_RequireAny", requirementType: "\"Any\"", clientFiltersJsonString: nonEmptyFilters)
};
var mockResponse = new Mock();
@@ -1488,10 +2008,60 @@ public void WithRequirementType()
})
.Build();
- Assert.Null(config["FeatureManagement:MyFeature2:RequirementType"]);
- Assert.Null(config["FeatureManagement:Feature_NoFilters:RequirementType"]);
- Assert.Equal("All", config["FeatureManagement:Feature_RequireAll:RequirementType"]);
- Assert.Equal("Any", config["FeatureManagement:Feature_RequireAny:RequirementType"]);
+ Assert.Null(config["feature_management:feature_flags:0:requirement_type"]);
+ Assert.Equal("Feature_NoFilters", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("All", config["feature_management:feature_flags:1:conditions:requirement_type"]);
+ Assert.Equal("Feature_RequireAll", config["feature_management:feature_flags:1:id"]);
+ Assert.Equal("Any", config["feature_management:feature_flags:2:conditions:requirement_type"]);
+ Assert.Equal("Feature_RequireAny", config["feature_management:feature_flags:2:id"]);
+ }
+
+ [Fact]
+ public void ThrowsOnIncorrectJsonTypes()
+ {
+ var settings = new List()
+ {
+ CreateFeatureFlag("Feature1", variantsJsonString: @"[{""name"": 1}]"),
+ CreateFeatureFlag("Feature2", variantsJsonString: @"[{""configuration_reference"": true}]"),
+ CreateFeatureFlag("Feature3", variantsJsonString: @"[{""status_override"": []}]"),
+ CreateFeatureFlag("Feature4", seed: "{}"),
+ CreateFeatureFlag("Feature5", defaultWhenDisabled: "5"),
+ CreateFeatureFlag("Feature6", defaultWhenEnabled: "6"),
+ CreateFeatureFlag("Feature7", userJsonString: @"[{""variant"": []}]"),
+ CreateFeatureFlag("Feature8", userJsonString: @"[{""users"": [ {""name"": ""8""} ]}]"),
+ CreateFeatureFlag("Feature9", groupJsonString: @"[{""variant"": false}]"),
+ CreateFeatureFlag("Feature10", groupJsonString: @"[{""groups"": 10}]"),
+ CreateFeatureFlag("Feature11", percentileJsonString: @"[{""variant"": []}]"),
+ CreateFeatureFlag("Feature12", percentileJsonString: @"[{""from"": true}]"),
+ CreateFeatureFlag("Feature13", percentileJsonString: @"[{""to"": {}}]"),
+ CreateFeatureFlag("Feature14", telemetryEnabled: "14"),
+ CreateFeatureFlag("Feature15", telemetryMetadataJsonString: @"{""key"": 15}"),
+ CreateFeatureFlag("Feature16", clientFiltersJsonString: @"[{""name"": 16}]"),
+ CreateFeatureFlag("Feature17", clientFiltersJsonString: @"{""key"": [{""name"": ""name"", ""parameters"": 17}]}"),
+ CreateFeatureFlag("Feature18", requirementType: "18")
+ };
+
+ var mockResponse = new Mock();
+ var mockClient = new Mock(MockBehavior.Strict);
+
+ foreach (ConfigurationSetting setting in settings)
+ {
+ var featureFlags = new List { setting };
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns(new MockAsyncPageable(featureFlags));
+
+ void action() => new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.UseFeatureFlags();
+ }).Build();
+
+ var exception = Assert.Throws(action);
+
+ Assert.False(exception.InnerException is JsonException);
+ }
}
Response GetIfChanged(ConfigurationSetting setting, bool onlyIfChanged, CancellationToken cancellationToken)
@@ -1504,18 +2074,42 @@ Response GetTestKey(string key, string label, Cancellation
return Response.FromValue(TestHelpers.CloneSetting(FirstKeyValue), new Mock().Object);
}
- private ConfigurationSetting FeatureWithRequirementType(string featureId, string requirementType, string clientFiltersJsonString)
+ private ConfigurationSetting CreateFeatureFlag(string featureId,
+ string requirementType = "null",
+ string clientFiltersJsonString = "null",
+ string variantsJsonString = "null",
+ string seed = "null",
+ string defaultWhenDisabled = "null",
+ string defaultWhenEnabled = "null",
+ string userJsonString = "null",
+ string groupJsonString = "null",
+ string percentileJsonString = "null",
+ string telemetryEnabled = "null",
+ string telemetryMetadataJsonString = "null")
{
return ConfigurationModelFactory.ConfigurationSetting(
key: FeatureManagementConstants.FeatureFlagMarker + featureId,
value: $@"
{{
- ""id"": ""{featureId}"",
- ""enabled"": true,
- ""conditions"": {{
- ""requirement_type"": ""{requirementType}"",
- ""client_filters"": {clientFiltersJsonString}
- }}
+ ""id"": ""{featureId}"",
+ ""enabled"": true,
+ ""conditions"": {{
+ ""requirement_type"": {requirementType},
+ ""client_filters"": {clientFiltersJsonString}
+ }},
+ ""variants"": {variantsJsonString},
+ ""allocation"": {{
+ ""seed"": {seed},
+ ""default_when_disabled"": {defaultWhenDisabled},
+ ""default_when_enabled"": {defaultWhenEnabled},
+ ""user"": {userJsonString},
+ ""group"": {groupJsonString},
+ ""percentile"": {percentileJsonString}
+ }},
+ ""telemetry"": {{
+ ""enabled"": {telemetryEnabled},
+ ""metadata"": {telemetryMetadataJsonString}
+ }}
}}
",
contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
diff --git a/tests/Tests.AzureAppConfiguration/JsonContentTypeTests.cs b/tests/Tests.AzureAppConfiguration/JsonContentTypeTests.cs
index 7723a612..91cb03a8 100644
--- a/tests/Tests.AzureAppConfiguration/JsonContentTypeTests.cs
+++ b/tests/Tests.AzureAppConfiguration/JsonContentTypeTests.cs
@@ -28,20 +28,15 @@ public void JsonContentTypeTests_CompareJsonSettingsBetweenAppConfigAndJsonFile(
var appconfigSettings = new ConfigurationBuilder()
.AddAzureAppConfiguration(options => options.ClientManager = mockClientManager)
- .Build()
- .AsEnumerable();
+ .Build();
var jsonSettings = new ConfigurationBuilder()
.AddJsonFile(jsonFilePath)
- .Build()
- .AsEnumerable();
-
- Assert.Equal(jsonSettings.Count(), appconfigSettings.Count());
+ .Build();
- foreach (KeyValuePair jsonSetting in jsonSettings)
+ foreach (KeyValuePair jsonSetting in jsonSettings.AsEnumerable())
{
- KeyValuePair appconfigSetting = appconfigSettings.SingleOrDefault(x => x.Key == jsonSetting.Key);
- Assert.Equal(jsonSetting, appconfigSetting);
+ Assert.Equal(jsonSettings.GetSection(jsonSetting.Key).Value, appconfigSettings.GetSection(jsonSetting.Key).Value);
}
}
diff --git a/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs b/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
index 9a80a350..6108519c 100644
--- a/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
+++ b/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
@@ -12,6 +12,7 @@
using Moq;
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
@@ -82,6 +83,91 @@ public class KeyVaultReferenceTests
contentType: KeyVaultConstants.ContentType + "; charset=utf-8"),
};
+ List _invalidJsonKvCollection = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key:"MissingClosingBracket",
+ value: @"
+ {
+ ""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/TheTrialSecret""
+ ",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"),
+ contentType: KeyVaultConstants.ContentType + "; charset=utf-8"),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key:"MissingOpeningBracket",
+ value: @"
+ ""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/TheTrialSecret""
+ }
+ ",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"),
+ contentType: KeyVaultConstants.ContentType + "; charset=utf-8"),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key:"MissingUriInRootJson",
+ value: @"
+ {
+ {
+ ""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/TheTrialSecret""
+ }
+ }
+ ",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"),
+ contentType: KeyVaultConstants.ContentType + "; charset=utf-8"),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key:"UriValueInsideObject",
+ value: @"
+ {
+ {
+ ""uri"": {
+ ""https://keyvault-theclassics.vault.azure.net/secrets/TheTrialSecret""
+ }
+ }
+ }
+ ",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"),
+ contentType: KeyVaultConstants.ContentType + "; charset=utf-8")
+ };
+
+ List _validJsonKvCollection = new List
+ {
+ ConfigurationModelFactory.ConfigurationSetting(
+ key:"AdditionalProperty1",
+ value: @"
+ {
+ ""additional_property"":""additional_property"",
+ ""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/TheTrialSecret""
+ }
+ ",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"),
+ contentType: KeyVaultConstants.ContentType + "; charset=utf-8"),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key:"AdditionalProperty2",
+ value: @"
+ {
+ ""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/TheTrialSecret"",
+ ""additional_property"": {
+ ""inside_property"": ""inside_property""
+ }
+ }
+ ",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"),
+ contentType: KeyVaultConstants.ContentType + "; charset=utf-8"),
+
+ ConfigurationModelFactory.ConfigurationSetting(
+ key:"DuplicateUri",
+ value: @"
+ {
+ ""uri"":""https://keyvault-theclassics.vault.azure.net/certificates/TestCertificate"",
+ ""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/TheTrialSecret""
+ }
+ ",
+ eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"),
+ contentType: KeyVaultConstants.ContentType + "; charset=utf-8")
+ };
+
[Fact]
public void NotSecretIdentifierURI()
{
@@ -424,7 +510,8 @@ public void DoesNotThrowKeyVaultExceptionWhenProviderIsOptional()
.Returns(true);
mockKeyValueAdapter.Setup(adapter => adapter.ProcessKeyValue(_kv, It.IsAny(), It.IsAny(), It.IsAny()))
.Throws(new KeyVaultReferenceException("Key vault error", null));
- mockKeyValueAdapter.Setup(adapter => adapter.InvalidateCache(null));
+ mockKeyValueAdapter.Setup(adapter => adapter.OnChangeDetected(null));
+ mockKeyValueAdapter.Setup(adapter => adapter.OnConfigUpdated());
new ConfigurationBuilder()
.AddAzureAppConfiguration(options =>
@@ -597,10 +684,10 @@ public void ThrowsWhenSecretRefreshIntervalIsTooShort()
}
[Fact]
- public void SecretIsReturnedFromCacheIfSecretCacheHasNotExpired()
+ public async Task SecretIsReturnedFromCacheIfSecretCacheHasNotExpired()
{
IConfigurationRefresher refresher = null;
- TimeSpan cacheExpirationTime = TimeSpan.FromSeconds(1);
+ TimeSpan refreshInterval = TimeSpan.FromSeconds(1);
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
@@ -646,7 +733,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("Sentinel")
- .SetCacheExpiration(cacheExpirationTime);
+ .SetRefreshInterval(refreshInterval);
});
refresher = options.GetRefresher();
@@ -658,8 +745,8 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
// Update sentinel key-value
sentinelKv.Value = "Value2";
- Thread.Sleep(cacheExpirationTime);
- refresher.RefreshAsync().Wait();
+ Thread.Sleep(refreshInterval);
+ await refresher.RefreshAsync();
Assert.Equal("Value2", config["Sentinel"]);
Assert.Equal(_secretValue, config[_kv.Key]);
@@ -670,10 +757,10 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
}
[Fact]
- public void CachedSecretIsInvalidatedWhenRefreshAllIsTrue()
+ public async Task CachedSecretIsInvalidatedWhenRefreshAllIsTrue()
{
IConfigurationRefresher refresher = null;
- TimeSpan cacheExpirationTime = TimeSpan.FromSeconds(1);
+ TimeSpan refreshInterval = TimeSpan.FromSeconds(1);
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
@@ -718,7 +805,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("Sentinel", refreshAll: true)
- .SetCacheExpiration(cacheExpirationTime);
+ .SetRefreshInterval(refreshInterval);
});
refresher = options.GetRefresher();
@@ -730,8 +817,8 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
// Update sentinel key-value to trigger refresh operation
sentinelKv.Value = "Value2";
- Thread.Sleep(cacheExpirationTime);
- refresher.RefreshAsync().Wait();
+ Thread.Sleep(refreshInterval);
+ await refresher.RefreshAsync();
Assert.Equal("Value2", config["Sentinel"]);
Assert.Equal(_secretValue, config[_kv.Key]);
@@ -742,10 +829,10 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
}
[Fact]
- public void SecretIsReloadedFromKeyVaultWhenCacheExpires()
+ public async Task SecretIsReloadedFromKeyVaultWhenCacheExpires()
{
IConfigurationRefresher refresher = null;
- TimeSpan cacheExpirationTime = TimeSpan.FromSeconds(1);
+ TimeSpan refreshInterval = TimeSpan.FromSeconds(1);
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
@@ -765,7 +852,7 @@ public void SecretIsReloadedFromKeyVaultWhenCacheExpires()
options.ConfigureKeyVault(kv =>
{
kv.Register(mockSecretClient.Object);
- kv.SetSecretRefreshInterval(_kv.Key, cacheExpirationTime);
+ kv.SetSecretRefreshInterval(_kv.Key, refreshInterval);
});
refresher = options.GetRefresher();
@@ -775,8 +862,8 @@ public void SecretIsReloadedFromKeyVaultWhenCacheExpires()
Assert.Equal(_secretValue, config[_kv.Key]);
// Sleep to let the secret cache expire
- Thread.Sleep(cacheExpirationTime);
- refresher.RefreshAsync().Wait();
+ Thread.Sleep(refreshInterval);
+ await refresher.RefreshAsync();
Assert.Equal(_secretValue, config[_kv.Key]);
@@ -785,10 +872,10 @@ public void SecretIsReloadedFromKeyVaultWhenCacheExpires()
}
[Fact]
- public void SecretsWithDefaultRefreshInterval()
+ public async Task SecretsWithDefaultRefreshInterval()
{
IConfigurationRefresher refresher = null;
- TimeSpan shortCacheExpirationTime = TimeSpan.FromSeconds(1);
+ TimeSpan shortRefreshInterval = TimeSpan.FromSeconds(1);
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
@@ -808,7 +895,7 @@ public void SecretsWithDefaultRefreshInterval()
options.ConfigureKeyVault(kv =>
{
kv.Register(mockSecretClient.Object);
- kv.SetSecretRefreshInterval(shortCacheExpirationTime);
+ kv.SetSecretRefreshInterval(shortRefreshInterval);
});
refresher = options.GetRefresher();
@@ -819,8 +906,8 @@ public void SecretsWithDefaultRefreshInterval()
Assert.Equal(_secretValue, config["TK2"]);
// Sleep to let the secret cache expire for both secrets
- Thread.Sleep(shortCacheExpirationTime);
- refresher.RefreshAsync().Wait();
+ Thread.Sleep(shortRefreshInterval);
+ await refresher.RefreshAsync();
Assert.Equal(_secretValue, config["TK1"]);
Assert.Equal(_secretValue, config["TK2"]);
@@ -830,11 +917,11 @@ public void SecretsWithDefaultRefreshInterval()
}
[Fact]
- public void SecretsWithDifferentRefreshIntervals()
+ public async Task SecretsWithDifferentRefreshIntervals()
{
IConfigurationRefresher refresher = null;
- TimeSpan shortCacheExpirationTime = TimeSpan.FromSeconds(1);
- TimeSpan longCacheExpirationTime = TimeSpan.FromDays(1);
+ TimeSpan shortRefreshInterval = TimeSpan.FromSeconds(1);
+ TimeSpan longRefreshInterval = TimeSpan.FromDays(1);
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
@@ -854,8 +941,8 @@ public void SecretsWithDifferentRefreshIntervals()
options.ConfigureKeyVault(kv =>
{
kv.Register(mockSecretClient.Object);
- kv.SetSecretRefreshInterval("TK1", shortCacheExpirationTime);
- kv.SetSecretRefreshInterval(longCacheExpirationTime);
+ kv.SetSecretRefreshInterval("TK1", shortRefreshInterval);
+ kv.SetSecretRefreshInterval(longRefreshInterval);
});
refresher = options.GetRefresher();
@@ -866,8 +953,8 @@ public void SecretsWithDifferentRefreshIntervals()
Assert.Equal(_secretValue, config["TK2"]);
// Sleep to let the secret cache expire for one secret
- Thread.Sleep(shortCacheExpirationTime);
- refresher.RefreshAsync().Wait();
+ Thread.Sleep(shortRefreshInterval);
+ await refresher.RefreshAsync();
Assert.Equal(_secretValue, config["TK1"]);
Assert.Equal(_secretValue, config["TK2"]);
@@ -875,5 +962,95 @@ public void SecretsWithDifferentRefreshIntervals()
// Validate that 3 calls were made to fetch secrets from KeyVault because the secret cache had expired for only one secret.
mockSecretClient.Verify(client => client.GetSecretAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3));
}
+
+
+ [Fact]
+ public void ThrowsWhenInvalidKeyVaultSecretReferenceJson()
+ {
+ var mockResponse = new Mock();
+ var mockClient = new Mock(MockBehavior.Strict);
+ var cacheExpiration = TimeSpan.FromSeconds(1);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns((Func)GetTestKeys);
+
+ var mockSecretClient = new Mock(MockBehavior.Strict);
+ mockSecretClient.SetupGet(client => client.VaultUri).Returns(new Uri("https://keyvault-theclassics.vault.azure.net"));
+
+ MockAsyncPageable GetTestKeys(SettingSelector selector, CancellationToken ct)
+ {
+ var copy = new List();
+ var newSetting = _invalidJsonKvCollection.FirstOrDefault(s => s.Key == selector.KeyFilter);
+ if (newSetting != null)
+ copy.Add(TestHelpers.CloneSetting(newSetting));
+ return new MockAsyncPageable(copy);
+ };
+
+ var testClient = mockClient.Object;
+
+ foreach (ConfigurationSetting setting in _invalidJsonKvCollection)
+ {
+ void action() => new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.Select(setting.Key);
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(testClient);
+ options.ConfigureKeyVault(kv =>
+ {
+ kv.Register(mockSecretClient.Object);
+ });
+ })
+ .Build();
+
+ // Each of the secret references should throw an exception when parsed
+ Assert.Throws(action);
+ }
+ }
+
+ [Fact]
+ public void AlternateValidKeyVaultSecretReferenceJsons()
+ {
+ var mockResponse = new Mock();
+ var mockClient = new Mock(MockBehavior.Strict);
+ var cacheExpiration = TimeSpan.FromSeconds(1);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns((Func)GetTestKeys);
+
+ 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, _secretValue))));
+
+ MockAsyncPageable GetTestKeys(SettingSelector selector, CancellationToken ct)
+ {
+ var copy = new List();
+ var newSetting = _validJsonKvCollection.FirstOrDefault(s => s.Key == selector.KeyFilter);
+ if (newSetting != null)
+ copy.Add(TestHelpers.CloneSetting(newSetting));
+ return new MockAsyncPageable(copy);
+ };
+
+ var testClient = mockClient.Object;
+
+ foreach (ConfigurationSetting setting in _validJsonKvCollection)
+ {
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.Select(setting.Key);
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(testClient);
+ options.ConfigureKeyVault(kv =>
+ {
+ kv.Register(mockSecretClient.Object);
+ });
+ })
+ .Build();
+
+ // Each of the secret references should work as normal and use the uri
+ Assert.Equal(_secretValue, config[setting.Key]);
+ }
+ }
}
}
diff --git a/tests/Tests.AzureAppConfiguration/LoadBalancingTests.cs b/tests/Tests.AzureAppConfiguration/LoadBalancingTests.cs
new file mode 100644
index 00000000..4429c7be
--- /dev/null
+++ b/tests/Tests.AzureAppConfiguration/LoadBalancingTests.cs
@@ -0,0 +1,153 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Azure;
+using Azure.Core.Testing;
+using Azure.Data.AppConfiguration;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Configuration.AzureAppConfiguration;
+using Moq;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Tests.AzureAppConfiguration
+{
+ public class LoadBalancingTests
+ {
+ readonly ConfigurationSetting kv = ConfigurationModelFactory.ConfigurationSetting(key: "TestKey1", label: "label", value: "TestValue1",
+ eTag: new ETag("0a76e3d7-7ec1-4e37-883c-9ea6d0d89e63"),
+ contentType: "text");
+
+ TimeSpan RefreshInterval = TimeSpan.FromSeconds(1);
+
+ [Fact]
+ public async Task LoadBalancingTests_UsesAllEndpoints()
+ {
+ IConfigurationRefresher refresher = null;
+ var mockResponse = new MockResponse(200);
+
+ var mockClient1 = new Mock(MockBehavior.Strict);
+ mockClient1.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Returns(new MockAsyncPageable(Enumerable.Empty().ToList()));
+ mockClient1.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(Response.FromValue(kv, mockResponse));
+ mockClient1.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(Response.FromValue(kv, mockResponse));
+ mockClient1.Setup(c => c.Equals(mockClient1)).Returns(true);
+
+ var mockClient2 = new Mock(MockBehavior.Strict);
+ 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()))
+ .ReturnsAsync(Response.FromValue(kv, mockResponse));
+ mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(Response.FromValue(kv, mockResponse));
+ mockClient2.Setup(c => c.Equals(mockClient2)).Returns(true);
+
+ ConfigurationClientWrapper cw1 = new ConfigurationClientWrapper(TestHelpers.PrimaryConfigStoreEndpoint, mockClient1.Object);
+ ConfigurationClientWrapper cw2 = new ConfigurationClientWrapper(TestHelpers.SecondaryConfigStoreEndpoint, mockClient2.Object);
+
+ var clientList = new List() { cw1, cw2 };
+ var configClientManager = new ConfigurationClientManager(clientList);
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = configClientManager;
+ options.ConfigureRefresh(refreshOptions =>
+ {
+ refreshOptions.Register("TestKey1", "label")
+ .SetRefreshInterval(RefreshInterval);
+ });
+ options.ReplicaDiscoveryEnabled = false;
+ options.LoadBalancingEnabled = true;
+
+ refresher = options.GetRefresher();
+ }).Build();
+
+ // Ensure client 1 was used for startup
+ mockClient1.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Exactly(1));
+
+ Thread.Sleep(RefreshInterval);
+ await refresher.RefreshAsync();
+
+ // Ensure client 2 was used for refresh
+ mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(0));
+
+ mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1));
+
+ Thread.Sleep(RefreshInterval);
+ await refresher.RefreshAsync();
+
+ // Ensure client 1 was now used for refresh
+ mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1));
+ }
+
+ [Fact]
+ public async Task LoadBalancingTests_UsesClientAfterBackoffEnds()
+ {
+ IConfigurationRefresher refresher = null;
+ var mockResponse = new MockResponse(200);
+
+ var mockClient1 = new Mock(MockBehavior.Strict);
+ mockClient1.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Throws(new RequestFailedException(503, "Request failed."));
+ mockClient1.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(Response.FromValue(kv, mockResponse));
+ mockClient1.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(Response.FromValue(kv, mockResponse));
+ mockClient1.Setup(c => c.Equals(mockClient1)).Returns(true);
+
+ var mockClient2 = new Mock(MockBehavior.Strict);
+ 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()))
+ .ReturnsAsync(Response.FromValue(kv, mockResponse));
+ mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(Response.FromValue(kv, mockResponse));
+ mockClient2.Setup(c => c.Equals(mockClient2)).Returns(true);
+
+ ConfigurationClientWrapper cw1 = new ConfigurationClientWrapper(TestHelpers.PrimaryConfigStoreEndpoint, mockClient1.Object);
+ ConfigurationClientWrapper cw2 = new ConfigurationClientWrapper(TestHelpers.SecondaryConfigStoreEndpoint, mockClient2.Object);
+
+ var clientList = new List() { cw1, cw2 };
+ var configClientManager = new ConfigurationClientManager(clientList);
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.MinBackoffDuration = TimeSpan.FromSeconds(2);
+ options.ClientManager = configClientManager;
+ options.ConfigureRefresh(refreshOptions =>
+ {
+ refreshOptions.Register("TestKey1", "label")
+ .SetRefreshInterval(RefreshInterval);
+ });
+ options.ReplicaDiscoveryEnabled = false;
+ options.LoadBalancingEnabled = true;
+
+ refresher = options.GetRefresher();
+ }).Build();
+
+ // Ensure client 2 was used for startup
+ mockClient2.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Exactly(1));
+
+ Thread.Sleep(TimeSpan.FromSeconds(2));
+ await refresher.RefreshAsync();
+
+ // Ensure client 1 has recovered and is used for refresh
+ mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(0));
+
+ mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1));
+
+ Thread.Sleep(RefreshInterval);
+ await refresher.RefreshAsync();
+
+ mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1));
+ }
+ }
+}
diff --git a/tests/Tests.AzureAppConfiguration/LoggingTests.cs b/tests/Tests.AzureAppConfiguration/LoggingTests.cs
index 96aa5933..2c614acf 100644
--- a/tests/Tests.AzureAppConfiguration/LoggingTests.cs
+++ b/tests/Tests.AzureAppConfiguration/LoggingTests.cs
@@ -52,10 +52,10 @@ public class LoggingTests
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"),
contentType: KeyVaultConstants.ContentType + "; charset=utf-8");
- TimeSpan CacheExpirationTime = TimeSpan.FromSeconds(1);
+ TimeSpan RefreshInterval = TimeSpan.FromSeconds(1);
[Fact]
- public void ValidateExceptionLoggedDuringRefresh()
+ public async Task ValidateExceptionLoggedDuringRefresh()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -81,7 +81,7 @@ public void ValidateExceptionLoggedDuringRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -91,15 +91,15 @@ public void ValidateExceptionLoggedDuringRefresh()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.NotEqual("newValue1", config["TestKey1"]);
Assert.Contains(LoggingConstants.RefreshFailedError, warningInvocation);
}
[Fact]
- public void ValidateUnauthorizedExceptionLoggedDuringRefresh()
+ public async Task ValidateUnauthorizedExceptionLoggedDuringRefresh()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -123,7 +123,7 @@ public void ValidateUnauthorizedExceptionLoggedDuringRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -133,15 +133,15 @@ public void ValidateUnauthorizedExceptionLoggedDuringRefresh()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.NotEqual("newValue1", config["TestKey1"]);
Assert.Contains(LoggingConstants.RefreshFailedDueToAuthenticationError, warningInvocation);
}
[Fact]
- public void ValidateInvalidOperationExceptionLoggedDuringRefresh()
+ public async Task ValidateInvalidOperationExceptionLoggedDuringRefresh()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -165,7 +165,7 @@ public void ValidateInvalidOperationExceptionLoggedDuringRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -175,15 +175,15 @@ public void ValidateInvalidOperationExceptionLoggedDuringRefresh()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.NotEqual("newValue1", config["TestKey1"]);
Assert.Contains(LoggingConstants.RefreshFailedError, warningInvocation);
}
[Fact]
- public void ValidateKeyVaultExceptionLoggedDuringRefresh()
+ public async Task ValidateKeyVaultExceptionLoggedDuringRefresh()
{
IConfigurationRefresher refresher = null;
@@ -231,7 +231,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("SentinelKey", refreshAll: true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
})
@@ -241,14 +241,14 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
// Update sentinel key-value to trigger refreshAll operation
sentinelKv.Value = "UpdatedSentinelValue";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.Contains(LoggingConstants.RefreshFailedDueToKeyVaultError + "\nNo key vault credential or secret resolver callback configured, and no matching secret client could be found.", warningInvocation);
}
[Fact]
- public void ValidateAggregateExceptionWithInnerOperationCanceledExceptionLoggedDuringRefresh()
+ public async Task ValidateAggregateExceptionWithInnerOperationCanceledExceptionLoggedDuringRefresh()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -272,7 +272,7 @@ public void ValidateAggregateExceptionWithInnerOperationCanceledExceptionLoggedD
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -282,15 +282,15 @@ public void ValidateAggregateExceptionWithInnerOperationCanceledExceptionLoggedD
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.NotEqual("newValue1", config["TestKey1"]);
Assert.Contains(LoggingConstants.RefreshFailedError, warningInvocation);
}
[Fact]
- public void ValidateOperationCanceledExceptionLoggedDuringRefresh()
+ public async Task ValidateOperationCanceledExceptionLoggedDuringRefresh()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -312,7 +312,7 @@ public void ValidateOperationCanceledExceptionLoggedDuringRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -322,18 +322,18 @@ public void ValidateOperationCanceledExceptionLoggedDuringRefresh()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
using var cancellationSource = new CancellationTokenSource();
cancellationSource.Cancel();
- refresher.TryRefreshAsync(cancellationSource.Token).Wait();
+ await refresher.TryRefreshAsync(cancellationSource.Token);
Assert.NotEqual("newValue1", config["TestKey1"]);
Assert.Contains(LoggingConstants.RefreshCanceledError, warningInvocation);
}
[Fact]
- public void ValidateFailoverToDifferentEndpointMessageLoggedAfterFailover()
+ public async Task ValidateFailoverToDifferentEndpointMessageLoggedAfterFailover()
{
IConfigurationRefresher refresher = null;
var mockClient1 = GetMockConfigurationClient();
@@ -368,7 +368,7 @@ public void ValidateFailoverToDifferentEndpointMessageLoggedAfterFailover()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -380,8 +380,8 @@ public void ValidateFailoverToDifferentEndpointMessageLoggedAfterFailover()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.Equal("newValue1", config["TestKey1"]);
Assert.Contains(LogHelper.BuildFailoverMessage(TestHelpers.PrimaryConfigStoreEndpoint.ToString(), TestHelpers.SecondaryConfigStoreEndpoint.ToString()), warningInvocation);
@@ -392,15 +392,15 @@ public void ValidateFailoverToDifferentEndpointMessageLoggedAfterFailover()
FirstKeyValue.Value = "TestValue1";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.Equal("newValue1", config["TestKey1"]);
Assert.Contains(LogHelper.BuildLastEndpointFailedMessage(TestHelpers.SecondaryConfigStoreEndpoint.ToString()), warningInvocation);
}
[Fact]
- public void ValidateConfigurationUpdatedSuccessLoggedDuringRefresh()
+ public async Task ValidateConfigurationUpdatedSuccessLoggedDuringRefresh()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -424,7 +424,7 @@ public void ValidateConfigurationUpdatedSuccessLoggedDuringRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -434,15 +434,15 @@ public void ValidateConfigurationUpdatedSuccessLoggedDuringRefresh()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.Equal("newValue1", config["TestKey1"]);
Assert.Contains(LogHelper.BuildConfigurationUpdatedMessage(), invocation);
}
[Fact]
- public void ValidateCorrectEndpointLoggedOnConfigurationUpdate()
+ public async Task ValidateCorrectEndpointLoggedOnConfigurationUpdate()
{
IConfigurationRefresher refresher = null;
var mockClient1 = new Mock();
@@ -474,7 +474,7 @@ public void ValidateCorrectEndpointLoggedOnConfigurationUpdate()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -483,15 +483,15 @@ public void ValidateCorrectEndpointLoggedOnConfigurationUpdate()
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
// We should see the second client's endpoint logged since the first client is backed off
Assert.Contains(LogHelper.BuildKeyValueReadMessage(KeyValueChangeType.Modified, _kvCollection[0].Key, _kvCollection[0].Label, TestHelpers.SecondaryConfigStoreEndpoint.ToString().TrimEnd('/')), invocation);
}
[Fact]
- public void ValidateCorrectKeyValueLoggedDuringRefresh()
+ public async Task ValidateCorrectKeyValueLoggedDuringRefresh()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -520,7 +520,7 @@ public void ValidateCorrectKeyValueLoggedDuringRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", false).Register("TestKey2", false)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -530,8 +530,8 @@ public void ValidateCorrectKeyValueLoggedDuringRefresh()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.Equal("newValue1", config["TestKey1"]);
Assert.Contains(LogHelper.BuildKeyValueReadMessage(KeyValueChangeType.Modified, _kvCollection[0].Key, _kvCollection[0].Label, TestHelpers.PrimaryConfigStoreEndpoint.ToString().TrimEnd('/')), verboseInvocation);
@@ -540,7 +540,7 @@ public void ValidateCorrectKeyValueLoggedDuringRefresh()
}
[Fact]
- public void ValidateCorrectKeyVaultSecretLoggedDuringRefresh()
+ public async Task ValidateCorrectKeyVaultSecretLoggedDuringRefresh()
{
string _secretValue = "SecretValue from KeyVault";
Uri vaultUri = new Uri("https://keyvault-theclassics.vault.azure.net");
@@ -579,9 +579,9 @@ public void ValidateCorrectKeyVaultSecretLoggedDuringRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
- options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object).SetSecretRefreshInterval(CacheExpirationTime));
+ options.ConfigureKeyVault(kv => kv.Register(mockSecretClient.Object).SetSecretRefreshInterval(RefreshInterval));
refresher = options.GetRefresher();
})
.Build();
@@ -593,8 +593,8 @@ public void ValidateCorrectKeyVaultSecretLoggedDuringRefresh()
""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/Password3/6db5a48680104dda9097b1e6d859e553""
}
";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.Contains(LogHelper.BuildKeyVaultSecretReadMessage(_kvr.Key, _kvr.Label), verboseInvocation);
Assert.Contains(LogHelper.BuildKeyVaultSettingUpdatedMessage(_kvr.Key), informationalInvocation);
}
diff --git a/tests/Tests.AzureAppConfiguration/MapTests.cs b/tests/Tests.AzureAppConfiguration/MapTests.cs
index 140282b7..95e13525 100644
--- a/tests/Tests.AzureAppConfiguration/MapTests.cs
+++ b/tests/Tests.AzureAppConfiguration/MapTests.cs
@@ -48,7 +48,7 @@ public class MapTests
ConfigurationSetting FirstKeyValue => _kvCollection.First();
ConfigurationSetting sentinelKv = new ConfigurationSetting("SentinelKey", "SentinelValue");
- TimeSpan CacheExpirationTime = TimeSpan.FromSeconds(1);
+ TimeSpan RefreshInterval = TimeSpan.FromSeconds(1);
string _certValue = "Certificate Value from KeyVault";
string _secretValue = "SecretValue from KeyVault";
@@ -130,7 +130,7 @@ public void MapTransformKeyVaultValueBeforeAdapters()
}
[Fact]
- public void MapTransformWithRefresh()
+ public async Task MapTransformWithRefresh()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -144,7 +144,7 @@ public void MapTransformWithRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
options.Map((setting) =>
{
@@ -175,15 +175,15 @@ public void MapTransformWithRefresh()
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.Equal("newValue1 mapped first", config["TestKey1"]);
Assert.Equal("TestValue2 second", config["TestKey2"]);
}
[Fact]
- public void MapTransformSettingKeyWithRefresh()
+ public async Task MapTransformSettingKeyWithRefresh()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -197,7 +197,7 @@ public void MapTransformSettingKeyWithRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
options.Map((setting) =>
{
@@ -225,15 +225,15 @@ public void MapTransformSettingKeyWithRefresh()
FirstKeyValue.Value = "newValue1";
_kvCollection.Last().Value = "newValue2";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.Equal("newValue1 changed", config["newTestKey1"]);
Assert.Equal("newValue2", config["TestKey2"]);
}
[Fact]
- public void MapTransformSettingLabelWithRefresh()
+ public async Task MapTransformSettingLabelWithRefresh()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -247,7 +247,7 @@ public void MapTransformSettingLabelWithRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
options.Map((setting) =>
{
@@ -273,15 +273,15 @@ public void MapTransformSettingLabelWithRefresh()
FirstKeyValue.Value = "newValue1";
_kvCollection.Last().Value = "newValue2";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.Equal("newValue1 changed", config["TestKey1"]);
Assert.Equal("newValue2 changed", config["TestKey2"]);
}
[Fact]
- public void MapTransformSettingCreateDuplicateKeyWithRefresh()
+ public async Task MapTransformSettingCreateDuplicateKeyWithRefresh()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -295,7 +295,7 @@ public void MapTransformSettingCreateDuplicateKeyWithRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
options.Map((setting) =>
{
@@ -321,15 +321,15 @@ public void MapTransformSettingCreateDuplicateKeyWithRefresh()
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.Equal("TestValue2 changed", config["TestKey2"]);
Assert.Null(config["TestKey1"]);
}
[Fact]
- public void MapCreateNewSettingWithRefresh()
+ public async Task MapCreateNewSettingWithRefresh()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -343,7 +343,7 @@ public void MapCreateNewSettingWithRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
options.Map((setting) =>
{
@@ -366,8 +366,8 @@ public void MapCreateNewSettingWithRefresh()
Assert.Equal("TestValue2", config["TestKey2"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.Equal("mappedValue1", config["TestKey1"]);
Assert.Equal("TestValue2", config["TestKey2"]);
@@ -450,7 +450,7 @@ public void MapAsyncResolveKeyVaultReference()
}
[Fact]
- public void MapTransformSettingKeyWithLogAndRefresh()
+ public async Task MapTransformSettingKeyWithLogAndRefresh()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -479,7 +479,7 @@ public void MapTransformSettingKeyWithLogAndRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
options.Map((setting) =>
{
@@ -507,8 +507,8 @@ public void MapTransformSettingKeyWithLogAndRefresh()
FirstKeyValue.Value = "newValue1";
_kvCollection.Last().Value = "newValue2";
- Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ Thread.Sleep(RefreshInterval);
+ await refresher.TryRefreshAsync();
Assert.Equal("newValue1 changed", config["newTestKey1"]);
Assert.Equal("newValue2", config["TestKey2"]);
diff --git a/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs b/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs
index aa88fc86..1b3291b2 100644
--- a/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs
+++ b/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs
@@ -170,10 +170,59 @@ public class PushRefreshTests
"Microsoft.AppConfiguration.KeyValueModified", "2",
BinaryData.FromString("{\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"syncToken\":\"sn;CRAle3342\"}")
)
+ },
+
+ // Test that last syncToken is used
+ {
+ "sn;BYRte4456",
+ new EventGridEvent(
+ "https://store2.resource.io/kv/searchQuery2",
+ "Microsoft.AppConfiguration.KeyValueModified", "2",
+ BinaryData.FromString("{\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"syncToken\":\"sn;CRAle3342\",\"syncToken\":\"sn;BYRte4456\"}")
+ )
}
};
- ConfigurationSetting FirstKeyValue => _kvCollection.First();
+ Dictionary _invalidFormatEventGridEvents = new Dictionary
+ {
+ {
+ "sn;Vxujfidne",
+ new EventGridEvent(
+ "https://store1.resource.io/kv/searchQuery1",
+ "Microsoft.AppConfiguration.KeyValueModified", "2",
+ BinaryData.FromString("\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"syncToken\":\"sn;Vxujfidne\"}")
+ )
+ },
+
+ {
+ "sn;AxRty78B",
+ new EventGridEvent(
+ "https://store1.resource.io/kv/searchQuery1",
+ "Microsoft.AppConfiguration.KeyValueModified", "2",
+ BinaryData.FromString("{\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"syncToken\":\"sn;Vxujfidne\"")
+ )
+ },
+
+ {
+ "sn;Ttylmable",
+ new EventGridEvent(
+ "https://store1.resource.io/kv/searchQuery2",
+ "Microsoft.AppConfiguration.KeyValueDeleted", "2",
+ BinaryData.FromString("{\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"fake_property\":{\"syncToken\":\"sn;Ttylmable\"}}")
+ )
+ },
+
+ {
+ "sn;CRAle3342",
+ new EventGridEvent(
+ "https://store2.resource.io/kv/searchQuery2",
+ "Microsoft.AppConfiguration.KeyValueModified", "2",
+ BinaryData.FromString("{\"fake_property\":{\"key\":\"searchQuery1\",\"etag\":\"etagValue1\",\"syncToken\":\"sn;CRAle3342\"}}")
+ )
+ }
+ };
+
+ ConfigurationSetting FirstKeyValue => _kvCollection.First();
[Fact]
public void ValidatePushNotificationCreation()
@@ -191,6 +240,17 @@ public void ValidatePushNotificationCreation()
}
}
+ [Fact]
+ public void InvalidPushNotificationCreation()
+ {
+ foreach (KeyValuePair eventGridAndSync in _invalidFormatEventGridEvents)
+ {
+ EventGridEvent eventGridEvent = eventGridAndSync.Value;
+
+ Assert.False(eventGridEvent.TryCreatePushNotification(out PushNotification _));
+ }
+ }
+
[Fact]
public void ProcessPushNotificationThrowsArgumentExceptions()
{
@@ -207,7 +267,7 @@ public void ProcessPushNotificationThrowsArgumentExceptions()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromDays(30));
+ .SetRefreshInterval(TimeSpan.FromDays(30));
});
refresher = options.GetRefresher();
})
@@ -226,7 +286,7 @@ public void ProcessPushNotificationThrowsArgumentExceptions()
}
[Fact]
- public void SyncTokenUpdatesCorrectNumberOfTimes()
+ public async Task SyncTokenUpdatesCorrectNumberOfTimes()
{
// Arrange
var mockResponse = new Mock();
@@ -243,7 +303,7 @@ public void SyncTokenUpdatesCorrectNumberOfTimes()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromDays(30));
+ .SetRefreshInterval(TimeSpan.FromDays(30));
});
refresher = options.GetRefresher();
})
@@ -252,7 +312,7 @@ public void SyncTokenUpdatesCorrectNumberOfTimes()
foreach (PushNotification pushNotification in _pushNotificationList)
{
refresher.ProcessPushNotification(pushNotification, TimeSpan.FromSeconds(0));
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
}
var validNotificationKVWatcherCount = 8;
@@ -264,7 +324,7 @@ public void SyncTokenUpdatesCorrectNumberOfTimes()
}
[Fact]
- public void RefreshAsyncUpdatesConfig()
+ public async Task RefreshAsyncUpdatesConfig()
{
// Arrange
var mockResponse = new Mock();
@@ -280,7 +340,7 @@ public void RefreshAsyncUpdatesConfig()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromDays(30));
+ .SetRefreshInterval(TimeSpan.FromDays(30));
});
refresher = options.GetRefresher();
})
@@ -291,7 +351,7 @@ public void RefreshAsyncUpdatesConfig()
FirstKeyValue.Value = "newValue1";
refresher.ProcessPushNotification(_pushNotificationList.First(), TimeSpan.FromSeconds(0));
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
Assert.Equal("newValue1", config["TestKey1"]);
}
diff --git a/tests/Tests.AzureAppConfiguration/RefreshTests.cs b/tests/Tests.AzureAppConfiguration/RefreshTests.cs
index 48becbd9..1faae290 100644
--- a/tests/Tests.AzureAppConfiguration/RefreshTests.cs
+++ b/tests/Tests.AzureAppConfiguration/RefreshTests.cs
@@ -97,7 +97,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
options.ConfigureRefresh(refresh =>
{
refresh.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(60));
+ .SetRefreshInterval(TimeSpan.FromSeconds(60));
});
})
.Build();
@@ -119,7 +119,7 @@ public void RefreshTests_RefreshRegisteredKeysAreLoadedOnStartup_CustomUseQuery(
{
refreshOptions.Register("TestKey2", "label")
.Register("TestKey3", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(60));
+ .SetRefreshInterval(TimeSpan.FromSeconds(60));
});
})
.Build();
@@ -130,7 +130,7 @@ public void RefreshTests_RefreshRegisteredKeysAreLoadedOnStartup_CustomUseQuery(
}
[Fact]
- public void RefreshTests_RefreshIsSkippedIfCacheIsNotExpired()
+ public async Task RefreshTests_RefreshIsSkippedIfCacheIsNotExpired()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -142,7 +142,7 @@ public void RefreshTests_RefreshIsSkippedIfCacheIsNotExpired()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(10));
+ .SetRefreshInterval(TimeSpan.FromSeconds(10));
});
refresher = options.GetRefresher();
@@ -152,13 +152,13 @@ public void RefreshTests_RefreshIsSkippedIfCacheIsNotExpired()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
Assert.Equal("TestValue1", config["TestKey1"]);
}
[Fact]
- public void RefreshTests_RefreshIsSkippedIfKvNotInSelectAndCacheIsNotExpired()
+ public async Task RefreshTests_RefreshIsSkippedIfKvNotInSelectAndCacheIsNotExpired()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClientSelectKeyLabel();
@@ -171,7 +171,7 @@ public void RefreshTests_RefreshIsSkippedIfKvNotInSelectAndCacheIsNotExpired()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(10));
+ .SetRefreshInterval(TimeSpan.FromSeconds(10));
});
refresher = options.GetRefresher();
@@ -181,13 +181,13 @@ public void RefreshTests_RefreshIsSkippedIfKvNotInSelectAndCacheIsNotExpired()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
Assert.Equal("TestValue1", config["TestKey1"]);
}
[Fact]
- public void RefreshTests_RefreshIsNotSkippedIfCacheIsExpired()
+ public async Task RefreshTests_RefreshIsNotSkippedIfCacheIsExpired()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -200,7 +200,7 @@ public void RefreshTests_RefreshIsNotSkippedIfCacheIsExpired()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -213,13 +213,13 @@ public void RefreshTests_RefreshIsNotSkippedIfCacheIsExpired()
// Wait for the cache to expire
Thread.Sleep(1500);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
Assert.Equal("newValue", config["TestKey1"]);
}
[Fact]
- public void RefreshTests_RefreshAllFalseDoesNotUpdateEntireConfiguration()
+ public async Task RefreshTests_RefreshAllFalseDoesNotUpdateEntireConfiguration()
{
var keyValueCollection = new List(_kvCollection);
IConfigurationRefresher refresher = null;
@@ -233,7 +233,7 @@ public void RefreshTests_RefreshAllFalseDoesNotUpdateEntireConfiguration()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label") // refreshAll: false
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -249,7 +249,7 @@ public void RefreshTests_RefreshAllFalseDoesNotUpdateEntireConfiguration()
// Wait for the cache to expire
Thread.Sleep(1500);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
Assert.Equal("newValue", config["TestKey1"]);
Assert.NotEqual("newValue", config["TestKey2"]);
@@ -257,7 +257,7 @@ public void RefreshTests_RefreshAllFalseDoesNotUpdateEntireConfiguration()
}
[Fact]
- public void RefreshTests_RefreshAllTrueUpdatesEntireConfiguration()
+ public async Task RefreshTests_RefreshAllTrueUpdatesEntireConfiguration()
{
var keyValueCollection = new List(_kvCollection);
IConfigurationRefresher refresher = null;
@@ -271,7 +271,7 @@ public void RefreshTests_RefreshAllTrueUpdatesEntireConfiguration()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", refreshAll: true)
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -287,7 +287,7 @@ public void RefreshTests_RefreshAllTrueUpdatesEntireConfiguration()
// Wait for the cache to expire
Thread.Sleep(1500);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
Assert.Equal("newValue", config["TestKey1"]);
Assert.Equal("newValue", config["TestKey2"]);
@@ -295,7 +295,7 @@ public void RefreshTests_RefreshAllTrueUpdatesEntireConfiguration()
}
[Fact]
- public void RefreshTests_RefreshAllTrueRemovesDeletedConfiguration()
+ public async Task RefreshTests_RefreshAllTrueRemovesDeletedConfiguration()
{
var keyValueCollection = new List(_kvCollection);
var mockResponse = new Mock();
@@ -342,7 +342,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", refreshAll: true)
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -359,7 +359,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
// Wait for the cache to expire
Thread.Sleep(1500);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
Assert.Equal("newValue", config["TestKey1"]);
Assert.Equal("TestValue2", config["TestKey2"]);
@@ -367,7 +367,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
}
[Fact]
- public void RefreshTests_RefreshAllForNonExistentSentinelDoesNothing()
+ public async Task RefreshTests_RefreshAllForNonExistentSentinelDoesNothing()
{
var keyValueCollection = new List(_kvCollection);
var mockResponse = new Mock();
@@ -415,7 +415,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
{
refreshOptions.Register("TestKey1", "label")
.Register("NonExistentKey", refreshAll: true)
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -433,7 +433,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
// Wait for the cache to expire
Thread.Sleep(1500);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
// Validate that key-values registered for refresh were updated
Assert.Equal("newValue1", config["TestKey1"]);
@@ -444,7 +444,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
}
[Fact]
- public void RefreshTests_SingleServerCallOnSimultaneousMultipleRefresh()
+ public async void RefreshTests_SingleServerCallOnSimultaneousMultipleRefresh()
{
var keyValueCollection = new List(_kvCollection);
var requestCount = 0;
@@ -489,7 +489,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -505,7 +505,8 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
var task1 = Task.Run(() => WaitAndRefresh(refresher, 1500));
var task2 = Task.Run(() => WaitAndRefresh(refresher, 3000));
var task3 = Task.Run(() => WaitAndRefresh(refresher, 4500));
- Task.WaitAll(task1, task2, task3);
+
+ await Task.WhenAll(task1, task2, task3);
Assert.Equal("newValue", config["TestKey1"]);
Assert.Equal(2, requestCount);
@@ -525,7 +526,7 @@ public void RefreshTests_RefreshAsyncThrowsOnRequestFailedException()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -548,7 +549,7 @@ public void RefreshTests_RefreshAsyncThrowsOnRequestFailedException()
}
[Fact]
- public void RefreshTests_TryRefreshAsyncReturnsFalseOnRequestFailedException()
+ public async Task RefreshTests_TryRefreshAsyncReturnsFalseOnRequestFailedException()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -561,7 +562,7 @@ public void RefreshTests_TryRefreshAsyncReturnsFalseOnRequestFailedException()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -577,14 +578,14 @@ public void RefreshTests_TryRefreshAsyncReturnsFalseOnRequestFailedException()
// Wait for the cache to expire
Thread.Sleep(1500);
- bool result = refresher.TryRefreshAsync().Result;
+ bool result = await refresher.TryRefreshAsync();
Assert.False(result);
Assert.NotEqual("newValue", config["TestKey1"]);
}
[Fact]
- public void RefreshTests_TryRefreshAsyncUpdatesConfigurationAndReturnsTrueOnSuccess()
+ public async Task RefreshTests_TryRefreshAsyncUpdatesConfigurationAndReturnsTrueOnSuccess()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -597,7 +598,7 @@ public void RefreshTests_TryRefreshAsyncUpdatesConfigurationAndReturnsTrueOnSucc
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -610,14 +611,14 @@ public void RefreshTests_TryRefreshAsyncUpdatesConfigurationAndReturnsTrueOnSucc
// Wait for the cache to expire
Thread.Sleep(1500);
- bool result = refresher.TryRefreshAsync().Result;
+ bool result = await refresher.TryRefreshAsync();
Assert.True(result);
Assert.Equal("newValue", config["TestKey1"]);
}
[Fact]
- public void RefreshTests_TryRefreshAsyncReturnsFalseForAuthenticationFailedException()
+ public async Task RefreshTests_TryRefreshAsyncReturnsFalseForAuthenticationFailedException()
{
IConfigurationRefresher refresher = null;
var mockResponse = new Mock();
@@ -640,7 +641,7 @@ public void RefreshTests_TryRefreshAsyncReturnsFalseForAuthenticationFailedExcep
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -654,13 +655,13 @@ public void RefreshTests_TryRefreshAsyncReturnsFalseForAuthenticationFailedExcep
Thread.Sleep(1500);
// First call to GetConfigurationSettingAsync does not throw
- Assert.True(refresher.TryRefreshAsync().Result);
+ Assert.True(await refresher.TryRefreshAsync());
// Wait for the cache to expire
Thread.Sleep(1500);
// Second call to GetConfigurationSettingAsync throws KeyVaultReferenceException
- Assert.False(refresher.TryRefreshAsync().Result);
+ Assert.False(await refresher.TryRefreshAsync());
}
[Fact]
@@ -693,7 +694,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", refreshAll: true)
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -735,7 +736,7 @@ public async Task RefreshTests_UpdatesAllSettingsIfInitialLoadFails()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -774,7 +775,7 @@ await Assert.ThrowsAsync(async () =>
}
[Fact]
- public void RefreshTests_SentinelKeyNotUpdatedOnRefreshAllFailure()
+ public async Task RefreshTests_SentinelKeyNotUpdatedOnRefreshAllFailure()
{
var keyValueCollection = new List(_kvCollection);
var mockResponse = new Mock();
@@ -811,7 +812,7 @@ Response