From 5966deb897983f64dd577cdf60014802eb1cfb36 Mon Sep 17 00:00:00 2001
From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
Date: Wed, 3 Apr 2024 13:49:10 -0700
Subject: [PATCH 01/21] Target .NET 8 and update test packages (#529)
* target .NET 8 and update other testing packages
* use version of dependency injection equal to net version
* remove test comment
* fix json content test
* revert change to flattener
* change back version of json package
* add back versions
* fix test to work for new versions of microsoft.extensions.configuration.json
* remove conditional packages
* fix spacing, test with ado pipeline
* fix spacing
* update other examples
* allow informationalversionattribute format in test for useragentheader
---
build/install-dotnet.ps1 | 6 ++++--
.../ConfigStoreDemo/ConfigStoreDemo.csproj | 2 +-
.../ConsoleAppWithFailOver.csproj | 2 +-
.../ConsoleApplication.csproj | 2 +-
...t.Azure.AppConfiguration.AspNetCore.csproj | 2 +-
...e.AppConfiguration.Functions.Worker.csproj | 2 +-
...ts.AzureAppConfiguration.AspNetCore.csproj | 15 +++++++------
...reAppConfiguration.Functions.Worker.csproj | 15 +++++++------
.../JsonContentTypeTests.cs | 13 ++++--------
.../Tests.AzureAppConfiguration.csproj | 21 +++++++++----------
tests/Tests.AzureAppConfiguration/Tests.cs | 2 +-
11 files changed, 42 insertions(+), 40 deletions(-)
diff --git a/build/install-dotnet.ps1 b/build/install-dotnet.ps1
index 11552a78..194dbf5a 100644
--- a/build/install-dotnet.ps1
+++ b/build/install-dotnet.ps1
@@ -1,8 +1,10 @@
-# Installs .NET 6 and .NET 7 for CI/CD environment
+# Installs .NET 6, .NET 7, 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 7.0
+
+&([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/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj b/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
index dcff4691..484b64e2 100644
--- a/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
+++ b/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
@@ -2,7 +2,7 @@
Exe
- net7.0
+ net8.0
diff --git a/examples/ConsoleApplication/ConsoleApplication.csproj b/examples/ConsoleApplication/ConsoleApplication.csproj
index 2bc2e9ec..be38de42 100644
--- a/examples/ConsoleApplication/ConsoleApplication.csproj
+++ b/examples/ConsoleApplication/ConsoleApplication.csproj
@@ -3,7 +3,7 @@
false
Exe
- net7.0
+ net8.0
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 5107c108..5ef0cff7 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;net7.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
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 2dbb3aac..c8e017dc 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;net7.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
diff --git a/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj b/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj
index ef89659c..a8cd3b51 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;net7.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..f5af5a3f 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;net7.0;net8.0
8.0
false
true
@@ -10,11 +10,14 @@
-
-
-
-
-
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
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/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
index 0dfcabbd..45bb3a60 100644
--- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
+++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
@@ -1,7 +1,7 @@
- net48;net6.0;net7.0
+ net48;net6.0;net7.0;net8.0
8.0
false
true
@@ -11,23 +11,22 @@
-
-
-
+
+
-
-
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
-
+
-
-
-
-
Always
diff --git a/tests/Tests.AzureAppConfiguration/Tests.cs b/tests/Tests.AzureAppConfiguration/Tests.cs
index b7e61978..c3b27124 100644
--- a/tests/Tests.AzureAppConfiguration/Tests.cs
+++ b/tests/Tests.AzureAppConfiguration/Tests.cs
@@ -239,7 +239,7 @@ public void TestUserAgentHeader()
// 4. Contains the name and version of the App Configuration SDK package
// 5. Contains the runtime information (target framework, OS description etc.) in the format set by the SDK
// 6. Does not contain any additional components
- string userAgentRegex = @"^Microsoft\.Extensions\.Configuration\.AzureAppConfiguration/\d+\.\d+\.\d+(-preview(\.\d+)?)?,azsdk-net-Data.AppConfiguration/[.+\w-]+ \([.;\w\s]+\)$";
+ string userAgentRegex = @"^Microsoft\.Extensions\.Configuration\.AzureAppConfiguration/\d+\.\d+\.\d+(\+[a-z0-9]+)?(-preview(\.\d+)?)?,azsdk-net-Data.AppConfiguration/[.+\w-]+ \([.;\w\s]+\)$";
var response = new MockResponse(200);
response.SetContent(SerializationHelpers.Serialize(_kvCollectionPageOne.ToArray(), TestHelpers.SerializeBatch));
From 2745270d39a91fa5f27d2d23cccaf387bc110b63 Mon Sep 17 00:00:00 2001
From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
Date: Tue, 9 Apr 2024 14:04:20 -0700
Subject: [PATCH 02/21] Deprecate and rename refresh APIs for clarity (#532)
* some progress
* update variables and methods related to cache expiration
* update configstoredemo
* change cacheexpirationinterval to a set method
* allow either property or method for feature flag options for now
* fix summaries for refreshoptions
* PR revisions
* fix method name reference in obsolete message
* fix incorrect refreshinterval name
---
examples/ConfigStoreDemo/Program.cs | 2 +-
examples/ConsoleApplication/Program.cs | 2 +-
.../AzureAppConfigurationOptions.cs | 12 +-
.../AzureAppConfigurationProvider.cs | 45 ++--
.../AzureAppConfigurationRefreshOptions.cs | 23 +-
.../Constants/ErrorMessages.cs | 2 +-
.../Constants/RefreshConstants.cs | 8 +-
.../FeatureManagement/FeatureFlagOptions.cs | 33 ++-
.../Models/KeyValueWatcher.cs | 6 +-
.../FailoverTests.cs | 10 +-
.../FeatureManagementTests.cs | 213 ++++++++++++++----
.../KeyVaultReferenceTests.cs | 34 +--
.../LoggingTests.cs | 50 ++--
tests/Tests.AzureAppConfiguration/MapTests.cs | 26 +--
.../PushRefreshTests.cs | 6 +-
.../RefreshTests.cs | 44 ++--
16 files changed, 346 insertions(+), 170 deletions(-)
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/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.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
index d79838da..a14fba48 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
@@ -215,10 +215,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 +247,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 +381,7 @@ public AzureAppConfigurationOptions ConfigureRefresh(Action _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 +111,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
@@ -192,13 +192,13 @@ public async Task RefreshAsync(CancellationToken cancellationToken)
EnsureFeatureManagementVersionInspected();
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 +237,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 +266,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 +336,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 +350,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))
@@ -401,10 +401,10 @@ await CallWithRequestTracing(
adapter.InvalidateCache();
}
- // 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);
}
}
@@ -536,16 +536,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;
}
}
@@ -722,10 +722,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)
@@ -980,10 +980,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(
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/Constants/ErrorMessages.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
index 0c19cccf..44f97a63 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
@@ -5,7 +5,7 @@ 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.";
}
}
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/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/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/tests/Tests.AzureAppConfiguration/FailoverTests.cs b/tests/Tests.AzureAppConfiguration/FailoverTests.cs
index d302e407..603975b9 100644
--- a/tests/Tests.AzureAppConfiguration/FailoverTests.cs
+++ b/tests/Tests.AzureAppConfiguration/FailoverTests.cs
@@ -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;
@@ -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();
@@ -193,7 +193,7 @@ public void FailOverTests_BackoffStateIsUpdatedOnSuccessfulRequest()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -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();
})
@@ -372,7 +372,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..46e7bd14 100644
--- a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
+++ b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
@@ -338,7 +338,7 @@ public class FeatureManagementTests
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()
@@ -385,12 +385,82 @@ public void WatchesFeatureFlags()
.Returns(new MockAsyncPageable(featureFlags));
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();
+
+ 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);
+ refresher.RefreshAsync().Wait();
+
+ 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 void WatchesFeatureFlagsUsingCacheExpirationInterval()
+ {
+ 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));
+
+ var cacheExpirationInterval = TimeSpan.FromSeconds(1);
+
+ IConfigurationRefresher refresher = null;
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.UseFeatureFlags(o => o.CacheExpirationInterval = cacheExpirationInterval);
refresher = options.GetRefresher();
})
@@ -434,7 +504,7 @@ public void WatchesFeatureFlags()
featureFlags.Add(_kv2);
// Sleep to let the cache expire
- Thread.Sleep(cacheExpirationTimeSpan);
+ Thread.Sleep(cacheExpirationInterval);
refresher.RefreshAsync().Wait();
Assert.Equal("Browser", config["FeatureManagement:Beta:EnabledFor:0:Name"]);
@@ -443,9 +513,75 @@ public void WatchesFeatureFlags()
Assert.Equal("SuperUsers", config["FeatureManagement:MyFeature2:EnabledFor:0:Name"]);
}
+ [Fact]
+ public void 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);
+
+ refresher.RefreshAsync().Wait();
+
+ 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 void SkipRefreshIfCacheExpirationIntervalHasNotElapsed()
{
var featureFlags = new List { _kv };
@@ -575,19 +711,18 @@ public void UsesEtagForFeatureFlagRefresh()
.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();
mockClient.Verify(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Exactly(3));
@@ -600,7 +735,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,7 +747,7 @@ public void SelectFeatureFlags()
options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(testClient);
options.UseFeatureFlags(ff =>
{
- ff.CacheExpirationInterval = cacheExpiration;
+ ff.SetRefreshInterval(RefreshInterval);
ff.Select(featureFlagPrefix + "*", labelFilter);
});
})
@@ -829,7 +963,7 @@ public void MultipleCallsToUseFeatureFlagsWithSelectAndLabel()
}
[Fact]
- public void DifferentCacheExpirationsForMultipleFeatureFlagRegistrations()
+ public void DifferentRefreshIntervalsForMultipleFeatureFlagRegistrations()
{
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
@@ -837,8 +971,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 +990,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);
});
@@ -913,8 +1047,8 @@ 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);
+ // Sleep to let the refresh interval for feature flag with label1 elapse
+ Thread.Sleep(refreshInterval1);
refresher.RefreshAsync().Wait();
Assert.Equal("Browser", config["FeatureManagement:App1_Feature1:EnabledFor:0:Name"]);
@@ -924,17 +1058,17 @@ public void DifferentCacheExpirationsForMultipleFeatureFlagRegistrations()
Assert.Equal("Disabled", config["FeatureManagement:App2_Feature1:Status"]);
Assert.Equal("AlwaysOn", config["FeatureManagement:App2_Feature2:EnabledFor:0:Name"]);
- // even though App2_Feature3 feature flag has been added, its value should not be loaded in config because label2 cache has not expired
+ // even though App2_Feature3 feature flag has been added, its value should not be loaded in config because label2 refresh interval has not elapsed
Assert.Null(config["FeatureManagement:App2_Feature3"]);
}
[Fact]
- public void OverwrittenCacheExpirationForSameFeatureFlagRegistrations()
+ public void 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,13 +1083,13 @@ 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();
@@ -991,11 +1125,11 @@ public void OverwrittenCacheExpirationForSameFeatureFlagRegistrations()
contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1" + "f"));
- Thread.Sleep(cacheExpiration1);
+ Thread.Sleep(refreshInterval1);
refresher.RefreshAsync().Wait();
- // The cache expiration time for feature flags was overwritten by second call to UseFeatureFlags.
- // Sleeping for cacheExpiration1 time should not update feature flags.
+ // 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("AlwaysOn", config["FeatureManagement:App1_Feature1:EnabledFor:0:Name"]);
Assert.Equal("Disabled", config["FeatureManagement:App1_Feature2:Status"]);
Assert.Equal("Disabled", config["FeatureManagement:App2_Feature1:Status"]);
@@ -1010,7 +1144,6 @@ public void SelectAndRefreshSingleFeatureFlag()
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 +1160,7 @@ public void SelectAndRefreshSingleFeatureFlag()
options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
options.UseFeatureFlags(ff =>
{
- ff.CacheExpirationInterval = cacheExpiration;
+ ff.SetRefreshInterval(RefreshInterval);
ff.Select(prefix1, label1);
});
@@ -1060,8 +1193,8 @@ 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);
+ // Sleep to let the refresh interval for feature flag with label1 elapse
+ Thread.Sleep(RefreshInterval);
refresher.RefreshAsync().Wait();
Assert.Equal("Browser", config["FeatureManagement:Feature1:EnabledFor:0:Name"]);
@@ -1101,7 +1234,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,14 +1262,14 @@ public void ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefresh()
contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1" + "f"));
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
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);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.Null(config["FeatureManagement:MyFeature:EnabledFor:0:Name"]);
@@ -1177,11 +1310,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,7 +1323,7 @@ public void ValidateFeatureFlagsUnchangedLogged()
Assert.Equal("SuperUsers", config["FeatureManagement:MyFeature2:EnabledFor:0:Name"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.Equal("SuperUsers", config["FeatureManagement:MyFeature2:EnabledFor:0:Name"]);
Assert.Contains(LogHelper.BuildFeatureFlagsUnchangedMessage(TestHelpers.PrimaryConfigStoreEndpoint.ToString().TrimEnd('/')), verboseInvocation);
@@ -1245,9 +1378,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,7 +1435,7 @@ public void MapTransformFeatureFlagWithRefresh()
contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1" + "f"));
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.Equal("newValue1", config["TestKey1"]);
diff --git a/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs b/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
index 9a80a350..288cb1fb 100644
--- a/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
+++ b/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
@@ -600,7 +600,7 @@ public void ThrowsWhenSecretRefreshIntervalIsTooShort()
public void 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 +646,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("Sentinel")
- .SetCacheExpiration(cacheExpirationTime);
+ .SetRefreshInterval(refreshInterval);
});
refresher = options.GetRefresher();
@@ -658,7 +658,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
// Update sentinel key-value
sentinelKv.Value = "Value2";
- Thread.Sleep(cacheExpirationTime);
+ Thread.Sleep(refreshInterval);
refresher.RefreshAsync().Wait();
Assert.Equal("Value2", config["Sentinel"]);
@@ -673,7 +673,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
public void 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 +718,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("Sentinel", refreshAll: true)
- .SetCacheExpiration(cacheExpirationTime);
+ .SetRefreshInterval(refreshInterval);
});
refresher = options.GetRefresher();
@@ -730,7 +730,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
// Update sentinel key-value to trigger refresh operation
sentinelKv.Value = "Value2";
- Thread.Sleep(cacheExpirationTime);
+ Thread.Sleep(refreshInterval);
refresher.RefreshAsync().Wait();
Assert.Equal("Value2", config["Sentinel"]);
@@ -745,7 +745,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
public void 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 +765,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,7 +775,7 @@ public void SecretIsReloadedFromKeyVaultWhenCacheExpires()
Assert.Equal(_secretValue, config[_kv.Key]);
// Sleep to let the secret cache expire
- Thread.Sleep(cacheExpirationTime);
+ Thread.Sleep(refreshInterval);
refresher.RefreshAsync().Wait();
Assert.Equal(_secretValue, config[_kv.Key]);
@@ -788,7 +788,7 @@ public void SecretIsReloadedFromKeyVaultWhenCacheExpires()
public void 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 +808,7 @@ public void SecretsWithDefaultRefreshInterval()
options.ConfigureKeyVault(kv =>
{
kv.Register(mockSecretClient.Object);
- kv.SetSecretRefreshInterval(shortCacheExpirationTime);
+ kv.SetSecretRefreshInterval(shortRefreshInterval);
});
refresher = options.GetRefresher();
@@ -819,7 +819,7 @@ public void SecretsWithDefaultRefreshInterval()
Assert.Equal(_secretValue, config["TK2"]);
// Sleep to let the secret cache expire for both secrets
- Thread.Sleep(shortCacheExpirationTime);
+ Thread.Sleep(shortRefreshInterval);
refresher.RefreshAsync().Wait();
Assert.Equal(_secretValue, config["TK1"]);
@@ -833,8 +833,8 @@ public void SecretsWithDefaultRefreshInterval()
public void 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 +854,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,7 +866,7 @@ public void SecretsWithDifferentRefreshIntervals()
Assert.Equal(_secretValue, config["TK2"]);
// Sleep to let the secret cache expire for one secret
- Thread.Sleep(shortCacheExpirationTime);
+ Thread.Sleep(shortRefreshInterval);
refresher.RefreshAsync().Wait();
Assert.Equal(_secretValue, config["TK1"]);
diff --git a/tests/Tests.AzureAppConfiguration/LoggingTests.cs b/tests/Tests.AzureAppConfiguration/LoggingTests.cs
index 96aa5933..aa1ead13 100644
--- a/tests/Tests.AzureAppConfiguration/LoggingTests.cs
+++ b/tests/Tests.AzureAppConfiguration/LoggingTests.cs
@@ -52,7 +52,7 @@ 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()
@@ -81,7 +81,7 @@ public void ValidateExceptionLoggedDuringRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -91,7 +91,7 @@ public void ValidateExceptionLoggedDuringRefresh()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.NotEqual("newValue1", config["TestKey1"]);
@@ -123,7 +123,7 @@ public void ValidateUnauthorizedExceptionLoggedDuringRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -133,7 +133,7 @@ public void ValidateUnauthorizedExceptionLoggedDuringRefresh()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.NotEqual("newValue1", config["TestKey1"]);
@@ -165,7 +165,7 @@ public void ValidateInvalidOperationExceptionLoggedDuringRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -175,7 +175,7 @@ public void ValidateInvalidOperationExceptionLoggedDuringRefresh()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.NotEqual("newValue1", config["TestKey1"]);
@@ -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,7 +241,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
// Update sentinel key-value to trigger refreshAll operation
sentinelKv.Value = "UpdatedSentinelValue";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.Contains(LoggingConstants.RefreshFailedDueToKeyVaultError + "\nNo key vault credential or secret resolver callback configured, and no matching secret client could be found.", warningInvocation);
@@ -272,7 +272,7 @@ public void ValidateAggregateExceptionWithInnerOperationCanceledExceptionLoggedD
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -282,7 +282,7 @@ public void ValidateAggregateExceptionWithInnerOperationCanceledExceptionLoggedD
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.NotEqual("newValue1", config["TestKey1"]);
@@ -312,7 +312,7 @@ public void ValidateOperationCanceledExceptionLoggedDuringRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -322,7 +322,7 @@ public void ValidateOperationCanceledExceptionLoggedDuringRefresh()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
using var cancellationSource = new CancellationTokenSource();
cancellationSource.Cancel();
@@ -368,7 +368,7 @@ public void ValidateFailoverToDifferentEndpointMessageLoggedAfterFailover()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -380,7 +380,7 @@ public void ValidateFailoverToDifferentEndpointMessageLoggedAfterFailover()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.Equal("newValue1", config["TestKey1"]);
@@ -392,7 +392,7 @@ public void ValidateFailoverToDifferentEndpointMessageLoggedAfterFailover()
FirstKeyValue.Value = "TestValue1";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.Equal("newValue1", config["TestKey1"]);
@@ -424,7 +424,7 @@ public void ValidateConfigurationUpdatedSuccessLoggedDuringRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -434,7 +434,7 @@ public void ValidateConfigurationUpdatedSuccessLoggedDuringRefresh()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.Equal("newValue1", config["TestKey1"]);
@@ -474,7 +474,7 @@ public void ValidateCorrectEndpointLoggedOnConfigurationUpdate()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
refresher = options.GetRefresher();
@@ -483,7 +483,7 @@ public void ValidateCorrectEndpointLoggedOnConfigurationUpdate()
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
// We should see the second client's endpoint logged since the first client is backed off
@@ -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,7 +530,7 @@ public void ValidateCorrectKeyValueLoggedDuringRefresh()
Assert.Equal("TestValue1", config["TestKey1"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.Equal("newValue1", config["TestKey1"]);
@@ -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,7 +593,7 @@ public void ValidateCorrectKeyVaultSecretLoggedDuringRefresh()
""uri"":""https://keyvault-theclassics.vault.azure.net/secrets/Password3/6db5a48680104dda9097b1e6d859e553""
}
";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
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..7498aaef 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";
@@ -144,7 +144,7 @@ public void MapTransformWithRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
options.Map((setting) =>
{
@@ -175,7 +175,7 @@ public void MapTransformWithRefresh()
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.Equal("newValue1 mapped first", config["TestKey1"]);
@@ -197,7 +197,7 @@ public void MapTransformSettingKeyWithRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
options.Map((setting) =>
{
@@ -225,7 +225,7 @@ public void MapTransformSettingKeyWithRefresh()
FirstKeyValue.Value = "newValue1";
_kvCollection.Last().Value = "newValue2";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.Equal("newValue1 changed", config["newTestKey1"]);
@@ -247,7 +247,7 @@ public void MapTransformSettingLabelWithRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
options.Map((setting) =>
{
@@ -273,7 +273,7 @@ public void MapTransformSettingLabelWithRefresh()
FirstKeyValue.Value = "newValue1";
_kvCollection.Last().Value = "newValue2";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.Equal("newValue1 changed", config["TestKey1"]);
@@ -295,7 +295,7 @@ public void MapTransformSettingCreateDuplicateKeyWithRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
options.Map((setting) =>
{
@@ -321,7 +321,7 @@ public void MapTransformSettingCreateDuplicateKeyWithRefresh()
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.Equal("TestValue2 changed", config["TestKey2"]);
@@ -343,7 +343,7 @@ public void MapCreateNewSettingWithRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
options.Map((setting) =>
{
@@ -366,7 +366,7 @@ public void MapCreateNewSettingWithRefresh()
Assert.Equal("TestValue2", config["TestKey2"]);
FirstKeyValue.Value = "newValue1";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.Equal("mappedValue1", config["TestKey1"]);
@@ -479,7 +479,7 @@ public void MapTransformSettingKeyWithLogAndRefresh()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label", true)
- .SetCacheExpiration(CacheExpirationTime);
+ .SetRefreshInterval(RefreshInterval);
});
options.Map((setting) =>
{
@@ -507,7 +507,7 @@ public void MapTransformSettingKeyWithLogAndRefresh()
FirstKeyValue.Value = "newValue1";
_kvCollection.Last().Value = "newValue2";
- Thread.Sleep(CacheExpirationTime);
+ Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
Assert.Equal("newValue1 changed", config["newTestKey1"]);
diff --git a/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs b/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs
index aa88fc86..f1c5bde2 100644
--- a/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs
+++ b/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs
@@ -207,7 +207,7 @@ public void ProcessPushNotificationThrowsArgumentExceptions()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromDays(30));
+ .SetRefreshInterval(TimeSpan.FromDays(30));
});
refresher = options.GetRefresher();
})
@@ -243,7 +243,7 @@ public void SyncTokenUpdatesCorrectNumberOfTimes()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromDays(30));
+ .SetRefreshInterval(TimeSpan.FromDays(30));
});
refresher = options.GetRefresher();
})
@@ -280,7 +280,7 @@ public void RefreshAsyncUpdatesConfig()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromDays(30));
+ .SetRefreshInterval(TimeSpan.FromDays(30));
});
refresher = options.GetRefresher();
})
diff --git a/tests/Tests.AzureAppConfiguration/RefreshTests.cs b/tests/Tests.AzureAppConfiguration/RefreshTests.cs
index 48becbd9..27853ab3 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();
@@ -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();
@@ -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();
@@ -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();
@@ -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();
@@ -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();
@@ -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();
@@ -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();
@@ -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();
@@ -525,7 +525,7 @@ public void RefreshTests_RefreshAsyncThrowsOnRequestFailedException()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -561,7 +561,7 @@ public void RefreshTests_TryRefreshAsyncReturnsFalseOnRequestFailedException()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -597,7 +597,7 @@ public void RefreshTests_TryRefreshAsyncUpdatesConfigurationAndReturnsTrueOnSucc
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -640,7 +640,7 @@ public void RefreshTests_TryRefreshAsyncReturnsFalseForAuthenticationFailedExcep
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -693,7 +693,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 +735,7 @@ public async Task RefreshTests_UpdatesAllSettingsIfInitialLoadFails()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -811,7 +811,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();
@@ -861,7 +861,7 @@ public void RefreshTests_RefreshAllTrueForOverwrittenSentinelUpdatesEntireConfig
{
refreshOptions.Register("TestKeyWithMultipleLabels", "label1", refreshAll: true)
.Register("TestKeyWithMultipleLabels", "label2")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -903,7 +903,7 @@ public void RefreshTests_RefreshAllFalseForOverwrittenSentinelUpdatesConfig()
{
refreshOptions.Register("TestKeyWithMultipleLabels", "label1")
.Register("TestKeyWithMultipleLabels", "label2")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -945,7 +945,7 @@ public void RefreshTests_RefreshRegisteredKvOverwritesSelectedKv()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKeyWithMultipleLabels", "label1")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
@@ -1013,7 +1013,7 @@ public void RefreshTests_ConfigureRefreshThrowsOnNoRegistration()
{
options.ConfigureRefresh(refreshOptions =>
{
- refreshOptions.SetCacheExpiration(TimeSpan.FromSeconds(1));
+ refreshOptions.SetRefreshInterval(TimeSpan.FromSeconds(1));
});
})
.Build();
@@ -1034,7 +1034,7 @@ public void RefreshTests_RefreshIsCancelled()
options.ConfigureRefresh(refreshOptions =>
{
refreshOptions.Register("TestKey1", "label")
- .SetCacheExpiration(TimeSpan.FromSeconds(1));
+ .SetRefreshInterval(TimeSpan.FromSeconds(1));
});
refresher = options.GetRefresher();
From ebb6eef8d6270e0765c741a7a51d43baf20f0129 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 15 Apr 2024 17:16:51 -0700
Subject: [PATCH 03/21] Bump Azure.Identity in /examples/ConsoleAppWithFailOver
(#544)
Bumps [Azure.Identity](https://github.com/Azure/azure-sdk-for-net) from 1.10.2 to 1.11.0.
- [Release notes](https://github.com/Azure/azure-sdk-for-net/releases)
- [Commits](https://github.com/Azure/azure-sdk-for-net/compare/Azure.Identity_1.10.2...Azure.Identity_1.11.0)
---
updated-dependencies:
- dependency-name: Azure.Identity
dependency-type: direct:production
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj b/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
index 484b64e2..ff4b0398 100644
--- a/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
+++ b/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
@@ -6,7 +6,7 @@
-
+
From 4ce9ec53988dc27d3045406601ee9961c5e1cda8 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 16 Apr 2024 10:44:33 -0700
Subject: [PATCH 04/21] Bump Azure.Identity in
/tests/Tests.AzureAppConfiguration (#545)
Bumps [Azure.Identity](https://github.com/Azure/azure-sdk-for-net) from 1.10.2 to 1.11.0.
- [Release notes](https://github.com/Azure/azure-sdk-for-net/releases)
- [Commits](https://github.com/Azure/azure-sdk-for-net/compare/Azure.Identity_1.10.2...Azure.Identity_1.11.0)
---
updated-dependencies:
- dependency-name: Azure.Identity
dependency-type: direct:production
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
---
.../Tests.AzureAppConfiguration.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
index 45bb3a60..182d5377 100644
--- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
+++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
@@ -10,7 +10,7 @@
-
+
From a81ed763f983b910d95172efe9e56a91c5a5ba32 Mon Sep 17 00:00:00 2001
From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
Date: Mon, 22 Apr 2024 11:05:10 -0700
Subject: [PATCH 05/21] update sdk package to 1.4.1 to resolve 304 not modified
error issue (#548)
---
...rosoft.Extensions.Configuration.AzureAppConfiguration.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 6d6ddfd7..2657d805 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
@@ -14,7 +14,7 @@
-
+
From e74a679dca0fbd66d7b0c7aa2baf18e39a442778 Mon Sep 17 00:00:00 2001
From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
Date: Mon, 22 Apr 2024 12:14:58 -0700
Subject: [PATCH 06/21] Add an option to enable load balancing between replicas
(#535)
* in progress shuffle clients
* first draft load balancing, need tests
* WIP logic for client shuffling - unsure how to incorporate priority
* WIP
* shuffle all clients together, fix logic for order of clients used
* WIP
* WIP store shuffle order for combined list
* WIP shuffle logic
* WIP new design
* clean up logic/leftover code
* move tests, check if dynamic clients are available in getclients
* remove unused code
* fix syntax issues, extend test
* fix logic to increment client index
* add clarifying comment
* remove tests for now
* WIP tests
* add some tests, will add more
* add to last test
* remove unused usings
* add extra verify statement to check client isnt used
* edit logic to treat passed in clients as highest priority
* PR comment revisions
* check for more than one client in load balancing logic
* set clients equal to new copied list before finding next available client
* remove convert list to clients
---
.../AzureAppConfigurationOptions.cs | 7 +-
.../AzureAppConfigurationProvider.cs | 24 +++
.../AzureAppConfigurationSource.cs | 13 +-
.../ConfigurationClientManager.cs | 19 ++-
.../FailoverTests.cs | 15 +-
.../LoadBalancingTests.cs | 152 ++++++++++++++++++
6 files changed, 220 insertions(+), 10 deletions(-)
create mode 100644 tests/Tests.AzureAppConfiguration/LoadBalancingTests.cs
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
index a14fba48..b715f656 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.
///
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
index 031495b2..e5a8ac42 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
@@ -29,6 +29,7 @@ internal class AzureAppConfigurationProvider : ConfigurationProvider, IConfigura
private bool _isFeatureManagementVersionInspected;
private readonly bool _requestTracingEnabled;
private readonly IConfigurationClientManager _configClientManager;
+ private Uri _lastSuccessfulEndpoint;
private AzureAppConfigurationOptions _options;
private Dictionary _mappedData;
private Dictionary _watchedSettings = new Dictionary();
@@ -990,6 +991,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();
@@ -1010,6 +1032,8 @@ private async Task ExecuteWithFailOverPolicyAsync(
T result = await funcToExecute(currentClient).ConfigureAwait(false);
success = true;
+ _lastSuccessfulEndpoint = _configClientManager.GetEndpointForClient(currentClient);
+
return result;
}
catch (RequestFailedException rfe)
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/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/tests/Tests.AzureAppConfiguration/FailoverTests.cs b/tests/Tests.AzureAppConfiguration/FailoverTests.cs
index 603975b9..8df25e56 100644
--- a/tests/Tests.AzureAppConfiguration/FailoverTests.cs
+++ b/tests/Tests.AzureAppConfiguration/FailoverTests.cs
@@ -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();
diff --git a/tests/Tests.AzureAppConfiguration/LoadBalancingTests.cs b/tests/Tests.AzureAppConfiguration/LoadBalancingTests.cs
new file mode 100644
index 00000000..6a5ff417
--- /dev/null
+++ b/tests/Tests.AzureAppConfiguration/LoadBalancingTests.cs
@@ -0,0 +1,152 @@
+// 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 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 CacheExpirationTime = TimeSpan.FromSeconds(1);
+
+ [Fact]
+ public void 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")
+ .SetCacheExpiration(CacheExpirationTime);
+ });
+ 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(CacheExpirationTime);
+ refresher.RefreshAsync().Wait();
+
+ // 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(CacheExpirationTime);
+ refresher.RefreshAsync().Wait();
+
+ // Ensure client 1 was now used for refresh
+ mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1));
+ }
+
+ [Fact]
+ public void 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")
+ .SetCacheExpiration(CacheExpirationTime);
+ });
+ 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));
+ refresher.RefreshAsync().Wait();
+
+ // 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(CacheExpirationTime);
+ refresher.RefreshAsync().Wait();
+
+ mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1));
+ }
+ }
+}
From 7e4389b0219b7c725a37cd38a83bb4954c3f2052 Mon Sep 17 00:00:00 2001
From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
Date: Mon, 29 Apr 2024 15:26:51 -0700
Subject: [PATCH 07/21] Process json key-values without reflection (#538)
* WIP adding utf8jsonreader
* WIP jsondocument
* replace all json adapters with jsondocument
* combine code to serialize feature flag with code for adding key values to configuration
* remove unused classes from jsonserializer approach
* remove jsonserializer from testhelpers
* WIP add handling and error messages for invalid values
* WIP adding error messages
* use method to create formatexception
* validate for key vault uri and throw exception, fix logic in featuremanagement adapter
* update error message, fix expected type for enabled
* WIP
* WIP testing aot compatible
* small logic updates
* improve variable names
* edit error message
* add isaotcompatible property to packages
* fix error message for bool
* WIP use utf8jsonreader
* WIP
* use utf8jsonreader for feature flags
* use utf8jsonreader for keyvault secret reference
* PR comment revision
* WIP adding tests, PR revisions
* WIP add some tests, PR revisions
* key vault tests, use keyvaultreferenceexception for all scenarios because it's the established pattern
* fix missing setup
* PR revisions
* update tests again, PR revisions
* don't handle invalidoperationexception
* remove unused exception var
---
...t.Azure.AppConfiguration.AspNetCore.csproj | 3 +-
...e.AppConfiguration.Functions.Worker.csproj | 3 +-
.../AzureKeyVaultKeyValueAdapter.cs | 69 +++-
.../KeyVaultConstants.cs | 2 +
.../KeyVaultSecretReference.cs | 13 -
.../Constants/ErrorMessages.cs | 3 +
.../Extensions/EventGridEventExtensions.cs | 54 ++-
.../Extensions/Utf8JsonReaderExtensions.cs | 20 -
.../FeatureManagement/ClientFilter.cs | 5 +-
.../FeatureManagement/FeatureConditions.cs | 5 +-
.../FeatureManagement/FeatureFlag.cs | 6 +-
.../FeatureManagementConstants.cs | 8 +
.../FeatureManagementKeyValueAdapter.cs | 314 ++++++++++++++--
.../JsonKeyValueAdapter.cs | 11 +-
...Configuration.AzureAppConfiguration.csproj | 3 +-
.../FeatureManagementTests.cs | 353 +++++++++++++++++-
.../KeyVaultReferenceTests.cs | 176 +++++++++
.../PushRefreshTests.cs | 62 ++-
.../Tests.AzureAppConfiguration/TestHelper.cs | 22 +-
.../Tests.AzureAppConfiguration.csproj | 1 +
20 files changed, 1002 insertions(+), 131 deletions(-)
delete mode 100644 src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/KeyVaultSecretReference.cs
delete mode 100644 src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/Utf8JsonReaderExtensions.cs
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 5ef0cff7..d0bd30a3 100644
--- a/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
+++ b/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
@@ -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 c8e017dc..b2ac95b5 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
@@ -37,7 +37,8 @@
..\..\AzureAppConfigurationRules.ruleset
- True
+ true
+ true
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs
index 21b5165f..dd5cddbc 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, 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
};
}
@@ -102,5 +92,50 @@ 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/Constants/ErrorMessages.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
index 0c19cccf..61b4bb3b 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
@@ -7,5 +7,8 @@ internal class ErrorMessages
{
public const string CacheExpirationTimeTooShort = "The cache expiration time 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/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/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 3be1d3ce..a26fb6cc 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlag.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlag.cs
@@ -1,19 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
-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; }
}
-}
+}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
index b0e09723..e4775950 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
@@ -10,5 +10,13 @@ internal class FeatureManagementConstants
public const string SectionName = "FeatureManagement";
public const string EnabledFor = "EnabledFor";
public const string RequirementType = "RequirementType";
+
+ public const string EnabledJsonPropertyName = "enabled";
+ public const string IdJsonPropertyName = "id";
+ public const string ConditionsJsonPropertyName = "conditions";
+ public const string RequirementTypeJsonPropertyName = "requirement_type";
+ public const string ClientFiltersJsonPropertyName = "client_filters";
+ public const string NameJsonPropertyName = "name";
+ public const string ParametersJsonPropertyName = "parameters";
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
index b5138a46..a4f64d6b 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
@@ -22,58 +22,48 @@ public FeatureManagementKeyValueAdapter(FeatureFilterTracing featureFilterTracin
public Task>> ProcessKeyValue(ConfigurationSetting setting, Logger logger, CancellationToken cancellationToken)
{
- FeatureFlag featureFlag;
- try
- {
- featureFlag = JsonSerializer.Deserialize(setting.Value);
- }
- catch (JsonException e)
- {
- throw new FormatException(setting.Key, e);
- }
+ FeatureFlag featureFlag = ParseFeatureFlag(setting.Key, setting.Value);
var keyValues = new List>();
- if (featureFlag.Enabled)
+ if (!string.IsNullOrEmpty(featureFlag.Id))
{
- //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.Enabled)
{
- //
- // Always on
- keyValues.Add(new KeyValuePair($"{FeatureManagementConstants.SectionName}:{featureFlag.Id}", true.ToString()));
- }
- else
- {
- //
- // Conditionally on based on feature filters
- for (int i = 0; i < featureFlag.Conditions.ClientFilters.Count; i++)
+ if (featureFlag.Conditions?.ClientFilters == null || !featureFlag.Conditions.ClientFilters.Any())
+ {
+ keyValues.Add(new KeyValuePair($"{FeatureManagementConstants.SectionName}:{featureFlag.Id}", true.ToString()));
+ }
+ else
{
- ClientFilter clientFilter = featureFlag.Conditions.ClientFilters[i];
+ for (int i = 0; i < featureFlag.Conditions.ClientFilters.Count; i++)
+ {
+ ClientFilter clientFilter = featureFlag.Conditions.ClientFilters[i];
- _featureFilterTracing.UpdateFeatureFilterTracing(clientFilter.Name);
+ _featureFilterTracing.UpdateFeatureFilterTracing(clientFilter.Name);
- keyValues.Add(new KeyValuePair($"{FeatureManagementConstants.SectionName}:{featureFlag.Id}:{FeatureManagementConstants.EnabledFor}:{i}:Name", clientFilter.Name));
+ keyValues.Add(new KeyValuePair($"{FeatureManagementConstants.SectionName}:{featureFlag.Id}:{FeatureManagementConstants.EnabledFor}:{i}:Name", clientFilter.Name));
- foreach (KeyValuePair kvp in new JsonFlattener().FlattenJson(clientFilter.Parameters))
- {
- keyValues.Add(new KeyValuePair($"{FeatureManagementConstants.SectionName}:{featureFlag.Id}:{FeatureManagementConstants.EnabledFor}:{i}:Parameters:{kvp.Key}", kvp.Value));
+ foreach (KeyValuePair kvp in new JsonFlattener().FlattenJson(clientFilter.Parameters))
+ {
+ keyValues.Add(new KeyValuePair($"{FeatureManagementConstants.SectionName}:{featureFlag.Id}:{FeatureManagementConstants.EnabledFor}:{i}:Parameters:{kvp.Key}", kvp.Value));
+ }
}
- }
- //
- // process RequirementType only when filters are not empty
- if (featureFlag.Conditions.RequirementType != null)
- {
- keyValues.Add(new KeyValuePair(
- $"{FeatureManagementConstants.SectionName}:{featureFlag.Id}:{FeatureManagementConstants.RequirementType}",
- featureFlag.Conditions.RequirementType));
+ //
+ // process RequirementType only when filters are not empty
+ if (featureFlag.Conditions.RequirementType != null)
+ {
+ keyValues.Add(new KeyValuePair(
+ $"{FeatureManagementConstants.SectionName}:{featureFlag.Id}:{FeatureManagementConstants.RequirementType}",
+ featureFlag.Conditions.RequirementType));
+ }
}
}
- }
- else
- {
- keyValues.Add(new KeyValuePair($"{FeatureManagementConstants.SectionName}:{featureFlag.Id}", false.ToString()));
+ else
+ {
+ keyValues.Add(new KeyValuePair($"{FeatureManagementConstants.SectionName}:{featureFlag.Id}", false.ToString()));
+ }
}
return Task.FromResult>>(keyValues);
@@ -96,5 +86,251 @@ public bool NeedsRefresh()
{
return false;
}
+
+ private FormatException CreateFeatureFlagFormatException(string jsonPropertyName, string settingKey, string foundJsonValueKind, string expectedJsonValueKind)
+ {
+ return new FormatException(string.Format(
+ ErrorMessages.FeatureFlagInvalidJsonProperty,
+ jsonPropertyName,
+ settingKey,
+ foundJsonValueKind,
+ expectedJsonValueKind));
+ }
+
+ private FeatureFlag ParseFeatureFlag(string settingKey, string settingValue)
+ {
+ FeatureFlag 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.IdJsonPropertyName:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ featureFlag.Id = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.IdJsonPropertyName,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.EnabledJsonPropertyName:
+ {
+ 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.EnabledJsonPropertyName,
+ settingKey,
+ reader.TokenType.ToString(),
+ $"{JsonTokenType.True}' or '{JsonTokenType.False}");
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.ConditionsJsonPropertyName:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.StartObject)
+ {
+ featureFlag.Conditions = ParseFeatureConditions(ref reader, settingKey);
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.ConditionsJsonPropertyName,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartObject.ToString());
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+
+ break;
+ }
+ }
+ }
+ catch (JsonException e)
+ {
+ throw new FormatException(settingKey, e);
+ }
+
+ return featureFlag;
+ }
+
+ private FeatureConditions ParseFeatureConditions(ref Utf8JsonReader reader, string settingKey)
+ {
+ 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.ClientFiltersJsonPropertyName:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.Null)
+ {
+ break;
+ }
+ else if (reader.TokenType == JsonTokenType.StartArray)
+ {
+ 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
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.ClientFiltersJsonPropertyName,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartArray.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.RequirementTypeJsonPropertyName:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ featureConditions.RequirementType = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.RequirementTypeJsonPropertyName,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+
+ break;
+ }
+ }
+
+ return featureConditions;
+ }
+
+ private ClientFilter ParseClientFilter(ref Utf8JsonReader reader, string settingKey)
+ {
+ 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.NameJsonPropertyName:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.String)
+ {
+ clientFilter.Name = reader.GetString();
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.NameJsonPropertyName,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.String.ToString());
+ }
+
+ break;
+ }
+
+ case FeatureManagementConstants.ParametersJsonPropertyName:
+ {
+ if (reader.Read() && reader.TokenType == JsonTokenType.StartObject)
+ {
+ clientFilter.Parameters = JsonDocument.ParseValue(ref reader).RootElement;
+ }
+ else if (reader.TokenType != JsonTokenType.Null)
+ {
+ throw CreateFeatureFlagFormatException(
+ FeatureManagementConstants.ParametersJsonPropertyName,
+ settingKey,
+ reader.TokenType.ToString(),
+ JsonTokenType.StartObject.ToString());
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+
+ break;
+ }
+ }
+
+ return clientFilter;
+ }
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs
index 0a47923f..2c11d423 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)
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 2657d805..b195a26f 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
@@ -47,7 +47,8 @@
..\..\AzureAppConfigurationRules.ruleset
- True
+ true
+ true
diff --git a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
index 4eb83a90..99348464 100644
--- a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
+++ b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
@@ -9,7 +9,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
-using Microsoft.Extensions.Options;
using Moq;
using System;
using System.Collections.Generic;
@@ -85,7 +84,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",
@@ -477,6 +708,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 cacheExpiration = 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.CacheExpirationInterval = cacheExpiration;
+ 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 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 = _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.CacheExpirationInterval = cacheExpiration;
+ 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()
{
diff --git a/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs b/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
index 0cccf3cd..88c15ffb 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()
{
@@ -875,5 +961,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/PushRefreshTests.cs b/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs
index aa88fc86..c94e877f 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()
{
diff --git a/tests/Tests.AzureAppConfiguration/TestHelper.cs b/tests/Tests.AzureAppConfiguration/TestHelper.cs
index fe1685d9..477b4429 100644
--- a/tests/Tests.AzureAppConfiguration/TestHelper.cs
+++ b/tests/Tests.AzureAppConfiguration/TestHelper.cs
@@ -115,16 +115,22 @@ public static ConfigurationSetting CloneSetting(ConfigurationSetting setting)
public static List LoadJsonSettingsFromFile(string path)
{
List _kvCollection = new List();
- var valueArray = JsonSerializer.Deserialize(File.ReadAllText(path)).EnumerateArray();
- foreach (var setting in valueArray)
+
+ using (JsonDocument document = JsonDocument.Parse(File.ReadAllText(path)))
{
- ConfigurationSetting kv = ConfigurationModelFactory
- .ConfigurationSetting(
- key: setting.GetProperty("key").ToString(),
- value: setting.GetProperty("value").GetRawText(),
- contentType: setting.GetProperty("contentType").ToString());
- _kvCollection.Add(kv);
+ var valueArray = document.RootElement.EnumerateArray();
+
+ foreach (var setting in valueArray)
+ {
+ ConfigurationSetting kv = ConfigurationModelFactory
+ .ConfigurationSetting(
+ key: setting.GetProperty("key").ToString(),
+ value: setting.GetProperty("value").GetRawText(),
+ contentType: setting.GetProperty("contentType").ToString());
+ _kvCollection.Add(kv);
+ }
}
+
return _kvCollection;
}
diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
index 182d5377..b0aef653 100644
--- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
+++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
@@ -7,6 +7,7 @@
true
..\..\build\AzureAppConfiguration.snk
false
+ false
From a9eacbc1ba62e12eb1a42c2a2ebb326289f5e0f8 Mon Sep 17 00:00:00 2001
From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
Date: Wed, 1 May 2024 13:04:08 -0700
Subject: [PATCH 08/21] Update package versions for 7.2.0 stable release (#552)
---
.../Microsoft.Azure.AppConfiguration.AspNetCore.csproj | 2 +-
.../Microsoft.Azure.AppConfiguration.Functions.Worker.csproj | 2 +-
...rosoft.Extensions.Configuration.AzureAppConfiguration.csproj | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj b/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
index d0bd30a3..c081cacd 100644
--- a/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
+++ b/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
@@ -21,7 +21,7 @@
- 7.1.0
+ 7.2.0
diff --git a/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj b/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj
index b2ac95b5..90831b4d 100644
--- a/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj
+++ b/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj
@@ -24,7 +24,7 @@
- 7.1.0
+ 7.2.0
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
index b195a26f..466c58be 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
@@ -34,7 +34,7 @@
- 7.1.0
+ 7.2.0
From 03be00689a4a702d99dcb70dcc4d5ec3a3405e57 Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Fri, 31 May 2024 20:22:16 +0800
Subject: [PATCH 09/21] fix xUnit1031 warning (#557)
---
.../FailoverTests.cs | 10 +--
.../FeatureManagementTests.cs | 26 ++++----
.../KeyVaultReferenceTests.cs | 20 +++---
.../LoggingTests.cs | 46 ++++++-------
tests/Tests.AzureAppConfiguration/MapTests.cs | 24 +++----
.../PushRefreshTests.cs | 8 +--
.../RefreshTests.cs | 65 ++++++++++---------
7 files changed, 100 insertions(+), 99 deletions(-)
diff --git a/tests/Tests.AzureAppConfiguration/FailoverTests.cs b/tests/Tests.AzureAppConfiguration/FailoverTests.cs
index d302e407..e9a7a12a 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;
@@ -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);
@@ -144,7 +144,7 @@ public void FailOverTests_PropagatesNonFailOverableExceptions()
}
[Fact]
- public void FailOverTests_BackoffStateIsUpdatedOnSuccessfulRequest()
+ public async Task FailOverTests_BackoffStateIsUpdatedOnSuccessfulRequest()
{
// Arrange
IConfigurationRefresher refresher = null;
@@ -199,7 +199,7 @@ public void FailOverTests_BackoffStateIsUpdatedOnSuccessfulRequest()
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));
diff --git a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
index 99348464..809187fd 100644
--- a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
+++ b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
@@ -452,7 +452,7 @@ public void UsesFeatureFlags()
}
[Fact]
- public void WatchesFeatureFlags()
+ public async Task WatchesFeatureFlags()
{
var featureFlags = new List { _kv };
@@ -513,7 +513,7 @@ public void WatchesFeatureFlags()
// Sleep to let the cache expire
Thread.Sleep(cacheExpirationTimeSpan);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
Assert.Equal("Browser", config["FeatureManagement:Beta:EnabledFor:0:Name"]);
Assert.Equal("Chrome", config["FeatureManagement:Beta:EnabledFor:0:Parameters:AllowedBrowsers:0"]);
@@ -523,7 +523,7 @@ public void WatchesFeatureFlags()
[Fact]
- public void SkipRefreshIfCacheNotExpired()
+ public async Task SkipRefreshIfCacheNotExpired()
{
var featureFlags = new List { _kv };
@@ -581,7 +581,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"]);
@@ -618,7 +618,7 @@ public void PreservesDefaultQuery()
}
[Fact]
- public void QueriesFeatureFlags()
+ public async Task QueriesFeatureFlags()
{
var mockTransport = new MockTransport(req =>
{
@@ -646,7 +646,7 @@ 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()))
@@ -667,7 +667,7 @@ public void UsesEtagForFeatureFlagRefresh()
// Sleep to let the cache expire
Thread.Sleep(cacheExpirationTimeSpan);
- refresher.TryRefreshAsync().Wait();
+ await refresher.TryRefreshAsync();
mockClient.Verify(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Exactly(3));
}
@@ -1025,7 +1025,7 @@ public void MultipleCallsToUseFeatureFlagsWithSelectAndLabel()
}
[Fact]
- public void DifferentCacheExpirationsForMultipleFeatureFlagRegistrations()
+ public async Task DifferentCacheExpirationsForMultipleFeatureFlagRegistrations()
{
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
@@ -1111,7 +1111,7 @@ public void DifferentCacheExpirationsForMultipleFeatureFlagRegistrations()
// Sleep to let the cache for feature flag with label1 expire
Thread.Sleep(cacheExpiration1);
- refresher.RefreshAsync().Wait();
+ 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"]);
@@ -1125,7 +1125,7 @@ public void DifferentCacheExpirationsForMultipleFeatureFlagRegistrations()
}
[Fact]
- public void OverwrittenCacheExpirationForSameFeatureFlagRegistrations()
+ public async Task OverwrittenCacheExpirationForSameFeatureFlagRegistrations()
{
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
@@ -1188,7 +1188,7 @@ public void OverwrittenCacheExpirationForSameFeatureFlagRegistrations()
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1" + "f"));
Thread.Sleep(cacheExpiration1);
- refresher.RefreshAsync().Wait();
+ 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.
@@ -1200,7 +1200,7 @@ public void OverwrittenCacheExpirationForSameFeatureFlagRegistrations()
}
[Fact]
- public void SelectAndRefreshSingleFeatureFlag()
+ public async Task SelectAndRefreshSingleFeatureFlag()
{
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
@@ -1258,7 +1258,7 @@ public void SelectAndRefreshSingleFeatureFlag()
// Sleep to let the cache for feature flag with label1 expire
Thread.Sleep(cacheExpiration);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
Assert.Equal("Browser", config["FeatureManagement:Feature1:EnabledFor:0:Name"]);
Assert.Equal("Chrome", config["FeatureManagement:Feature1:EnabledFor:0:Parameters:AllowedBrowsers:0"]);
diff --git a/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs b/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
index 88c15ffb..9d4ce1e5 100644
--- a/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
+++ b/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
@@ -683,7 +683,7 @@ public void ThrowsWhenSecretRefreshIntervalIsTooShort()
}
[Fact]
- public void SecretIsReturnedFromCacheIfSecretCacheHasNotExpired()
+ public async Task SecretIsReturnedFromCacheIfSecretCacheHasNotExpired()
{
IConfigurationRefresher refresher = null;
TimeSpan cacheExpirationTime = TimeSpan.FromSeconds(1);
@@ -745,7 +745,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
// Update sentinel key-value
sentinelKv.Value = "Value2";
Thread.Sleep(cacheExpirationTime);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
Assert.Equal("Value2", config["Sentinel"]);
Assert.Equal(_secretValue, config[_kv.Key]);
@@ -756,7 +756,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
}
[Fact]
- public void CachedSecretIsInvalidatedWhenRefreshAllIsTrue()
+ public async Task CachedSecretIsInvalidatedWhenRefreshAllIsTrue()
{
IConfigurationRefresher refresher = null;
TimeSpan cacheExpirationTime = TimeSpan.FromSeconds(1);
@@ -817,7 +817,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
// Update sentinel key-value to trigger refresh operation
sentinelKv.Value = "Value2";
Thread.Sleep(cacheExpirationTime);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
Assert.Equal("Value2", config["Sentinel"]);
Assert.Equal(_secretValue, config[_kv.Key]);
@@ -828,7 +828,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
}
[Fact]
- public void SecretIsReloadedFromKeyVaultWhenCacheExpires()
+ public async Task SecretIsReloadedFromKeyVaultWhenCacheExpires()
{
IConfigurationRefresher refresher = null;
TimeSpan cacheExpirationTime = TimeSpan.FromSeconds(1);
@@ -862,7 +862,7 @@ public void SecretIsReloadedFromKeyVaultWhenCacheExpires()
// Sleep to let the secret cache expire
Thread.Sleep(cacheExpirationTime);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
Assert.Equal(_secretValue, config[_kv.Key]);
@@ -871,7 +871,7 @@ public void SecretIsReloadedFromKeyVaultWhenCacheExpires()
}
[Fact]
- public void SecretsWithDefaultRefreshInterval()
+ public async Task SecretsWithDefaultRefreshInterval()
{
IConfigurationRefresher refresher = null;
TimeSpan shortCacheExpirationTime = TimeSpan.FromSeconds(1);
@@ -906,7 +906,7 @@ public void SecretsWithDefaultRefreshInterval()
// Sleep to let the secret cache expire for both secrets
Thread.Sleep(shortCacheExpirationTime);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
Assert.Equal(_secretValue, config["TK1"]);
Assert.Equal(_secretValue, config["TK2"]);
@@ -916,7 +916,7 @@ public void SecretsWithDefaultRefreshInterval()
}
[Fact]
- public void SecretsWithDifferentRefreshIntervals()
+ public async Task SecretsWithDifferentRefreshIntervals()
{
IConfigurationRefresher refresher = null;
TimeSpan shortCacheExpirationTime = TimeSpan.FromSeconds(1);
@@ -953,7 +953,7 @@ public void SecretsWithDifferentRefreshIntervals()
// Sleep to let the secret cache expire for one secret
Thread.Sleep(shortCacheExpirationTime);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
Assert.Equal(_secretValue, config["TK1"]);
Assert.Equal(_secretValue, config["TK2"]);
diff --git a/tests/Tests.AzureAppConfiguration/LoggingTests.cs b/tests/Tests.AzureAppConfiguration/LoggingTests.cs
index 96aa5933..489bb78e 100644
--- a/tests/Tests.AzureAppConfiguration/LoggingTests.cs
+++ b/tests/Tests.AzureAppConfiguration/LoggingTests.cs
@@ -55,7 +55,7 @@ public class LoggingTests
TimeSpan CacheExpirationTime = TimeSpan.FromSeconds(1);
[Fact]
- public void ValidateExceptionLoggedDuringRefresh()
+ public async Task ValidateExceptionLoggedDuringRefresh()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -92,14 +92,14 @@ public void ValidateExceptionLoggedDuringRefresh()
FirstKeyValue.Value = "newValue1";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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();
@@ -134,14 +134,14 @@ public void ValidateUnauthorizedExceptionLoggedDuringRefresh()
FirstKeyValue.Value = "newValue1";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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();
@@ -176,14 +176,14 @@ public void ValidateInvalidOperationExceptionLoggedDuringRefresh()
FirstKeyValue.Value = "newValue1";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ await refresher.TryRefreshAsync();
Assert.NotEqual("newValue1", config["TestKey1"]);
Assert.Contains(LoggingConstants.RefreshFailedError, warningInvocation);
}
[Fact]
- public void ValidateKeyVaultExceptionLoggedDuringRefresh()
+ public async Task ValidateKeyVaultExceptionLoggedDuringRefresh()
{
IConfigurationRefresher refresher = null;
@@ -242,13 +242,13 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
// Update sentinel key-value to trigger refreshAll operation
sentinelKv.Value = "UpdatedSentinelValue";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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();
@@ -283,14 +283,14 @@ public void ValidateAggregateExceptionWithInnerOperationCanceledExceptionLoggedD
FirstKeyValue.Value = "newValue1";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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();
@@ -326,14 +326,14 @@ public void ValidateOperationCanceledExceptionLoggedDuringRefresh()
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();
@@ -381,7 +381,7 @@ public void ValidateFailoverToDifferentEndpointMessageLoggedAfterFailover()
FirstKeyValue.Value = "newValue1";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ await refresher.TryRefreshAsync();
Assert.Equal("newValue1", config["TestKey1"]);
Assert.Contains(LogHelper.BuildFailoverMessage(TestHelpers.PrimaryConfigStoreEndpoint.ToString(), TestHelpers.SecondaryConfigStoreEndpoint.ToString()), warningInvocation);
@@ -393,14 +393,14 @@ public void ValidateFailoverToDifferentEndpointMessageLoggedAfterFailover()
FirstKeyValue.Value = "TestValue1";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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();
@@ -435,14 +435,14 @@ public void ValidateConfigurationUpdatedSuccessLoggedDuringRefresh()
FirstKeyValue.Value = "newValue1";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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();
@@ -484,14 +484,14 @@ public void ValidateCorrectEndpointLoggedOnConfigurationUpdate()
FirstKeyValue.Value = "newValue1";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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();
@@ -531,7 +531,7 @@ public void ValidateCorrectKeyValueLoggedDuringRefresh()
FirstKeyValue.Value = "newValue1";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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");
@@ -594,7 +594,7 @@ public void ValidateCorrectKeyVaultSecretLoggedDuringRefresh()
}
";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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..ea5b45ca 100644
--- a/tests/Tests.AzureAppConfiguration/MapTests.cs
+++ b/tests/Tests.AzureAppConfiguration/MapTests.cs
@@ -130,7 +130,7 @@ public void MapTransformKeyVaultValueBeforeAdapters()
}
[Fact]
- public void MapTransformWithRefresh()
+ public async Task MapTransformWithRefresh()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -176,14 +176,14 @@ public void MapTransformWithRefresh()
FirstKeyValue.Value = "newValue1";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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();
@@ -226,14 +226,14 @@ public void MapTransformSettingKeyWithRefresh()
_kvCollection.Last().Value = "newValue2";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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();
@@ -274,14 +274,14 @@ public void MapTransformSettingLabelWithRefresh()
_kvCollection.Last().Value = "newValue2";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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();
@@ -322,14 +322,14 @@ public void MapTransformSettingCreateDuplicateKeyWithRefresh()
FirstKeyValue.Value = "newValue1";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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();
@@ -367,7 +367,7 @@ public void MapCreateNewSettingWithRefresh()
FirstKeyValue.Value = "newValue1";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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();
@@ -508,7 +508,7 @@ public void MapTransformSettingKeyWithLogAndRefresh()
_kvCollection.Last().Value = "newValue2";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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 c94e877f..304a7e02 100644
--- a/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs
+++ b/tests/Tests.AzureAppConfiguration/PushRefreshTests.cs
@@ -286,7 +286,7 @@ public void ProcessPushNotificationThrowsArgumentExceptions()
}
[Fact]
- public void SyncTokenUpdatesCorrectNumberOfTimes()
+ public async Task SyncTokenUpdatesCorrectNumberOfTimes()
{
// Arrange
var mockResponse = new Mock();
@@ -312,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;
@@ -324,7 +324,7 @@ public void SyncTokenUpdatesCorrectNumberOfTimes()
}
[Fact]
- public void RefreshAsyncUpdatesConfig()
+ public async Task RefreshAsyncUpdatesConfig()
{
// Arrange
var mockResponse = new Mock();
@@ -351,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..298fa4b8 100644
--- a/tests/Tests.AzureAppConfiguration/RefreshTests.cs
+++ b/tests/Tests.AzureAppConfiguration/RefreshTests.cs
@@ -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();
@@ -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();
@@ -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();
@@ -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;
@@ -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;
@@ -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();
@@ -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();
@@ -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;
@@ -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);
@@ -548,7 +549,7 @@ public void RefreshTests_RefreshAsyncThrowsOnRequestFailedException()
}
[Fact]
- public void RefreshTests_TryRefreshAsyncReturnsFalseOnRequestFailedException()
+ public async Task RefreshTests_TryRefreshAsyncReturnsFalseOnRequestFailedException()
{
IConfigurationRefresher refresher = null;
var mockClient = GetMockConfigurationClient();
@@ -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();
@@ -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();
@@ -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]
@@ -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();
@@ -827,7 +828,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
// Wait for the cache to expire
Thread.Sleep(1500);
- bool firstRefreshResult = refresher.TryRefreshAsync().Result;
+ bool firstRefreshResult = await refresher.TryRefreshAsync();
Assert.False(firstRefreshResult);
Assert.Equal("TestValue1", config["TestKey1"]);
@@ -837,7 +838,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
// Wait for the cache to expire
Thread.Sleep(1500);
- bool secondRefreshResult = refresher.TryRefreshAsync().Result;
+ bool secondRefreshResult = await refresher.TryRefreshAsync();
Assert.True(secondRefreshResult);
Assert.Equal("newValue", config["TestKey1"]);
@@ -846,7 +847,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o
}
[Fact]
- public void RefreshTests_RefreshAllTrueForOverwrittenSentinelUpdatesEntireConfiguration()
+ public async Task RefreshTests_RefreshAllTrueForOverwrittenSentinelUpdatesEntireConfiguration()
{
var keyValueCollection = new List(_kvCollection);
IConfigurationRefresher refresher = null;
@@ -878,7 +879,7 @@ public void RefreshTests_RefreshAllTrueForOverwrittenSentinelUpdatesEntireConfig
// 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"]);
@@ -887,7 +888,7 @@ public void RefreshTests_RefreshAllTrueForOverwrittenSentinelUpdatesEntireConfig
}
[Fact]
- public void RefreshTests_RefreshAllFalseForOverwrittenSentinelUpdatesConfig()
+ public async Task RefreshTests_RefreshAllFalseForOverwrittenSentinelUpdatesConfig()
{
var keyValueCollection = new List(_kvCollection);
ConfigurationSetting refreshRegisteredSetting = keyValueCollection.FirstOrDefault(s => s.Key == "TestKeyWithMultipleLabels" && s.Label == "label1");
@@ -920,7 +921,7 @@ public void RefreshTests_RefreshAllFalseForOverwrittenSentinelUpdatesConfig()
// Wait for the cache to expire
Thread.Sleep(1500);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
// Validate that refresh registered key-value was updated
Assert.Equal("TestValue1", config["TestKey1"]);
@@ -930,7 +931,7 @@ public void RefreshTests_RefreshAllFalseForOverwrittenSentinelUpdatesConfig()
}
[Fact]
- public void RefreshTests_RefreshRegisteredKvOverwritesSelectedKv()
+ public async Task RefreshTests_RefreshRegisteredKvOverwritesSelectedKv()
{
var keyValueCollection = new List(_kvCollection);
ConfigurationSetting refreshAllRegisteredSetting = keyValueCollection.FirstOrDefault(s => s.Key == "TestKeyWithMultipleLabels" && s.Label == "label1");
@@ -963,7 +964,7 @@ public void RefreshTests_RefreshRegisteredKvOverwritesSelectedKv()
// Wait for the cache to expire
Thread.Sleep(1500);
- refresher.RefreshAsync().Wait();
+ await refresher.RefreshAsync();
// Validate that only the refresh registered key-value was updated
Assert.Equal("TestValue1", config["TestKey1"]);
From 487c6c87f2bbe05b20fd7a77e122659efeaee22e Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Wed, 19 Jun 2024 13:55:15 +0800
Subject: [PATCH 10/21] Stop using packages of deprecated version and version
with vulnerability in example projects (#560)
* update package
* update package
---
.../ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj | 6 +++---
examples/ConsoleApplication/ConsoleApplication.csproj | 4 ++--
.../Tests.AzureAppConfiguration.csproj | 2 +-
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj b/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
index ff4b0398..ddcb2b93 100644
--- a/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
+++ b/examples/ConsoleAppWithFailOver/ConsoleAppWithFailOver.csproj
@@ -6,9 +6,9 @@
-
-
-
+
+
+
diff --git a/examples/ConsoleApplication/ConsoleApplication.csproj b/examples/ConsoleApplication/ConsoleApplication.csproj
index be38de42..bd4756fa 100644
--- a/examples/ConsoleApplication/ConsoleApplication.csproj
+++ b/examples/ConsoleApplication/ConsoleApplication.csproj
@@ -7,8 +7,8 @@
-
-
+
+
diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
index b0aef653..9b0f17c0 100644
--- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
+++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
@@ -11,7 +11,7 @@
-
+
From c437c87f57507c1b2bcdf1b9e1e6afd57ba404ba Mon Sep 17 00:00:00 2001
From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
Date: Fri, 21 Jun 2024 11:12:50 -0700
Subject: [PATCH 11/21] Use Microsoft feature flag schema and remove use of
reflection for json parsing (#556)
* WIP change constants to microsoft version, edit properties in fmkvadapter
* index feature flags within new microsoft section feature_flags
* fix all tests, use reset state
* add warning log for old feature management libraries with new provider package
* WIP adding deserialization changes, updating for variants/telemetry
* WIP adding exceptions for incorrect types within arrays, allocation properties
* WIP allocations mostly done
* update remaining properties, fix tests
* improve metadata error message
* update tests, add variant and telemetry testing
* remove alwayson
* fix warning version check
* Add tests for invalid cases
* test boolean allows string
* fix tests
* remove unused using
* add tests from original main PR for json parsing
* revisions
* run dotnet format on tests file
* Update src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
Co-authored-by: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
* some PR comment revisions
* make from and to nullable in percentile to check if any values were set
* Update src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/LoggingConstants.cs
Co-authored-by: Avani Gupta
* Update src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/LoggingConstants.cs
Co-authored-by: Avani Gupta
* PR comment revisions
* combine if statements
* change feature flag index state method
* use on configuration refresh and updated as adapter events
* change method names
* removed unused methods
---------
Co-authored-by: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Co-authored-by: Avani Gupta
---
.../AzureAppConfigurationProvider.cs | 23 +-
.../AzureKeyVaultKeyValueAdapter.cs | 7 +-
.../Constants/ErrorMessages.cs | 3 +
.../Constants/LoggingConstants.cs | 3 +
.../FeatureManagement/ClientFilter.cs | 3 -
.../FeatureManagement/FeatureAllocation.cs | 7 -
.../FeatureManagement/FeatureConditions.cs | 3 -
.../FeatureManagement/FeatureFlag.cs | 7 -
.../FeatureGroupAllocation.cs | 3 -
.../FeatureManagementConstants.cs | 62 +-
.../FeatureManagementKeyValueAdapter.cs | 1044 ++++++++++++++-
.../FeaturePercentileAllocation.cs | 5 -
.../FeatureManagement/FeatureTelemetry.cs | 5 +-
.../FeatureUserAllocation.cs | 3 -
.../FeatureManagement/FeatureVariant.cs | 5 -
.../IKeyValueAdapter.cs | 4 +-
.../JsonKeyValueAdapter.cs | 7 +-
.../LogHelper.cs | 5 +
.../FeatureManagementTests.cs | 1172 ++++++++++++-----
.../JsonContentTypeTests.cs | 6 +-
.../KeyVaultReferenceTests.cs | 3 +-
21 files changed, 1937 insertions(+), 443 deletions(-)
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
index e5a8ac42..993d2def 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
@@ -388,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);
}
}
}
@@ -399,7 +399,7 @@ await CallWithRequestTracing(
// Invalidate all the cached KeyVault secrets
foreach (IKeyValueAdapter adapter in _options.Adapters)
{
- adapter.InvalidateCache();
+ adapter.OnChangeDetected();
}
// Update the next refresh time for all refresh registered settings and feature flags
@@ -734,7 +734,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);
@@ -913,6 +913,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();
}
@@ -1206,11 +1211,21 @@ private void EnsureFeatureManagementVersionInspected()
{
if (!_isFeatureManagementVersionInspected)
{
+ const string FeatureManagementMinimumVersion = "3.2.0";
+
_isFeatureManagementVersionInspected = true;
if (_requestTracingEnabled && _requestTracingOptions != null)
{
- _requestTracingOptions.FeatureManagementVersion = TracingUtils.GetAssemblyVersion(RequestTracingConstants.FeatureManagementAssemblyName);
+ string featureManagementVersion = TracingUtils.GetAssemblyVersion(RequestTracingConstants.FeatureManagementAssemblyName);
+
+ // If the version is less than 3.2.0, log the schema version warning
+ if (featureManagementVersion != null && Version.Parse(featureManagementVersion) < Version.Parse(FeatureManagementMinimumVersion))
+ {
+ _logger.LogWarning(LogHelper.BuildFeatureManagementMicrosoftSchemaVersionWarningMessage());
+ }
+
+ _requestTracingOptions.FeatureManagementVersion = featureManagementVersion;
_requestTracingOptions.FeatureManagementAspNetCoreVersion = TracingUtils.GetAssemblyVersion(RequestTracingConstants.FeatureManagementAspNetCoreAssemblyName);
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs
index e49deb2c..8688e937 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureKeyVaultReference/AzureKeyVaultKeyValueAdapter.cs
@@ -86,7 +86,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,6 +98,11 @@ public void InvalidateCache(ConfigurationSetting setting = null)
}
}
+ public void OnConfigUpdated()
+ {
+ return;
+ }
+
public bool NeedsRefresh()
{
return _secretProvider.ShouldRefreshKeyVaultSecrets();
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
index 44f97a63..c7974736 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/ErrorMessages.cs
@@ -7,5 +7,8 @@ internal class ErrorMessages
{
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..4c62baa6 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/LoggingConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/LoggingConstants.cs
@@ -32,5 +32,8 @@ internal class LoggingConstants
public const string RefreshSkippedNoClientAvailable = "Refresh skipped because no endpoint is accessible.";
public const string RefreshFailedToGetSettingsFromEndpoint = "Failed to get configuration settings from endpoint";
public const string FailingOverToEndpoint = "Failing over to endpoint";
+ public const string FeatureManagementMicrosoftSchemaVersionWarning = "Your application may be using an older version of " +
+ "Microsoft.FeatureManagement library that isn't compatible with Microsoft.Extensions.Configuration.AzureAppConfiguration. Please update " +
+ "the Microsoft.FeatureManagement package to version 3.2.0 or later.";
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/ClientFilter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/ClientFilter.cs
index efd4023d..12145cd7 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; }
}
}
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..d1c23003 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; }
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlag.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlag.cs
index dddf52c3..31af50a6 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; }
}
}
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..9568a2cb 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
@@ -1,42 +1,48 @@
// 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";
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
index 7b15f881..62b0dcb1 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
@@ -17,6 +17,7 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManage
internal class FeatureManagementKeyValueAdapter : IKeyValueAdapter
{
private FeatureFilterTracing _featureFilterTracing;
+ private int _featureFlagIndex = 0;
public FeatureManagementKeyValueAdapter(FeatureFilterTracing featureFilterTracing)
{
@@ -25,40 +26,36 @@ public FeatureManagementKeyValueAdapter(FeatureFilterTracing featureFilterTracin
public Task>> ProcessKeyValue(ConfigurationSetting setting, Uri endpoint, Logger logger, CancellationToken cancellationToken)
{
- FeatureFlag featureFlag;
- try
- {
- featureFlag = JsonSerializer.Deserialize(setting.Value);
- }
- catch (JsonException e)
+ FeatureFlag featureFlag = ParseFeatureFlag(setting.Key, setting.Value);
+
+ var keyValues = new List>();
+
+ if (string.IsNullOrEmpty(featureFlag.Id))
{
- throw new FormatException(setting.Key, e);
+ return Task.FromResult>>(keyValues);
}
- var keyValues = new List>();
+ string featureFlagPath = $"{FeatureManagementConstants.FeatureManagementSectionName}:{FeatureManagementConstants.FeatureFlagsSectionName}:{_featureFlagIndex}";
+
+ _featureFlagIndex++;
+
+ keyValues.Add(new KeyValuePair($"{featureFlagPath}:{FeatureManagementConstants.Id}", featureFlag.Id));
- string featureFlagPath = $"{FeatureManagementConstants.SectionName}:{featureFlag.Id}";
+ keyValues.Add(new KeyValuePair($"{featureFlagPath}:{FeatureManagementConstants.Enabled}", featureFlag.Enabled.ToString()));
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
- {
- keyValues.Add(new KeyValuePair($"{featureFlagPath}:{FeatureManagementConstants.EnabledFor}:{0}:{FeatureManagementConstants.Name}", FeatureManagementConstants.AlwaysOnFilter));
- }
- else
+ if (featureFlag.Conditions?.ClientFilters != null && featureFlag.Conditions.ClientFilters.Any()) // workaround since we are not yet setting client filters to null
{
//
- // Conditionally on based on feature filters
+ // Conditionally based on feature filters
for (int i = 0; i < featureFlag.Conditions.ClientFilters.Count; i++)
{
ClientFilter clientFilter = featureFlag.Conditions.ClientFilters[i];
_featureFilterTracing.UpdateFeatureFilterTracing(clientFilter.Name);
- string clientFiltersPath = $"{featureFlagPath}:{FeatureManagementConstants.EnabledFor}:{i}";
+ string clientFiltersPath = $"{featureFlagPath}:{FeatureManagementConstants.Conditions}:{FeatureManagementConstants.ClientFilters}:{i}";
keyValues.Add(new KeyValuePair($"{clientFiltersPath}:{FeatureManagementConstants.Name}", clientFilter.Name));
@@ -73,15 +70,11 @@ public Task>> ProcessKeyValue(Configura
if (featureFlag.Conditions.RequirementType != null)
{
keyValues.Add(new KeyValuePair(
- $"{featureFlagPath}:{FeatureManagementConstants.RequirementType}",
+ $"{featureFlagPath}:{FeatureManagementConstants.Conditions}:{FeatureManagementConstants.RequirementType}",
featureFlag.Conditions.RequirementType));
}
}
}
- else
- {
- keyValues.Add(new KeyValuePair($"{featureFlagPath}:{FeatureManagementConstants.Status}", FeatureManagementConstants.Disabled));
- }
if (featureFlag.Variants != null)
{
@@ -135,13 +128,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 +149,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 +170,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++;
}
@@ -247,6 +240,995 @@ public bool NeedsRefresh()
return false;
}
+ public void OnChangeDetected(ConfigurationSetting setting = null)
+ {
+ return;
+ }
+
+ public void OnConfigUpdated()
+ {
+ _featureFlagIndex = 0;
+
+ return;
+ }
+
+ private FormatException CreateFeatureFlagFormatException(string jsonPropertyName, string settingKey, string foundJsonValueKind, string expectedJsonValueKind)
+ {
+ return new FormatException(string.Format(
+ ErrorMessages.FeatureFlagInvalidJsonProperty,
+ jsonPropertyName,
+ settingKey,
+ foundJsonValueKind,
+ expectedJsonValueKind));
+ }
+
+ 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;
+ }
+
+ private FeatureConditions ParseFeatureConditions(ref Utf8JsonReader reader, string settingKey)
+ {
+ 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;
+ }
+
+ private ClientFilter ParseClientFilter(ref Utf8JsonReader reader, string settingKey)
+ {
+ 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)
{
byte[] featureFlagIdHash;
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..ecc0302f 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs
@@ -90,7 +90,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..979fa7b7 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/LogHelper.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/LogHelper.cs
@@ -89,5 +89,10 @@ public static string BuildFallbackClientLookupFailMessage(string exceptionMessag
{
return $"{LoggingConstants.FallbackClientLookupError}\n{exceptionMessage}";
}
+
+ public static string BuildFeatureManagementMicrosoftSchemaVersionWarningMessage()
+ {
+ return LoggingConstants.FeatureManagementMicrosoftSchemaVersionWarning;
+ }
}
}
diff --git a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
index 46e7bd14..e0dc5d5b 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,6 +88,238 @@ public class FeatureManagementTests
contentType: FeatureManagementConstants.ContentType + ";charset=utf-8",
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1"));
+ 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(
@@ -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,34 +531,96 @@ 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 RefreshInterval = TimeSpan.FromSeconds(1);
@@ -361,16 +645,18 @@ public void UsesFeatureFlags()
})
.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"]);
+ Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("Firefox", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Safari", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
+ Assert.Equal("RollOut", config["feature_management:feature_flags:0:conditions:client_filters:1:name"]);
+ Assert.Equal("20", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Percentage"]);
+ Assert.Equal("US", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Region"]);
+ Assert.Equal("SuperUsers", config["feature_management:feature_flags:0:conditions:client_filters:2:name"]);
+ Assert.Equal("TimeWindow", config["feature_management:feature_flags:0:conditions:client_filters:3:name"]);
+ Assert.Equal("/Date(1578643200000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:Start"]);
+ Assert.Equal("/Date(1578686400000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:End"]);
}
[Fact]
@@ -395,16 +681,18 @@ public void WatchesFeatureFlags()
})
.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"]);
+ Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("Firefox", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Safari", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
+ Assert.Equal("RollOut", config["feature_management:feature_flags:0:conditions:client_filters:1:name"]);
+ Assert.Equal("20", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Percentage"]);
+ Assert.Equal("US", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Region"]);
+ Assert.Equal("SuperUsers", config["feature_management:feature_flags:0:conditions:client_filters:2:name"]);
+ Assert.Equal("TimeWindow", config["feature_management:feature_flags:0:conditions:client_filters:3:name"]);
+ Assert.Equal("/Date(1578643200000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:Start"]);
+ Assert.Equal("/Date(1578686400000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:End"]);
featureFlags[0] = ConfigurationModelFactory.ConfigurationSetting(
key: FeatureManagementConstants.FeatureFlagMarker + "myFeature",
@@ -436,10 +724,12 @@ public void WatchesFeatureFlags()
Thread.Sleep(RefreshInterval);
refresher.RefreshAsync().Wait();
- 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"]);
+ Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("Chrome", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Edge", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
+ Assert.Equal("SuperUsers", config["feature_management:feature_flags:1:conditions:client_filters:0:name"]);
}
[Fact]
@@ -466,16 +756,18 @@ public void WatchesFeatureFlagsUsingCacheExpirationInterval()
})
.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"]);
+ Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("Firefox", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Safari", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
+ Assert.Equal("RollOut", config["feature_management:feature_flags:0:conditions:client_filters:1:name"]);
+ Assert.Equal("20", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Percentage"]);
+ Assert.Equal("US", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Region"]);
+ Assert.Equal("SuperUsers", config["feature_management:feature_flags:0:conditions:client_filters:2:name"]);
+ Assert.Equal("TimeWindow", config["feature_management:feature_flags:0:conditions:client_filters:3:name"]);
+ Assert.Equal("/Date(1578643200000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:Start"]);
+ Assert.Equal("/Date(1578686400000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:End"]);
featureFlags[0] = ConfigurationModelFactory.ConfigurationSetting(
key: FeatureManagementConstants.FeatureFlagMarker + "myFeature",
@@ -507,10 +799,12 @@ public void WatchesFeatureFlagsUsingCacheExpirationInterval()
Thread.Sleep(cacheExpirationInterval);
refresher.RefreshAsync().Wait();
- 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"]);
+ Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("Chrome", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Edge", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
+ Assert.Equal("SuperUsers", config["feature_management:feature_flags:1:conditions:client_filters:0:name"]);
}
[Fact]
@@ -535,16 +829,18 @@ public void SkipRefreshIfRefreshIntervalHasNotElapsed()
})
.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"]);
+ Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("Firefox", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Safari", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
+ Assert.Equal("RollOut", config["feature_management:feature_flags:0:conditions:client_filters:1:name"]);
+ Assert.Equal("20", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Percentage"]);
+ Assert.Equal("US", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Region"]);
+ Assert.Equal("SuperUsers", config["feature_management:feature_flags:0:conditions:client_filters:2:name"]);
+ Assert.Equal("TimeWindow", config["feature_management:feature_flags:0:conditions:client_filters:3:name"]);
+ Assert.Equal("/Date(1578643200000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:Start"]);
+ Assert.Equal("/Date(1578686400000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:End"]);
featureFlags[0] = ConfigurationModelFactory.ConfigurationSetting(
key: FeatureManagementConstants.FeatureFlagMarker + "myFeature",
@@ -574,10 +870,12 @@ public void SkipRefreshIfRefreshIntervalHasNotElapsed()
refresher.RefreshAsync().Wait();
- 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"]);
+ Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("Firefox", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Safari", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
+ Assert.Null(config["feature_management:feature_flags:1:conditions:client_filters:0:name"]);
}
[Fact]
@@ -602,16 +900,18 @@ public void SkipRefreshIfCacheExpirationIntervalHasNotElapsed()
})
.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"]);
+ Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("Firefox", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Safari", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
+ Assert.Equal("RollOut", config["feature_management:feature_flags:0:conditions:client_filters:1:name"]);
+ Assert.Equal("20", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Percentage"]);
+ Assert.Equal("US", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Region"]);
+ Assert.Equal("SuperUsers", config["feature_management:feature_flags:0:conditions:client_filters:2:name"]);
+ Assert.Equal("TimeWindow", config["feature_management:feature_flags:0:conditions:client_filters:3:name"]);
+ Assert.Equal("/Date(1578643200000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:Start"]);
+ Assert.Equal("/Date(1578686400000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:End"]);
featureFlags[0] = ConfigurationModelFactory.ConfigurationSetting(
key: FeatureManagementConstants.FeatureFlagMarker + "myFeature",
@@ -641,10 +941,12 @@ public void SkipRefreshIfCacheExpirationIntervalHasNotElapsed()
refresher.RefreshAsync().Wait();
- 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"]);
+ Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("Firefox", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Safari", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
+ Assert.Null(config["feature_management:feature_flags:1:conditions:client_filters:0:name"]);
}
[Fact]
@@ -753,15 +1055,120 @@ public void SelectFeatureFlags()
})
.Build();
- Assert.Equal("AlwaysOn", config["FeatureManagement:App1_Feature1:EnabledFor:0:Name"]);
- Assert.Equal("Disabled", config["FeatureManagement:App1_Feature2:Status"]);
+ Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
// Verify that the feature flag that did not start with the specified prefix was not loaded
- Assert.Null(config["FeatureManagement:Feature1"]);
+ Assert.Null(config["feature_management:feature_flags:2"]);
// Verify that the feature flag that did not match the specified label was not loaded
- Assert.Null(config["FeatureManagement:App2_Feature1"]);
- Assert.Null(config["FeatureManagement:App2_Feature2"]);
+ Assert.Null(config["feature_management:feature_flags:3"]);
+ Assert.Null(config["feature_management:feature_flags:4"]);
+ }
+
+ [Fact]
+ public void TestNullAndMissingValuesForConditions()
+ {
+ 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(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.CacheExpirationInterval = cacheExpiration;
+ ff.Select(KeyFilter.Any);
+ });
+ })
+ .Build();
+
+ Assert.Equal("Filter", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Null(config["feature_management:feature_flags:0:conditions:client_filters:0:parameters"]);
+ Assert.Null(config["feature_management:feature_flags:1:conditions"]);
+ Assert.Null(config["feature_management:feature_flags:2:conditions"]);
+ Assert.Null(config["feature_management:feature_flags:3:conditions"]);
+ Assert.Null(config["feature_management:feature_flags:4:conditions"]);
+ Assert.Null(config["feature_management:feature_flags:5:conditions"]);
+ }
+
+ [Fact]
+ public void InvalidFeatureFlagFormatsThrowFormatException()
+ {
+ 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 = _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.CacheExpirationInterval = cacheExpiration;
+ 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(new MockAsyncPageable(_validFormatFeatureFlagCollection));
+
+ var testClient = mockClient.Object;
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(testClient);
+ options.UseFeatureFlags(ff =>
+ {
+ ff.CacheExpirationInterval = cacheExpiration;
+ ff.Select(KeyFilter.Any);
+ });
+ })
+ .Build();
+
+ // None of the feature flags should throw an exception, and the flag should be loaded like normal
+ Assert.Equal("True", config[$"feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("True", config[$"feature_management:feature_flags:1:enabled"]);
+ Assert.Equal("True", config[$"feature_management:feature_flags:2:enabled"]);
}
[Fact]
@@ -796,13 +1203,17 @@ 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["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("App1_Feature1", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
+ Assert.Equal("App1_Feature2", config["feature_management:feature_flags:1:id"]);
+ Assert.Equal("False", config["feature_management:feature_flags:2:enabled"]);
+ Assert.Equal("App2_Feature1", config["feature_management:feature_flags:2:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:3:enabled"]);
+ Assert.Equal("App2_Feature2", config["feature_management:feature_flags:3:id"]);
// Verify that the feature flag that did not start with the specified prefix was not loaded
- Assert.Null(config["FeatureManagement:Feature1"]);
+ Assert.Null(config["feature_management:feature_flags:4"]);
}
[Fact]
@@ -837,7 +1248,8 @@ public void KeepSelectorPrecedenceAfterDedup()
})
.Build();
// label: App1_Label has higher precedence
- Assert.Equal("AlwaysOn", config["FeatureManagement:Feature1:EnabledFor:0:Name"]);
+ Assert.Equal("Feature1", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
}
[Fact]
@@ -888,7 +1300,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());
});
@@ -909,13 +1321,17 @@ 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"]);
-
- // Verify that the feature flag that did not start with the specified prefix was not loaded
- Assert.Null(config["FeatureManagement:Feature1"]);
+ Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("App1_Feature1", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
+ Assert.Equal("App1_Feature2", config["feature_management:feature_flags:1:id"]);
+ Assert.Equal("False", config["feature_management:feature_flags:2:enabled"]);
+ Assert.Equal("App2_Feature1", config["feature_management:feature_flags:2:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:3:enabled"]);
+ Assert.Equal("App2_Feature2", config["feature_management:feature_flags:3:id"]);
+
+ // Verify that the feature flag Feature1 did not start with the specified prefix was not loaded
+ Assert.Null(config["feature_management:feature_flags:4"]);
}
[Fact]
@@ -953,13 +1369,18 @@ 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["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("App1_Feature1", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
+ Assert.Equal("App1_Feature2", config["feature_management:feature_flags:1:id"]);
// 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["feature_management:feature_flags:2:enabled"]);
+ Assert.Equal("App2_Feature1", config["feature_management:feature_flags:2:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:3:enabled"]);
+ Assert.Equal("App2_Feature2", config["feature_management:feature_flags:3:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:4:enabled"]);
+ Assert.Equal("Feature1", config["feature_management:feature_flags:4:id"]);
}
[Fact]
@@ -1003,10 +1424,14 @@ public void DifferentRefreshIntervalsForMultipleFeatureFlagRegistrations()
})
.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["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("App1_Feature1", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
+ Assert.Equal("App1_Feature2", config["feature_management:feature_flags:1:id"]);
+ Assert.Equal("False", config["feature_management:feature_flags:2:enabled"]);
+ Assert.Equal("App2_Feature1", config["feature_management:feature_flags:2:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:3:enabled"]);
+ Assert.Equal("App2_Feature2", config["feature_management:feature_flags:3:id"]);
// update the value of App1_Feature1 feature flag with label1
featureFlagCollection[0] = ConfigurationModelFactory.ConfigurationSetting(
@@ -1051,15 +1476,18 @@ public void DifferentRefreshIntervalsForMultipleFeatureFlagRegistrations()
Thread.Sleep(refreshInterval1);
refresher.RefreshAsync().Wait();
- 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("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("Chrome", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Edge", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
+ Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
+ Assert.Equal("App1_Feature2", config["feature_management:feature_flags:1:id"]);
+ Assert.Equal("False", config["feature_management:feature_flags:2:enabled"]);
+ Assert.Equal("App2_Feature1", config["feature_management:feature_flags:2:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:3:enabled"]);
+ Assert.Equal("App2_Feature2", config["feature_management:feature_flags:3:id"]);
// even though App2_Feature3 feature flag has been added, its value should not be loaded in config because label2 refresh interval has not elapsed
- Assert.Null(config["FeatureManagement:App2_Feature3"]);
+ Assert.Null(config["feature_management:feature_flags:4"]);
}
[Fact]
@@ -1096,11 +1524,16 @@ public void OverwrittenRefreshIntervalForSameFeatureFlagRegistrations()
})
.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["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("App1_Feature1", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
+ Assert.Equal("App1_Feature2", config["feature_management:feature_flags:1:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:2:enabled"]);
+ Assert.Equal("Feature1", config["feature_management:feature_flags:2:id"]);
+ Assert.Equal("False", config["feature_management:feature_flags:3:enabled"]);
+ Assert.Equal("App2_Feature1", config["feature_management:feature_flags:3:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:4:enabled"]);
+ Assert.Equal("App2_Feature2", config["feature_management:feature_flags:4:id"]);
// update the value of App1_Feature1 feature flag with label1
featureFlagCollection[0] = ConfigurationModelFactory.ConfigurationSetting(
@@ -1130,11 +1563,16 @@ public void OverwrittenRefreshIntervalForSameFeatureFlagRegistrations()
// 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("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["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("App1_Feature1", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
+ Assert.Equal("App1_Feature2", config["feature_management:feature_flags:1:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:2:enabled"]);
+ Assert.Equal("Feature1", config["feature_management:feature_flags:2:id"]);
+ Assert.Equal("False", config["feature_management:feature_flags:3:enabled"]);
+ Assert.Equal("App2_Feature1", config["feature_management:feature_flags:3:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:4:enabled"]);
+ Assert.Equal("App2_Feature2", config["feature_management:feature_flags:4:id"]);
}
[Fact]
@@ -1168,7 +1606,8 @@ public void SelectAndRefreshSingleFeatureFlag()
})
.Build();
- Assert.Equal("Disabled", config["FeatureManagement:Feature1:Status"]);
+ Assert.Equal("False", config["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("Feature1", config["feature_management:feature_flags:0:id"]);
// update the value of Feature1 feature flag with App1_Label
featureFlagCollection[2] = ConfigurationModelFactory.ConfigurationSetting(
@@ -1197,9 +1636,9 @@ public void SelectAndRefreshSingleFeatureFlag()
Thread.Sleep(RefreshInterval);
refresher.RefreshAsync().Wait();
- Assert.Equal("Browser", config["FeatureManagement:Feature1:EnabledFor:0:Name"]);
- Assert.Equal("Chrome", config["FeatureManagement:Feature1:EnabledFor:0:Parameters:AllowedBrowsers:0"]);
- Assert.Equal("Edge", config["FeatureManagement:Feature1:EnabledFor:0:Parameters:AllowedBrowsers:1"]);
+ Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("Chrome", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Edge", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
}
[Fact]
@@ -1239,7 +1678,8 @@ public void ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefresh()
})
.Build();
- Assert.Equal("SuperUsers", config["FeatureManagement:MyFeature2:EnabledFor:0:Name"]);
+ Assert.Equal("SuperUsers", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("MyFeature2", config["feature_management:feature_flags:0:id"]);
featureFlags[0] = ConfigurationModelFactory.ConfigurationSetting(
key: FeatureManagementConstants.FeatureFlagMarker + "myFeature1",
@@ -1264,7 +1704,8 @@ public void ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefresh()
Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
- Assert.Equal("AllUsers", config["FeatureManagement:MyFeature:EnabledFor:0:Name"]);
+ Assert.Equal("AllUsers", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("MyFeature", config["feature_management:feature_flags:0:id"]);
Assert.Contains(LogHelper.BuildFeatureFlagReadMessage("myFeature1", null, TestHelpers.PrimaryConfigStoreEndpoint.ToString().TrimEnd('/')), verboseInvocation);
Assert.Contains(LogHelper.BuildFeatureFlagUpdatedMessage("myFeature1"), informationalInvocation);
@@ -1272,7 +1713,7 @@ public void ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefresh()
Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
- Assert.Null(config["FeatureManagement:MyFeature:EnabledFor:0:Name"]);
+ Assert.Null(config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
Assert.Contains(LogHelper.BuildFeatureFlagReadMessage("myFeature1", null, TestHelpers.PrimaryConfigStoreEndpoint.ToString().TrimEnd('/')), verboseInvocation);
Assert.Contains(LogHelper.BuildFeatureFlagUpdatedMessage("myFeature1"), informationalInvocation);
}
@@ -1320,12 +1761,12 @@ public void ValidateFeatureFlagsUnchangedLogged()
})
.Build();
- Assert.Equal("SuperUsers", config["FeatureManagement:MyFeature2:EnabledFor:0:Name"]);
+ Assert.Equal("SuperUsers", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
FirstKeyValue.Value = "newValue1";
Thread.Sleep(RefreshInterval);
refresher.TryRefreshAsync().Wait();
- Assert.Equal("SuperUsers", config["FeatureManagement:MyFeature2:EnabledFor:0:Name"]);
+ Assert.Equal("SuperUsers", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
Assert.Contains(LogHelper.BuildFeatureFlagsUnchangedMessage(TestHelpers.PrimaryConfigStoreEndpoint.ToString().TrimEnd('/')), verboseInvocation);
}
@@ -1411,7 +1852,8 @@ public void MapTransformFeatureFlagWithRefresh()
.Build();
Assert.Equal("TestValue1", config["TestKey1"]);
- Assert.Equal("NoUsers", config["FeatureManagement:MyFeature:EnabledFor:0:Name"]);
+ Assert.Equal("NoUsers", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("MyFeature", config["feature_management:feature_flags:0:id"]);
FirstKeyValue.Value = "newValue1";
featureFlags[0] = ConfigurationModelFactory.ConfigurationSetting(
@@ -1439,23 +1881,17 @@ public void MapTransformFeatureFlagWithRefresh()
refresher.TryRefreshAsync().Wait();
Assert.Equal("newValue1", config["TestKey1"]);
- Assert.Equal("NoUsers", config["FeatureManagement:MyFeature:EnabledFor:0:Name"]);
+ Assert.Equal("NoUsers", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
}
[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 =>
@@ -1465,93 +1901,82 @@ 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("VariantsFeature4", config["feature_management:feature_flags:3:id"]);
+ Assert.Equal("True", config["feature_management:feature_flags:3:enabled"]);
+ Assert.Null(config["feature_management:feature_flags:3:variants"]);
+ Assert.Null(config["feature_management:feature_flags:3:allocation"]);
}
[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 =>
@@ -1562,16 +1987,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)
@@ -1579,8 +2005,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"]);
}
@@ -1602,9 +2032,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();
@@ -1621,10 +2051,62 @@ 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("MyFeature2", config["feature_management:feature_flags:0:id"]);
+ Assert.Null(config["feature_management:feature_flags:1:requirement_type"]);
+ Assert.Equal("Feature_NoFilters", config["feature_management:feature_flags:1:id"]);
+ Assert.Equal("All", config["feature_management:feature_flags:2:conditions:requirement_type"]);
+ Assert.Equal("Feature_RequireAll", config["feature_management:feature_flags:2:id"]);
+ Assert.Equal("Any", config["feature_management:feature_flags:3:conditions:requirement_type"]);
+ Assert.Equal("Feature_RequireAny", config["feature_management:feature_flags:3: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)
@@ -1637,18 +2119,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..c0fa1e85 100644
--- a/tests/Tests.AzureAppConfiguration/JsonContentTypeTests.cs
+++ b/tests/Tests.AzureAppConfiguration/JsonContentTypeTests.cs
@@ -221,9 +221,9 @@ public void JsonContentTypeTests_DontFlattenFeatureFlagAsJsonObject()
.AddAzureAppConfiguration(options => options.ClientManager = mockClientManager)
.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("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("Firefox", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Safari", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
}
[Fact]
diff --git a/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs b/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
index 288cb1fb..1b51971f 100644
--- a/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
+++ b/tests/Tests.AzureAppConfiguration/KeyVaultReferenceTests.cs
@@ -424,7 +424,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 =>
From 6a682ff799a0c25374e54b0d74632140b62c1891 Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Sat, 22 Jun 2024 02:44:26 +0800
Subject: [PATCH 12/21] fix xunit1031 and netsdk1210 (#561)
---
...ns.Configuration.AzureAppConfiguration.csproj | 2 +-
.../FeatureManagementTests.cs | 16 ++++++++--------
2 files changed, 9 insertions(+), 9 deletions(-)
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 466c58be..12b99a00 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
@@ -48,7 +48,7 @@
..\..\AzureAppConfigurationRules.ruleset
true
- true
+ true
diff --git a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
index 809187fd..abcb7595 100644
--- a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
+++ b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
@@ -618,7 +618,7 @@ public void PreservesDefaultQuery()
}
[Fact]
- public async Task QueriesFeatureFlags()
+ public void QueriesFeatureFlags()
{
var mockTransport = new MockTransport(req =>
{
@@ -1266,7 +1266,7 @@ public async Task SelectAndRefreshSingleFeatureFlag()
}
[Fact]
- public void ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefresh()
+ public async Task ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefresh()
{
IConfigurationRefresher refresher = null;
var featureFlags = new List { _kv2 };
@@ -1326,14 +1326,14 @@ public void ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefresh()
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1" + "f"));
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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();
+ await refresher.TryRefreshAsync();
Assert.Null(config["FeatureManagement:MyFeature:EnabledFor:0:Name"]);
Assert.Contains(LogHelper.BuildFeatureFlagReadMessage("myFeature1", null, TestHelpers.PrimaryConfigStoreEndpoint.ToString().TrimEnd('/')), verboseInvocation);
@@ -1341,7 +1341,7 @@ public void ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefresh()
}
[Fact]
- public void ValidateFeatureFlagsUnchangedLogged()
+ public async Task ValidateFeatureFlagsUnchangedLogged()
{
IConfigurationRefresher refresher = null;
var featureFlags = new List { _kv2 };
@@ -1387,13 +1387,13 @@ public void ValidateFeatureFlagsUnchangedLogged()
FirstKeyValue.Value = "newValue1";
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ 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",
@@ -1499,7 +1499,7 @@ public void MapTransformFeatureFlagWithRefresh()
eTag: new ETag("c3c231fd-39a0-4cb6-3237-4614474b92c1" + "f"));
Thread.Sleep(CacheExpirationTime);
- refresher.TryRefreshAsync().Wait();
+ await refresher.TryRefreshAsync();
Assert.Equal("newValue1", config["TestKey1"]);
Assert.Equal("NoUsers", config["FeatureManagement:MyFeature:EnabledFor:0:Name"]);
From 96a5e5a07b8d4b4aa55d31bcab14219ee6912a04 Mon Sep 17 00:00:00 2001
From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
Date: Fri, 21 Jun 2024 12:40:17 -0700
Subject: [PATCH 13/21] Add handling for FormatException thrown by invalid
feature flag json (#551)
* add log for formatexception from invalid feature flag json
* remove capitalized json
* remove unused using
* fix comments
* fix constant reference
---
.../AzureAppConfigurationProvider.cs | 7 +++++++
.../Constants/LoggingConstants.cs | 1 +
.../FeatureManagement/FeatureManagementKeyValueAdapter.cs | 2 +-
.../LogHelper.cs | 5 +++++
4 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
index 9cf9947b..182c819a 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
@@ -492,6 +492,12 @@ public async Task TryRefreshAsync(CancellationToken cancellationToken)
return false;
}
+ catch (FormatException fe)
+ {
+ _logger.LogWarning(LogHelper.BuildRefreshFailedDueToFormattingErrorMessage(fe.Message));
+
+ return false;
+ }
return true;
}
@@ -634,6 +640,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)))
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/FeatureManagement/FeatureManagementKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
index a4f64d6b..5b0a6a96 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
@@ -188,7 +188,7 @@ private FeatureFlag ParseFeatureFlag(string settingKey, string settingValue)
}
catch (JsonException e)
{
- throw new FormatException(settingKey, e);
+ throw new FormatException(string.Format(ErrorMessages.FeatureFlagInvalidFormat, settingKey), e);
}
return featureFlag;
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/LogHelper.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/LogHelper.cs
index 113a1499..e7429a9e 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/LogHelper.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/LogHelper.cs
@@ -89,5 +89,10 @@ public static string BuildFallbackClientLookupFailMessage(string exceptionMessag
{
return $"{LoggingConstants.FallbackClientLookupError}\n{exceptionMessage}";
}
+
+ public static string BuildRefreshFailedDueToFormattingErrorMessage(string exceptionMessage)
+ {
+ return $"{LoggingConstants.RefreshFailedDueToFormattingError}\n{exceptionMessage}";
+ }
}
}
From 88a83d6c310758b8c6acadbae65d755d5eeec069 Mon Sep 17 00:00:00 2001
From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
Date: Tue, 25 Jun 2024 12:43:03 -0700
Subject: [PATCH 14/21] Show full informational version for feature management
library (#558)
* get full informational version name
* use getcustomattribute
* comment revision
---
.../TracingUtils.cs | 25 +++++++++++++++++--
1 file changed, 23 insertions(+), 2 deletions(-)
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs
index 3ca5271f..8b225f2e 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;
From c3b5ac1551cd9944db114201b35285bb5268bd33 Mon Sep 17 00:00:00 2001
From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
Date: Fri, 28 Jun 2024 12:24:30 -0700
Subject: [PATCH 15/21] Remove support for .NET 7 (#567)
* remove net7 references and target frameworks
* update install script
---
build/install-dotnet.ps1 | 4 +---
.../Microsoft.Azure.AppConfiguration.AspNetCore.csproj | 2 +-
.../Microsoft.Azure.AppConfiguration.Functions.Worker.csproj | 2 +-
.../Tests.AzureAppConfiguration.AspNetCore.csproj | 2 +-
.../Tests.AzureAppConfiguration.Functions.Worker.csproj | 2 +-
tests/Tests.AzureAppConfiguration/RefreshTests.cs | 2 +-
.../Tests.AzureAppConfiguration.csproj | 2 +-
7 files changed, 7 insertions(+), 9 deletions(-)
diff --git a/build/install-dotnet.ps1 b/build/install-dotnet.ps1
index 194dbf5a..eeb52b70 100644
--- a/build/install-dotnet.ps1
+++ b/build/install-dotnet.ps1
@@ -1,10 +1,8 @@
-# Installs .NET 6, .NET 7, and .NET 8 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
-
&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://dot.net/v1/dotnet-install.ps1'))) -Channel 8.0
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 98b0a68a..f79bf688 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;net8.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
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 c0476b84..03b9df50 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;net8.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
diff --git a/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj b/tests/Tests.AzureAppConfiguration.AspNetCore/Tests.AzureAppConfiguration.AspNetCore.csproj
index a8cd3b51..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;net8.0
+ net6.0;net8.0
8.0
false
true
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 f5af5a3f..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;net8.0
+ net6.0;net8.0
8.0
false
true
diff --git a/tests/Tests.AzureAppConfiguration/RefreshTests.cs b/tests/Tests.AzureAppConfiguration/RefreshTests.cs
index 67a66265..1faae290 100644
--- a/tests/Tests.AzureAppConfiguration/RefreshTests.cs
+++ b/tests/Tests.AzureAppConfiguration/RefreshTests.cs
@@ -1056,7 +1056,7 @@ public void RefreshTests_RefreshIsCancelled()
Assert.Equal("TestValue1", config["TestKey1"]);
}
-#if NET7_0
+#if NET8_0
[Fact]
public void RefreshTests_ChainedConfigurationProviderUsedAsRootForRefresherProvider()
{
diff --git a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
index 9b0f17c0..e628ecca 100644
--- a/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
+++ b/tests/Tests.AzureAppConfiguration/Tests.AzureAppConfiguration.csproj
@@ -1,7 +1,7 @@
- net48;net6.0;net7.0;net8.0
+ net48;net6.0;net8.0
8.0
false
true
From 8a0075035c47953bb29cd857ec2a9262f453a992 Mon Sep 17 00:00:00 2001
From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
Date: Wed, 10 Jul 2024 10:41:47 -0700
Subject: [PATCH 16/21] Support both .NET and microsoft schema for feature
flags (#566)
* adding new private methods for dotnet versus microsoft schema, fixing tests
* remove unused method
* remove schema warning, PR comments
* unify process feature flag approaches with id, fix schema check
* fix tests to match changes
* check for empty variants when deciding schema
---
.../AzureAppConfigurationProvider.cs | 12 +-
.../Constants/LoggingConstants.cs | 3 -
.../FeatureManagementConstants.cs | 5 +
.../FeatureManagementKeyValueAdapter.cs | 133 +++++--
.../LogHelper.cs | 6 -
.../FeatureManagementTests.cs | 375 ++++++++----------
.../JsonContentTypeTests.cs | 6 +-
7 files changed, 273 insertions(+), 267 deletions(-)
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
index 354a450f..a8755ce8 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
@@ -1218,21 +1218,11 @@ private void EnsureFeatureManagementVersionInspected()
{
if (!_isFeatureManagementVersionInspected)
{
- const string FeatureManagementMinimumVersion = "3.2.0";
-
_isFeatureManagementVersionInspected = true;
if (_requestTracingEnabled && _requestTracingOptions != null)
{
- string featureManagementVersion = TracingUtils.GetAssemblyVersion(RequestTracingConstants.FeatureManagementAssemblyName);
-
- // If the version is less than 3.2.0, log the schema version warning
- if (featureManagementVersion != null && Version.Parse(featureManagementVersion) < Version.Parse(FeatureManagementMinimumVersion))
- {
- _logger.LogWarning(LogHelper.BuildFeatureManagementMicrosoftSchemaVersionWarningMessage());
- }
-
- _requestTracingOptions.FeatureManagementVersion = featureManagementVersion;
+ _requestTracingOptions.FeatureManagementVersion = TracingUtils.GetAssemblyVersion(RequestTracingConstants.FeatureManagementAssemblyName);
_requestTracingOptions.FeatureManagementAspNetCoreVersion = TracingUtils.GetAssemblyVersion(RequestTracingConstants.FeatureManagementAspNetCoreAssemblyName);
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/LoggingConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/LoggingConstants.cs
index e202d79b..86576a48 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/LoggingConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/LoggingConstants.cs
@@ -33,8 +33,5 @@ internal class LoggingConstants
public const string RefreshSkippedNoClientAvailable = "Refresh skipped because no endpoint is accessible.";
public const string RefreshFailedToGetSettingsFromEndpoint = "Failed to get configuration settings from endpoint";
public const string FailingOverToEndpoint = "Failing over to endpoint";
- public const string FeatureManagementMicrosoftSchemaVersionWarning = "Your application may be using an older version of " +
- "Microsoft.FeatureManagement library that isn't compatible with Microsoft.Extensions.Configuration.AzureAppConfiguration. Please update " +
- "the Microsoft.FeatureManagement package to version 3.2.0 or later.";
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
index 9568a2cb..c6d86d84 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs
@@ -44,5 +44,10 @@ internal class FeatureManagementConstants
public const string ETag = "ETag";
public const string FeatureFlagId = "FeatureFlagId";
public const string FeatureFlagReference = "FeatureFlagReference";
+
+ // 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 04f9841d..b7639bb8 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs
@@ -30,9 +30,104 @@ public Task>> ProcessKeyValue(Configura
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)
+ {
+ keyValues = ProcessMicrosoftSchemaFeatureFlag(featureFlag, setting, endpoint);
+ }
+ else
+ {
+ 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>();
+
+ if (string.IsNullOrEmpty(featureFlag.Id))
+ {
+ return keyValues;
+ }
+
+ string featureFlagPath = $"{FeatureManagementConstants.DotnetSchemaSectionName}:{featureFlag.Id}";
+
+ if (featureFlag.Enabled)
+ {
+ if (featureFlag.Conditions?.ClientFilters == null || !featureFlag.Conditions.ClientFilters.Any())
+ {
+ keyValues.Add(new KeyValuePair(featureFlagPath, true.ToString()));
+ }
+ else
+ {
+ for (int i = 0; i < featureFlag.Conditions.ClientFilters.Count; i++)
+ {
+ ClientFilter clientFilter = featureFlag.Conditions.ClientFilters[i];
+
+ _featureFilterTracing.UpdateFeatureFilterTracing(clientFilter.Name);
+
+ string clientFiltersPath = $"{featureFlagPath}:{FeatureManagementConstants.DotnetSchemaEnabledFor}:{i}";
+
+ keyValues.Add(new KeyValuePair($"{clientFiltersPath}:Name", clientFilter.Name));
+
+ foreach (KeyValuePair kvp in new JsonFlattener().FlattenJson(clientFilter.Parameters))
+ {
+ keyValues.Add(new KeyValuePair($"{clientFiltersPath}:Parameters:{kvp.Key}", kvp.Value));
+ }
+ }
+
+ //
+ // process RequirementType only when filters are not empty
+ if (featureFlag.Conditions.RequirementType != null)
+ {
+ keyValues.Add(new KeyValuePair(
+ $"{featureFlagPath}:{FeatureManagementConstants.DotnetSchemaRequirementType}",
+ featureFlag.Conditions.RequirementType));
+ }
+ }
+ }
+ else
+ {
+ 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 Task.FromResult>>(keyValues);
+ return keyValues;
}
string featureFlagPath = $"{FeatureManagementConstants.FeatureManagementSectionName}:{FeatureManagementConstants.FeatureFlagsSectionName}:{_featureFlagIndex}";
@@ -53,7 +148,7 @@ public Task>> ProcessKeyValue(Configura
{
ClientFilter clientFilter = featureFlag.Conditions.ClientFilters[i];
- _featureFilterTracing.UpdateFeatureFilterTracing(clientFilter.Name);
+ _featureFilterTracing.UpdateFeatureFilterTracing(clientFilter.Name);
string clientFiltersPath = $"{featureFlagPath}:{FeatureManagementConstants.Conditions}:{FeatureManagementConstants.ClientFilters}:{i}";
@@ -70,7 +165,7 @@ public Task>> ProcessKeyValue(Configura
if (featureFlag.Conditions.RequirementType != null)
{
keyValues.Add(new KeyValuePair(
- $"{featureFlagPath}:{FeatureManagementConstants.Conditions}:{FeatureManagementConstants.RequirementType}",
+ $"{featureFlagPath}:{FeatureManagementConstants.Conditions}:{FeatureManagementConstants.RequirementType}",
featureFlag.Conditions.RequirementType));
}
}
@@ -219,37 +314,7 @@ public Task>> ProcessKeyValue(Configura
}
}
- 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 void InvalidateCache(ConfigurationSetting setting = null)
- {
- return;
- }
-
- public bool NeedsRefresh()
- {
- return false;
- }
-
- public void OnChangeDetected(ConfigurationSetting setting = null)
- {
- return;
- }
-
- public void OnConfigUpdated()
- {
- _featureFlagIndex = 0;
-
- return;
+ return keyValues;
}
private FormatException CreateFeatureFlagFormatException(string jsonPropertyName, string settingKey, string foundJsonValueKind, string expectedJsonValueKind)
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/LogHelper.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/LogHelper.cs
index acb93fb9..11442dcb 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/LogHelper.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/LogHelper.cs
@@ -88,13 +88,7 @@ public static string BuildLastEndpointFailedMessage(string endpoint)
public static string BuildFallbackClientLookupFailMessage(string exceptionMessage)
{
return $"{LoggingConstants.FallbackClientLookupError}\n{exceptionMessage}";
- }
-
- public static string BuildFeatureManagementMicrosoftSchemaVersionWarningMessage()
- {
- return LoggingConstants.FeatureManagementMicrosoftSchemaVersionWarning;
}
-
public static string BuildRefreshFailedDueToFormattingErrorMessage(string exceptionMessage)
{
return $"{LoggingConstants.RefreshFailedDueToFormattingError}\n{exceptionMessage}";
diff --git a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
index 12a18a97..8e871ef2 100644
--- a/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
+++ b/tests/Tests.AzureAppConfiguration/FeatureManagementTests.cs
@@ -645,18 +645,16 @@ public void UsesFeatureFlags()
})
.Build();
- Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
- Assert.Equal("Firefox", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
- Assert.Equal("Safari", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
- Assert.Equal("RollOut", config["feature_management:feature_flags:0:conditions:client_filters:1:name"]);
- Assert.Equal("20", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Percentage"]);
- Assert.Equal("US", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Region"]);
- Assert.Equal("SuperUsers", config["feature_management:feature_flags:0:conditions:client_filters:2:name"]);
- Assert.Equal("TimeWindow", config["feature_management:feature_flags:0:conditions:client_filters:3:name"]);
- Assert.Equal("/Date(1578643200000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:Start"]);
- Assert.Equal("/Date(1578686400000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:End"]);
+ 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"]);
}
[Fact]
@@ -681,18 +679,16 @@ public async Task WatchesFeatureFlags()
})
.Build();
- Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
- Assert.Equal("Firefox", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
- Assert.Equal("Safari", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
- Assert.Equal("RollOut", config["feature_management:feature_flags:0:conditions:client_filters:1:name"]);
- Assert.Equal("20", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Percentage"]);
- Assert.Equal("US", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Region"]);
- Assert.Equal("SuperUsers", config["feature_management:feature_flags:0:conditions:client_filters:2:name"]);
- Assert.Equal("TimeWindow", config["feature_management:feature_flags:0:conditions:client_filters:3:name"]);
- Assert.Equal("/Date(1578643200000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:Start"]);
- Assert.Equal("/Date(1578686400000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:End"]);
+ 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",
@@ -724,12 +720,10 @@ public async Task WatchesFeatureFlags()
Thread.Sleep(RefreshInterval);
await refresher.RefreshAsync();
- Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
- Assert.Equal("Chrome", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
- Assert.Equal("Edge", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
- Assert.Equal("SuperUsers", config["feature_management:feature_flags:1:conditions:client_filters:0:name"]);
+ 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]
@@ -756,18 +750,16 @@ public async Task WatchesFeatureFlagsUsingCacheExpirationInterval()
})
.Build();
- Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
- Assert.Equal("Firefox", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
- Assert.Equal("Safari", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
- Assert.Equal("RollOut", config["feature_management:feature_flags:0:conditions:client_filters:1:name"]);
- Assert.Equal("20", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Percentage"]);
- Assert.Equal("US", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Region"]);
- Assert.Equal("SuperUsers", config["feature_management:feature_flags:0:conditions:client_filters:2:name"]);
- Assert.Equal("TimeWindow", config["feature_management:feature_flags:0:conditions:client_filters:3:name"]);
- Assert.Equal("/Date(1578643200000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:Start"]);
- Assert.Equal("/Date(1578686400000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:End"]);
+ 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",
@@ -799,12 +791,10 @@ public async Task WatchesFeatureFlagsUsingCacheExpirationInterval()
Thread.Sleep(cacheExpirationInterval);
await refresher.RefreshAsync();
- Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
- Assert.Equal("Chrome", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
- Assert.Equal("Edge", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
- Assert.Equal("SuperUsers", config["feature_management:feature_flags:1:conditions:client_filters:0:name"]);
+ 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]
@@ -829,18 +819,16 @@ public async Task SkipRefreshIfRefreshIntervalHasNotElapsed()
})
.Build();
- Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
- Assert.Equal("Firefox", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
- Assert.Equal("Safari", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
- Assert.Equal("RollOut", config["feature_management:feature_flags:0:conditions:client_filters:1:name"]);
- Assert.Equal("20", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Percentage"]);
- Assert.Equal("US", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Region"]);
- Assert.Equal("SuperUsers", config["feature_management:feature_flags:0:conditions:client_filters:2:name"]);
- Assert.Equal("TimeWindow", config["feature_management:feature_flags:0:conditions:client_filters:3:name"]);
- Assert.Equal("/Date(1578643200000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:Start"]);
- Assert.Equal("/Date(1578686400000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:End"]);
+ 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",
@@ -870,12 +858,10 @@ public async Task SkipRefreshIfRefreshIntervalHasNotElapsed()
await refresher.RefreshAsync();
- Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
- Assert.Equal("Firefox", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
- Assert.Equal("Safari", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
- Assert.Null(config["feature_management:feature_flags:1:conditions:client_filters:0:name"]);
+ 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]
@@ -900,18 +886,16 @@ public async Task SkipRefreshIfCacheNotExpired()
})
.Build();
- Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
- Assert.Equal("Firefox", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
- Assert.Equal("Safari", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
- Assert.Equal("RollOut", config["feature_management:feature_flags:0:conditions:client_filters:1:name"]);
- Assert.Equal("20", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Percentage"]);
- Assert.Equal("US", config["feature_management:feature_flags:0:conditions:client_filters:1:parameters:Region"]);
- Assert.Equal("SuperUsers", config["feature_management:feature_flags:0:conditions:client_filters:2:name"]);
- Assert.Equal("TimeWindow", config["feature_management:feature_flags:0:conditions:client_filters:3:name"]);
- Assert.Equal("/Date(1578643200000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:Start"]);
- Assert.Equal("/Date(1578686400000)/", config["feature_management:feature_flags:0:conditions:client_filters:3:parameters:End"]);
+ 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",
@@ -941,12 +925,10 @@ public async Task SkipRefreshIfCacheNotExpired()
await refresher.RefreshAsync();
- Assert.Equal("Beta", config["feature_management:feature_flags:0:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
- Assert.Equal("Firefox", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
- Assert.Equal("Safari", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
- Assert.Null(config["feature_management:feature_flags:1:conditions:client_filters:0:name"]);
+ 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]
@@ -1055,15 +1037,15 @@ public void SelectFeatureFlags()
})
.Build();
- Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
+ 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["feature_management:feature_flags:2"]);
+ Assert.Null(config["FeatureManagement:Feature1"]);
// Verify that the feature flag that did not match the specified label was not loaded
- Assert.Null(config["feature_management:feature_flags:3"]);
- Assert.Null(config["feature_management:feature_flags:4"]);
+ Assert.Null(config["FeatureManagement:App2_Feature1"]);
+ Assert.Null(config["FeatureManagement:App2_Feature2"]);
}
[Fact]
@@ -1091,13 +1073,13 @@ public void TestNullAndMissingValuesForConditions()
})
.Build();
- Assert.Equal("Filter", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
- Assert.Null(config["feature_management:feature_flags:0:conditions:client_filters:0:parameters"]);
- Assert.Null(config["feature_management:feature_flags:1:conditions"]);
- Assert.Null(config["feature_management:feature_flags:2:conditions"]);
- Assert.Null(config["feature_management:feature_flags:3:conditions"]);
- Assert.Null(config["feature_management:feature_flags:4:conditions"]);
- Assert.Null(config["feature_management:feature_flags:5:conditions"]);
+ 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]
@@ -1146,29 +1128,42 @@ public void AlternateValidFeatureFlagFormats()
{
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
- var refreshInterval = TimeSpan.FromSeconds(1);
+ var cacheExpiration = TimeSpan.FromSeconds(1);
mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
- .Returns(new MockAsyncPageable(_validFormatFeatureFlagCollection));
+ .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;
- var config = new ConfigurationBuilder()
- .AddAzureAppConfiguration(options =>
+ foreach (ConfigurationSetting setting in _validFormatFeatureFlagCollection)
{
- options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(testClient);
- options.UseFeatureFlags(ff =>
+ string flagKey = setting.Key.Substring(FeatureManagementConstants.FeatureFlagMarker.Length);
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
{
- ff.SetRefreshInterval(refreshInterval);
- ff.Select(KeyFilter.Any);
- });
- })
- .Build();
+ 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[$"feature_management:feature_flags:0:enabled"]);
- Assert.Equal("True", config[$"feature_management:feature_flags:1:enabled"]);
- Assert.Equal("True", config[$"feature_management:feature_flags:2:enabled"]);
+ // None of the feature flags should throw an exception, and the flag should be loaded like normal
+ Assert.Equal("True", config[$"FeatureManagement:{flagKey}"]);
+ }
}
[Fact]
@@ -1203,17 +1198,13 @@ public void MultipleSelectsInSameUseFeatureFlags()
})
.Build();
- Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("App1_Feature1", config["feature_management:feature_flags:0:id"]);
- Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
- Assert.Equal("App1_Feature2", config["feature_management:feature_flags:1:id"]);
- Assert.Equal("False", config["feature_management:feature_flags:2:enabled"]);
- Assert.Equal("App2_Feature1", config["feature_management:feature_flags:2:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:3:enabled"]);
- Assert.Equal("App2_Feature2", config["feature_management:feature_flags:3:id"]);
+ 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["feature_management:feature_flags:4"]);
+ Assert.Null(config["FeatureManagement:Feature1"]);
}
[Fact]
@@ -1248,8 +1239,7 @@ public void KeepSelectorPrecedenceAfterDedup()
})
.Build();
// label: App1_Label has higher precedence
- Assert.Equal("Feature1", config["feature_management:feature_flags:0:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
+ Assert.Equal("True", config["FeatureManagement:Feature1"]);
}
[Fact]
@@ -1321,17 +1311,13 @@ public void MultipleCallsToUseFeatureFlags()
})
.Build();
- Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("App1_Feature1", config["feature_management:feature_flags:0:id"]);
- Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
- Assert.Equal("App1_Feature2", config["feature_management:feature_flags:1:id"]);
- Assert.Equal("False", config["feature_management:feature_flags:2:enabled"]);
- Assert.Equal("App2_Feature1", config["feature_management:feature_flags:2:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:3:enabled"]);
- Assert.Equal("App2_Feature2", config["feature_management:feature_flags:3:id"]);
-
- // Verify that the feature flag Feature1 did not start with the specified prefix was not loaded
- Assert.Null(config["feature_management:feature_flags:4"]);
+ 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"]);
}
[Fact]
@@ -1369,18 +1355,13 @@ public void MultipleCallsToUseFeatureFlagsWithSelectAndLabel()
.Build();
// Loaded from prefix1 and label1
- Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("App1_Feature1", config["feature_management:feature_flags:0:id"]);
- Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
- Assert.Equal("App1_Feature2", config["feature_management:feature_flags:1:id"]);
+ Assert.Equal("True", config["FeatureManagement:App1_Feature1"]);
+ Assert.Equal("False", config["FeatureManagement:App1_Feature2"]);
// Loaded from label2
- Assert.Equal("False", config["feature_management:feature_flags:2:enabled"]);
- Assert.Equal("App2_Feature1", config["feature_management:feature_flags:2:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:3:enabled"]);
- Assert.Equal("App2_Feature2", config["feature_management:feature_flags:3:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:4:enabled"]);
- Assert.Equal("Feature1", config["feature_management:feature_flags:4:id"]);
+ Assert.Equal("False", config["FeatureManagement:App2_Feature1"]);
+ Assert.Equal("True", config["FeatureManagement:App2_Feature2"]);
+ Assert.Equal("True", config["FeatureManagement:Feature1"]);
}
[Fact]
@@ -1424,14 +1405,10 @@ public async Task DifferentCacheExpirationsForMultipleFeatureFlagRegistrations()
})
.Build();
- Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("App1_Feature1", config["feature_management:feature_flags:0:id"]);
- Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
- Assert.Equal("App1_Feature2", config["feature_management:feature_flags:1:id"]);
- Assert.Equal("False", config["feature_management:feature_flags:2:enabled"]);
- Assert.Equal("App2_Feature1", config["feature_management:feature_flags:2:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:3:enabled"]);
- Assert.Equal("App2_Feature2", config["feature_management:feature_flags:3:id"]);
+ 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(
@@ -1476,22 +1453,19 @@ public async Task DifferentCacheExpirationsForMultipleFeatureFlagRegistrations()
Thread.Sleep(refreshInterval1);
await refresher.RefreshAsync();
- Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
- Assert.Equal("Chrome", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
- Assert.Equal("Edge", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
- Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
- Assert.Equal("App1_Feature2", config["feature_management:feature_flags:1:id"]);
- Assert.Equal("False", config["feature_management:feature_flags:2:enabled"]);
- Assert.Equal("App2_Feature1", config["feature_management:feature_flags:2:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:3:enabled"]);
- Assert.Equal("App2_Feature2", config["feature_management:feature_flags:3:id"]);
-
- // even though App2_Feature3 feature flag has been added, its value should not be loaded in config because label2 refresh interval has not elapsed
- Assert.Null(config["feature_management:feature_flags:4"]);
+ 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("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 async Task OverwrittenCacheExpirationForSameFeatureFlagRegistrations()
+ public async Task OverwrittenRefreshIntervalForSameFeatureFlagRegistrations()
{
var mockResponse = new Mock();
var mockClient = new Mock(MockBehavior.Strict);
@@ -1524,16 +1498,11 @@ public async Task OverwrittenCacheExpirationForSameFeatureFlagRegistrations()
})
.Build();
- Assert.Equal("True", config["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("App1_Feature1", config["feature_management:feature_flags:0:id"]);
- Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
- Assert.Equal("App1_Feature2", config["feature_management:feature_flags:1:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:2:enabled"]);
- Assert.Equal("Feature1", config["feature_management:feature_flags:2:id"]);
- Assert.Equal("False", config["feature_management:feature_flags:3:enabled"]);
- Assert.Equal("App2_Feature1", config["feature_management:feature_flags:3:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:4:enabled"]);
- Assert.Equal("App2_Feature2", config["feature_management:feature_flags:4:id"]);
+ 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(
@@ -1563,16 +1532,11 @@ public async Task OverwrittenCacheExpirationForSameFeatureFlagRegistrations()
// 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["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("App1_Feature1", config["feature_management:feature_flags:0:id"]);
- Assert.Equal("False", config["feature_management:feature_flags:1:enabled"]);
- Assert.Equal("App1_Feature2", config["feature_management:feature_flags:1:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:2:enabled"]);
- Assert.Equal("Feature1", config["feature_management:feature_flags:2:id"]);
- Assert.Equal("False", config["feature_management:feature_flags:3:enabled"]);
- Assert.Equal("App2_Feature1", config["feature_management:feature_flags:3:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:4:enabled"]);
- Assert.Equal("App2_Feature2", config["feature_management:feature_flags:4:id"]);
+ 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]
@@ -1606,8 +1570,7 @@ public async Task SelectAndRefreshSingleFeatureFlag()
})
.Build();
- Assert.Equal("False", config["feature_management:feature_flags:0:enabled"]);
- Assert.Equal("Feature1", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("False", config["FeatureManagement:Feature1"]);
// update the value of Feature1 feature flag with App1_Label
featureFlagCollection[2] = ConfigurationModelFactory.ConfigurationSetting(
@@ -1636,9 +1599,9 @@ public async Task SelectAndRefreshSingleFeatureFlag()
Thread.Sleep(RefreshInterval);
await refresher.RefreshAsync();
- Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
- Assert.Equal("Chrome", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
- Assert.Equal("Edge", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
+ Assert.Equal("Browser", config["FeatureManagement:Feature1:EnabledFor:0:Name"]);
+ Assert.Equal("Chrome", config["FeatureManagement:Feature1:EnabledFor:0:Parameters:AllowedBrowsers:0"]);
+ Assert.Equal("Edge", config["FeatureManagement:Feature1:EnabledFor:0:Parameters:AllowedBrowsers:1"]);
}
[Fact]
@@ -1678,8 +1641,7 @@ public async Task ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefre
})
.Build();
- Assert.Equal("SuperUsers", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
- Assert.Equal("MyFeature2", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("SuperUsers", config["FeatureManagement:MyFeature2:EnabledFor:0:Name"]);
featureFlags[0] = ConfigurationModelFactory.ConfigurationSetting(
key: FeatureManagementConstants.FeatureFlagMarker + "myFeature1",
@@ -1704,8 +1666,7 @@ public async Task ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefre
Thread.Sleep(RefreshInterval);
await refresher.TryRefreshAsync();
- Assert.Equal("AllUsers", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
- Assert.Equal("MyFeature", config["feature_management:feature_flags:0:id"]);
+ 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);
@@ -1713,7 +1674,7 @@ public async Task ValidateCorrectFeatureFlagLoggedIfModifiedOrRemovedDuringRefre
Thread.Sleep(RefreshInterval);
await refresher.TryRefreshAsync();
- Assert.Null(config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Null(config["FeatureManagement:MyFeature:EnabledFor:0:Name"]);
Assert.Contains(LogHelper.BuildFeatureFlagReadMessage("myFeature1", null, TestHelpers.PrimaryConfigStoreEndpoint.ToString().TrimEnd('/')), verboseInvocation);
Assert.Contains(LogHelper.BuildFeatureFlagUpdatedMessage("myFeature1"), informationalInvocation);
}
@@ -1761,12 +1722,12 @@ public async Task ValidateFeatureFlagsUnchangedLogged()
})
.Build();
- Assert.Equal("SuperUsers", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("SuperUsers", config["FeatureManagement:MyFeature2:EnabledFor:0:Name"]);
FirstKeyValue.Value = "newValue1";
Thread.Sleep(RefreshInterval);
await refresher.TryRefreshAsync();
- Assert.Equal("SuperUsers", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("SuperUsers", config["FeatureManagement:MyFeature2:EnabledFor:0:Name"]);
Assert.Contains(LogHelper.BuildFeatureFlagsUnchangedMessage(TestHelpers.PrimaryConfigStoreEndpoint.ToString().TrimEnd('/')), verboseInvocation);
}
@@ -1852,8 +1813,7 @@ public async Task MapTransformFeatureFlagWithRefresh()
.Build();
Assert.Equal("TestValue1", config["TestKey1"]);
- Assert.Equal("NoUsers", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
- Assert.Equal("MyFeature", config["feature_management:feature_flags:0:id"]);
+ Assert.Equal("NoUsers", config["FeatureManagement:MyFeature:EnabledFor:0:Name"]);
FirstKeyValue.Value = "newValue1";
featureFlags[0] = ConfigurationModelFactory.ConfigurationSetting(
@@ -1881,7 +1841,7 @@ public async Task MapTransformFeatureFlagWithRefresh()
await refresher.TryRefreshAsync();
Assert.Equal("newValue1", config["TestKey1"]);
- Assert.Equal("NoUsers", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
+ Assert.Equal("NoUsers", config["FeatureManagement:MyFeature:EnabledFor:0:Name"]);
}
[Fact]
@@ -1963,10 +1923,7 @@ public void WithVariants()
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("VariantsFeature4", config["feature_management:feature_flags:3:id"]);
- Assert.Equal("True", config["feature_management:feature_flags:3:enabled"]);
- Assert.Null(config["feature_management:feature_flags:3:variants"]);
- Assert.Null(config["feature_management:feature_flags:3:allocation"]);
+ Assert.Equal("True", config["FeatureManagement:VariantsFeature4"]);
}
[Fact]
@@ -2052,13 +2009,11 @@ public void WithRequirementType()
.Build();
Assert.Null(config["feature_management:feature_flags:0:requirement_type"]);
- Assert.Equal("MyFeature2", config["feature_management:feature_flags:0:id"]);
- Assert.Null(config["feature_management:feature_flags:1:requirement_type"]);
- Assert.Equal("Feature_NoFilters", config["feature_management:feature_flags:1:id"]);
- Assert.Equal("All", config["feature_management:feature_flags:2:conditions:requirement_type"]);
- Assert.Equal("Feature_RequireAll", config["feature_management:feature_flags:2:id"]);
- Assert.Equal("Any", config["feature_management:feature_flags:3:conditions:requirement_type"]);
- Assert.Equal("Feature_RequireAny", config["feature_management:feature_flags:3:id"]);
+ 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]
diff --git a/tests/Tests.AzureAppConfiguration/JsonContentTypeTests.cs b/tests/Tests.AzureAppConfiguration/JsonContentTypeTests.cs
index 7f36dfdb..91cb03a8 100644
--- a/tests/Tests.AzureAppConfiguration/JsonContentTypeTests.cs
+++ b/tests/Tests.AzureAppConfiguration/JsonContentTypeTests.cs
@@ -216,9 +216,9 @@ public void JsonContentTypeTests_DontFlattenFeatureFlagAsJsonObject()
.AddAzureAppConfiguration(options => options.ClientManager = mockClientManager)
.Build();
- Assert.Equal("Browser", config["feature_management:feature_flags:0:conditions:client_filters:0:name"]);
- Assert.Equal("Firefox", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:0"]);
- Assert.Equal("Safari", config["feature_management:feature_flags:0:conditions:client_filters:0:parameters:AllowedBrowsers:1"]);
+ 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"]);
}
[Fact]
From 73e8838b6c5dabdf71165dcb674274dadc7b185e Mon Sep 17 00:00:00 2001
From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
Date: Fri, 12 Jul 2024 12:42:06 -0700
Subject: [PATCH 17/21] update azure.security.keyvault.secrets (#572)
---
...rosoft.Extensions.Configuration.AzureAppConfiguration.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 12b99a00..ab923a8b 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
@@ -16,7 +16,7 @@
-
+
From 5c44ff69c09207915d2155a98487907ea3ac13f3 Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Tue, 16 Jul 2024 10:19:51 +0800
Subject: [PATCH 18/21] Detect SignalR usage for request tracing (#570)
* collect usage of SignalR
* fix typo
* fix typo
* rename signalR
---
.../AzureAppConfigurationProvider.cs | 15 ++++++++++-----
.../Constants/RequestTracingConstants.cs | 3 +++
.../RequestTracingOptions.cs | 5 +++++
.../TracingUtils.cs | 5 +++++
4 files changed, 23 insertions(+), 5 deletions(-)
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
index 182c819a..0868ea14 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
@@ -26,7 +26,7 @@ internal class AzureAppConfigurationProvider : ConfigurationProvider, IConfigura
{
private bool _optional;
private bool _isInitialLoadComplete = false;
- private bool _isFeatureManagementVersionInspected;
+ private bool _isAssemblyInspected;
private readonly bool _requestTracingEnabled;
private readonly IConfigurationClientManager _configClientManager;
private AzureAppConfigurationOptions _options;
@@ -189,7 +189,7 @@ 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);
@@ -1186,17 +1186,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/Constants/RequestTracingConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs
index 9ab29637..f1da875b 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs
@@ -30,6 +30,7 @@ internal class RequestTracingConstants
public const string KeyVaultConfiguredTag = "UsesKeyVault";
public const string KeyVaultRefreshConfiguredTag = "RefreshesKeyVault";
public const string ReplicaCountKey = "ReplicaCount";
+ public const string SignalRUsedTag = "UsesSignalR";
public const string DiagnosticHeaderActivityName = "Azure.CustomDiagnosticHeaders";
public const string CorrelationContextHeader = "Correlation-Context";
@@ -37,5 +38,7 @@ internal class RequestTracingConstants
public const string FeatureManagementAssemblyName = "Microsoft.FeatureManagement";
public const string FeatureManagementAspNetCoreAssemblyName = "Microsoft.FeatureManagement.AspNetCore";
+
+ public const string SignalRAssemblyName = "Microsoft.AspNetCore.SignalR";
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs
index 27af8fae..b5769b9d 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs
@@ -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..bbf8511c 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs
@@ -160,6 +160,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)
From 0c9e17a9e04b7be98388db7c7dc6e0dffc37939e Mon Sep 17 00:00:00 2001
From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
Date: Tue, 16 Jul 2024 10:44:13 -0700
Subject: [PATCH 19/21] update package versions to 7.3.0 (#575)
---
.../Microsoft.Azure.AppConfiguration.AspNetCore.csproj | 2 +-
.../Microsoft.Azure.AppConfiguration.Functions.Worker.csproj | 2 +-
...rosoft.Extensions.Configuration.AzureAppConfiguration.csproj | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj b/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
index c081cacd..5c33c144 100644
--- a/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
+++ b/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
@@ -21,7 +21,7 @@
- 7.2.0
+ 7.3.0
diff --git a/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj b/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj
index 90831b4d..43d8d181 100644
--- a/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj
+++ b/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj
@@ -24,7 +24,7 @@
-