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 @@ - 7.2.0 + 7.3.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 ab923a8b..da78f0e9 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.2.0 + 7.3.0 From 2f088aadfd636b64bed7bc48fd1d61a871693990 Mon Sep 17 00:00:00 2001 From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com> Date: Wed, 17 Jul 2024 10:44:03 -0700 Subject: [PATCH 20/21] Add variant allocation and flag telemetry information to request tracing (#540) * WIP create featurevariantstracing class * WIP class structure * add first working draft, combine tracing for fm and add allocation/telemetry * remove unused string * fix naming, add variant present tag * change constants names * update highest variants key and usesSeed tag * variant configuration suggestion * change variant config info to tag for ref only * reformatting tracing * add method for new features key in request tracing * fix tracing reference * PR comments * adjust features to have flag/other features lists * remove features key * Update src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs Co-authored-by: Avani Gupta * rename variables and methods --------- Co-authored-by: Avani Gupta --- .../AzureAppConfigurationOptions.cs | 4 +- .../AzureAppConfigurationProvider.cs | 6 +- .../Constants/RequestTracingConstants.cs | 7 ++ ...FilterTracing.cs => FeatureFlagTracing.cs} | 68 ++++++++++++++++--- .../FeatureManagementKeyValueAdapter.cs | 18 +++-- .../RequestTracingOptions.cs | 4 +- .../TracingUtils.cs | 14 +++- 7 files changed, 99 insertions(+), 22 deletions(-) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/{FeatureFilterTracing.cs => FeatureFlagTracing.cs} (65%) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index b715f656..7d9a9cad 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -124,7 +124,7 @@ internal IEnumerable Adapters /// /// Indicates all types of feature filters used by the application. /// - internal FeatureFilterTracing FeatureFilterTracing { get; set; } = new FeatureFilterTracing(); + internal FeatureFlagTracing FeatureFlagTracing { get; set; } = new FeatureFlagTracing(); /// /// Options used to configure provider startup. @@ -140,7 +140,7 @@ public AzureAppConfigurationOptions() { new AzureKeyVaultKeyValueAdapter(new AzureKeyVaultSecretProvider()), new JsonKeyValueAdapter(), - new FeatureManagementKeyValueAdapter(FeatureFilterTracing) + new FeatureManagementKeyValueAdapter(FeatureFlagTracing) }; } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index a8755ce8..385a65b1 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -560,8 +560,8 @@ private async Task> PrepareData(Dictionary(StringComparer.OrdinalIgnoreCase); - // Reset old filter tracing in order to track the filter types present in the current response from server. - _options.FeatureFilterTracing.ResetFeatureFilterTracing(); + // Reset old feature flag tracing in order to track the information present in the current response from server. + _options.FeatureFlagTracing.ResetFeatureFlagTracing(); foreach (KeyValuePair kvp in data) { @@ -968,7 +968,7 @@ private void SetRequestTracingOptions() IsKeyVaultConfigured = _options.IsKeyVaultConfigured, IsKeyVaultRefreshConfigured = _options.IsKeyVaultRefreshConfigured, ReplicaCount = _options.Endpoints?.Count() - 1 ?? _options.ConnectionStrings?.Count() - 1 ?? 0, - FilterTracing = _options.FeatureFilterTracing + FeatureFlagTracing = _options.FeatureFlagTracing }; } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs index 9ab29637..785fa5a0 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs @@ -30,6 +30,11 @@ internal class RequestTracingConstants public const string KeyVaultConfiguredTag = "UsesKeyVault"; public const string KeyVaultRefreshConfiguredTag = "RefreshesKeyVault"; public const string ReplicaCountKey = "ReplicaCount"; + public const string FeatureFlagUsesTelemetryTag = "Telemetry"; + public const string FeatureFlagUsesSeedTag = "Seed"; + public const string FeatureFlagMaxVariantsKey = "MaxVariants"; + public const string FeatureFlagUsesVariantConfigurationReferenceTag = "ConfigRef"; + public const string FeatureFlagFeaturesKey = "FFFeatures"; public const string DiagnosticHeaderActivityName = "Azure.CustomDiagnosticHeaders"; public const string CorrelationContextHeader = "Correlation-Context"; @@ -37,5 +42,7 @@ internal class RequestTracingConstants public const string FeatureManagementAssemblyName = "Microsoft.FeatureManagement"; public const string FeatureManagementAspNetCoreAssemblyName = "Microsoft.FeatureManagement.AspNetCore"; + + public const string Delimiter = "+"; } } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFilterTracing.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagTracing.cs similarity index 65% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFilterTracing.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagTracing.cs index 27b59205..c1e4aaa5 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFilterTracing.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureFlagTracing.cs @@ -12,13 +12,12 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManage /// /// Tracing for tracking built-in feature filter usage. /// - internal class FeatureFilterTracing + internal class FeatureFlagTracing { private const string CustomFilter = "CSTM"; private const string PercentageFilter = "PRCNT"; private const string TimeWindowFilter = "TIME"; private const string TargetingFilter = "TRGT"; - private const string FilterTypeDelimiter = "+"; // Built-in Feature Filter Names private readonly List PercentageFilterNames = new List { "Percentage", "Microsoft.Percentage", "PercentageFilter", "Microsoft.PercentageFilter" }; @@ -29,18 +28,31 @@ internal class FeatureFilterTracing public bool UsesPercentageFilter { get; set; } = false; public bool UsesTimeWindowFilter { get; set; } = false; public bool UsesTargetingFilter { get; set; } = false; - + public bool UsesSeed { get; set; } = false; + public bool UsesTelemetry { get; set; } = false; + public bool UsesVariantConfigurationReference { get; set; } = false; + public int MaxVariants { get; set; } + public bool UsesAnyFeatureFilter() { return UsesCustomFilter || UsesPercentageFilter || UsesTimeWindowFilter || UsesTargetingFilter; } - public void ResetFeatureFilterTracing() + public bool UsesAnyTracingFeature() + { + return UsesSeed || UsesTelemetry || UsesVariantConfigurationReference; + } + + public void ResetFeatureFlagTracing() { UsesCustomFilter = false; UsesPercentageFilter = false; UsesTimeWindowFilter = false; UsesTargetingFilter = false; + UsesSeed = false; + UsesTelemetry = false; + UsesVariantConfigurationReference = false; + MaxVariants = 0; } public void UpdateFeatureFilterTracing(string filterName) @@ -63,11 +75,19 @@ public void UpdateFeatureFilterTracing(string filterName) } } + public void NotifyMaxVariants(int currentFlagTotalVariants) + { + if (currentFlagTotalVariants > MaxVariants) + { + MaxVariants = currentFlagTotalVariants; + } + } + /// /// Returns a formatted string containing code names, indicating which feature filters are used by the application. /// /// Formatted string like: "CSTM+PRCNT+TIME+TRGT", "PRCNT+TRGT", etc. If no filters are used, empty string will be returned. - public override string ToString() + public string CreateFiltersString() { if (!UsesAnyFeatureFilter()) { @@ -85,7 +105,7 @@ public override string ToString() { if (sb.Length > 0) { - sb.Append(FilterTypeDelimiter); + sb.Append(RequestTracingConstants.Delimiter); } sb.Append(PercentageFilter); @@ -95,7 +115,7 @@ public override string ToString() { if (sb.Length > 0) { - sb.Append(FilterTypeDelimiter); + sb.Append(RequestTracingConstants.Delimiter); } sb.Append(TimeWindowFilter); @@ -105,7 +125,7 @@ public override string ToString() { if (sb.Length > 0) { - sb.Append(FilterTypeDelimiter); + sb.Append(RequestTracingConstants.Delimiter); } sb.Append(TargetingFilter); @@ -113,5 +133,37 @@ public override string ToString() return sb.ToString(); } + + public string CreateFeaturesString() + { + var sb = new StringBuilder(); + + if (UsesSeed) + { + sb.Append(RequestTracingConstants.FeatureFlagUsesSeedTag); + } + + if (UsesVariantConfigurationReference) + { + if (sb.Length > 0) + { + sb.Append(RequestTracingConstants.Delimiter); + } + + sb.Append(RequestTracingConstants.FeatureFlagUsesVariantConfigurationReferenceTag); + } + + if (UsesTelemetry) + { + if (sb.Length > 0) + { + sb.Append(RequestTracingConstants.Delimiter); + } + + sb.Append(RequestTracingConstants.FeatureFlagUsesTelemetryTag); + } + + return sb.ToString(); + } } } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs index b7639bb8..b562a904 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs @@ -16,12 +16,12 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManage { internal class FeatureManagementKeyValueAdapter : IKeyValueAdapter { - private FeatureFilterTracing _featureFilterTracing; + private FeatureFlagTracing _featureFlagTracing; private int _featureFlagIndex = 0; - public FeatureManagementKeyValueAdapter(FeatureFilterTracing featureFilterTracing) + public FeatureManagementKeyValueAdapter(FeatureFlagTracing featureFlagTracing) { - _featureFilterTracing = featureFilterTracing ?? throw new ArgumentNullException(nameof(featureFilterTracing)); + _featureFlagTracing = featureFlagTracing ?? throw new ArgumentNullException(nameof(featureFlagTracing)); } public Task>> ProcessKeyValue(ConfigurationSetting setting, Uri endpoint, Logger logger, CancellationToken cancellationToken) @@ -91,7 +91,7 @@ private List> ProcessDotnetSchemaFeatureFlag(Featur { ClientFilter clientFilter = featureFlag.Conditions.ClientFilters[i]; - _featureFilterTracing.UpdateFeatureFilterTracing(clientFilter.Name); + _featureFlagTracing.UpdateFeatureFilterTracing(clientFilter.Name); string clientFiltersPath = $"{featureFlagPath}:{FeatureManagementConstants.DotnetSchemaEnabledFor}:{i}"; @@ -148,7 +148,7 @@ private List> ProcessMicrosoftSchemaFeatureFlag(Fea { ClientFilter clientFilter = featureFlag.Conditions.ClientFilters[i]; - _featureFilterTracing.UpdateFeatureFilterTracing(clientFilter.Name); + _featureFlagTracing.UpdateFeatureFilterTracing(clientFilter.Name); string clientFiltersPath = $"{featureFlagPath}:{FeatureManagementConstants.Conditions}:{FeatureManagementConstants.ClientFilters}:{i}"; @@ -189,6 +189,8 @@ private List> ProcessMicrosoftSchemaFeatureFlag(Fea if (featureVariant.ConfigurationReference != null) { + _featureFlagTracing.UsesVariantConfigurationReference = true; + keyValues.Add(new KeyValuePair($"{variantsPath}:{FeatureManagementConstants.ConfigurationReference}", featureVariant.ConfigurationReference)); } @@ -199,6 +201,8 @@ private List> ProcessMicrosoftSchemaFeatureFlag(Fea i++; } + + _featureFlagTracing.NotifyMaxVariants(i); } if (featureFlag.Allocation != null) @@ -277,6 +281,8 @@ private List> ProcessMicrosoftSchemaFeatureFlag(Fea if (allocation.Seed != null) { + _featureFlagTracing.UsesSeed = true; + keyValues.Add(new KeyValuePair($"{allocationPath}:{FeatureManagementConstants.Seed}", allocation.Seed)); } } @@ -289,6 +295,8 @@ private List> ProcessMicrosoftSchemaFeatureFlag(Fea if (telemetry.Enabled) { + _featureFlagTracing.UsesTelemetry = true; + if (telemetry.Metadata != null) { foreach (KeyValuePair kvp in telemetry.Metadata) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs index 27af8fae..0a6a378c 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs @@ -33,9 +33,9 @@ internal class RequestTracingOptions public int ReplicaCount { get; set; } = 0; /// - /// Type of feature filters used by the application. + /// Information about feature flags in the application, like filter and variant usage. /// - public FeatureFilterTracing FilterTracing { get; set; } = new FeatureFilterTracing(); + public FeatureFlagTracing FeatureFlagTracing { get; set; } = new FeatureFlagTracing(); /// /// Version of the Microsoft.FeatureManagement assembly, if present in the application. diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs index 8b225f2e..1210812c 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs @@ -156,9 +156,19 @@ private static string CreateCorrelationContextHeader(RequestType requestType, Re correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.EnvironmentKey, RequestTracingConstants.DevEnvironmentValue)); } - if (requestTracingOptions.FilterTracing.UsesAnyFeatureFilter()) + if (requestTracingOptions.FeatureFlagTracing.UsesAnyFeatureFilter()) { - correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.FilterTypeKey, requestTracingOptions.FilterTracing.ToString())); + correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.FilterTypeKey, requestTracingOptions.FeatureFlagTracing.CreateFiltersString())); + } + + if (requestTracingOptions.FeatureFlagTracing.MaxVariants > 0) + { + correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.FeatureFlagMaxVariantsKey, requestTracingOptions.FeatureFlagTracing.MaxVariants.ToString())); + } + + if (requestTracingOptions.FeatureFlagTracing.UsesAnyTracingFeature()) + { + correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.FeatureFlagFeaturesKey, requestTracingOptions.FeatureFlagTracing.CreateFeaturesString())); } if (requestTracingOptions.FeatureManagementVersion != null) From 42ec7d14ed56275c213d8e9007b6121537957c09 Mon Sep 17 00:00:00 2001 From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com> Date: Thu, 18 Jul 2024 10:06:51 -0700 Subject: [PATCH 21/21] update package versions to 8.0.0-preview.3 (#580) --- .../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 f79bf688..cb429e03 100644 --- a/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj +++ b/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj @@ -21,7 +21,7 @@ - 8.0.0-preview.2 + 8.0.0-preview.3 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 03b9df50..67df4997 100644 --- a/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj +++ b/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj @@ -24,7 +24,7 @@ - 8.0.0-preview.2 + 8.0.0-preview.3 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 1fa30a62..31e1e854 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 @@ - 8.0.0-preview.2 + 8.0.0-preview.3