From b9b4aa2304b3b50cf568c3a83459c22e04be3130 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Sun, 18 Feb 2024 15:22:21 +0800 Subject: [PATCH 1/6] use snake case --- .../ConfigurationFeatureDefinitionProvider.cs | 69 +++--- ...cs => MicrosoftFeatureManagementFields.cs} | 9 +- .../FeatureManagement.cs | 196 ------------------ .../MicrosoftFeatureManagement.json | 77 +++++++ .../MicrosoftFeatureManagementSchema.cs | 192 +++++++++++++++++ .../Tests.FeatureManagement.csproj | 3 + 6 files changed, 320 insertions(+), 226 deletions(-) rename src/Microsoft.FeatureManagement/{MicrosoftFeatureFlagFields.cs => MicrosoftFeatureManagementFields.cs} (64%) create mode 100644 tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json create mode 100644 tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 6e940760..9d079e05 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -134,21 +134,38 @@ private void EnsureInit() { if (_initialized == 0) { + bool hasMicrosoftFeatureFlagSchema = false; + IConfiguration featureManagementConfigurationSection = _configuration + .GetChildren() + .FirstOrDefault(section => + string.Equals( + section.Key, + MicrosoftFeatureManagementFields.FeatureManagementSectionName, + StringComparison.OrdinalIgnoreCase)); + + if (featureManagementConfigurationSection != null) + { + hasMicrosoftFeatureFlagSchema = true; + } + else + { + featureManagementConfigurationSection = _configuration .GetChildren() .FirstOrDefault(section => string.Equals( section.Key, ConfigurationFields.FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)); + } if (featureManagementConfigurationSection == null && RootConfigurationFallbackEnabled) { featureManagementConfigurationSection = _configuration; - } - bool hasMicrosoftFeatureFlagSchema = featureManagementConfigurationSection != null && - HasMicrosoftFeatureFlagSchema(featureManagementConfigurationSection); + hasMicrosoftFeatureFlagSchema = featureManagementConfigurationSection != null && + HasMicrosoftFeatureFlagSchema(featureManagementConfigurationSection); + } lock (_lock) { @@ -321,20 +338,9 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection RequirementType requirementType = RequirementType.Any; - IConfigurationSection conditions = configurationSection.GetSection(MicrosoftFeatureFlagFields.Conditions); - - string rawRequirementType = conditions[MicrosoftFeatureFlagFields.RequirementType]; - - // - // If requirement type is specified, parse it and set the requirementType variable - if (!string.IsNullOrEmpty(rawRequirementType) && !Enum.TryParse(rawRequirementType, ignoreCase: true, out requirementType)) - { - throw new FeatureManagementException( - FeatureManagementError.InvalidConfigurationSetting, - $"Invalid value '{rawRequirementType}' for field '{MicrosoftFeatureFlagFields.RequirementType}' of feature '{featureName}'."); - } + IConfigurationSection conditions = configurationSection.GetSection(MicrosoftFeatureManagementFields.Conditions); - string rawEnabled = configurationSection[MicrosoftFeatureFlagFields.Enabled]; + string rawEnabled = configurationSection[MicrosoftFeatureManagementFields.Enabled]; bool enabled = false; @@ -342,12 +348,23 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection { throw new FeatureManagementException( FeatureManagementError.InvalidConfigurationSetting, - $"Invalid value '{rawEnabled}' for field '{MicrosoftFeatureFlagFields.Enabled}' of feature '{featureName}'."); + $"Invalid value '{rawEnabled}' for field '{MicrosoftFeatureManagementFields.Enabled}' of feature '{featureName}'."); } if (enabled) { - IEnumerable filterSections = conditions.GetSection(MicrosoftFeatureFlagFields.ClientFilters).GetChildren(); + string rawRequirementType = conditions[MicrosoftFeatureManagementFields.RequirementType]; + + // + // If requirement type is specified, parse it and set the requirementType variable + if (!string.IsNullOrEmpty(rawRequirementType) && !Enum.TryParse(rawRequirementType, ignoreCase: true, out requirementType)) + { + throw new FeatureManagementException( + FeatureManagementError.InvalidConfigurationSetting, + $"Invalid value '{rawRequirementType}' for field '{MicrosoftFeatureManagementFields.RequirementType}' of feature '{featureName}'."); + } + + IEnumerable filterSections = conditions.GetSection(MicrosoftFeatureManagementFields.ClientFilters).GetChildren(); if (filterSections.Any()) { @@ -356,12 +373,12 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection // // Arrays in json such as "myKey": [ "some", "values" ] // Are accessed through the configuration system by using the array index as the property name, e.g. "myKey": { "0": "some", "1": "values" } - if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[MicrosoftFeatureFlagFields.Name])) + if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[MicrosoftFeatureManagementFields.Name])) { enabledFor.Add(new FeatureFilterConfiguration() { - Name = section[MicrosoftFeatureFlagFields.Name], - Parameters = new ConfigurationWrapper(section.GetSection(MicrosoftFeatureFlagFields.Parameters)) + Name = section[MicrosoftFeatureManagementFields.Name], + Parameters = new ConfigurationWrapper(section.GetSection(MicrosoftFeatureManagementFields.Parameters)) }); } } @@ -387,7 +404,7 @@ private string GetFeatureName(IConfigurationSection section) { if (_microsoftFeatureFlagSchemaEnabled) { - return section[MicrosoftFeatureFlagFields.Id]; + return section[MicrosoftFeatureManagementFields.Id]; } return section.Key; @@ -407,7 +424,9 @@ private IEnumerable GetFeatureDefinitionSections() .FirstOrDefault(section => string.Equals( section.Key, - ConfigurationFields.FeatureManagementSectionName, + _microsoftFeatureFlagSchemaEnabled ? + MicrosoftFeatureManagementFields.FeatureManagementSectionName : + ConfigurationFields.FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)); if (featureManagementConfigurationSection == null) @@ -426,7 +445,7 @@ private IEnumerable GetFeatureDefinitionSections() if (_microsoftFeatureFlagSchemaEnabled) { - IConfigurationSection featureFlagsSection = featureManagementConfigurationSection.GetSection(MicrosoftFeatureFlagFields.FeatureFlagsSectionName); + IConfigurationSection featureFlagsSection = featureManagementConfigurationSection.GetSection(MicrosoftFeatureManagementFields.FeatureFlagsSectionName); return featureFlagsSection.GetChildren(); } @@ -441,7 +460,7 @@ private static bool HasMicrosoftFeatureFlagSchema(IConfiguration featureManageme .FirstOrDefault(section => string.Equals( section.Key, - MicrosoftFeatureFlagFields.FeatureFlagsSectionName, + MicrosoftFeatureManagementFields.FeatureFlagsSectionName, StringComparison.OrdinalIgnoreCase)); if (featureFlagsConfigurationSection != null) diff --git a/src/Microsoft.FeatureManagement/MicrosoftFeatureFlagFields.cs b/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs similarity index 64% rename from src/Microsoft.FeatureManagement/MicrosoftFeatureFlagFields.cs rename to src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs index 21286e5c..ce776897 100644 --- a/src/Microsoft.FeatureManagement/MicrosoftFeatureFlagFields.cs +++ b/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs @@ -4,14 +4,13 @@ namespace Microsoft.FeatureManagement { - // - // Microsoft feature flag schema: https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureFlag.v1.1.0.schema.json - internal static class MicrosoftFeatureFlagFields + internal static class MicrosoftFeatureManagementFields { - public const string FeatureFlagsSectionName = "FeatureFlags"; + public const string FeatureManagementSectionName = "feature_management"; + public const string FeatureFlagsSectionName = "feature_flags"; // - // Feature flag keywords + // Microsoft feature flag keywords public const string Id = "id"; public const string Enabled = "enabled"; public const string Conditions = "conditions"; diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index 8a360c89..8f965fd9 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -106,202 +106,6 @@ public async Task ReadsTopLevelConfiguration() Assert.True(await featureManager.IsEnabledAsync(feature)); } - [Fact] - public async Task ReadsMicrosoftFeatureFlagSchema() - { - string json = @" - { - ""AllowedHosts"": ""*"", - ""FeatureManagement"": { - ""MyFeature"": true, - ""FeatureFlags"": [ - { - ""id"": ""Alpha"", - ""enabled"": true, - ""conditions"": { - ""client_filters"": [] - } - }, - { - ""id"": ""Beta"", - ""enabled"": true, - ""conditions"": { - ""client_filters"": [ - { - ""name"": ""Percentage"", - ""parameters"": { - ""Value"": 100 - } - }, - { - ""name"": ""Targeting"", - ""parameters"": { - ""Audience"": { - ""Users"": [""Jeff""], - ""Groups"": [], - ""DefaultRolloutPercentage"": 0 - } - } - } - ], - ""requirement_type"" : ""all"" - } - }, - { - ""id"": ""Sigma"", - ""enabled"": false, - ""conditions"": { - ""client_filters"": [ - { - ""name"": ""Percentage"", - ""parameters"": { - ""Value"": 100 - } - } - ] - } - }, - { - ""id"": ""Omega"", - ""enabled"": true, - ""conditions"": { - ""client_filters"": [ - { - ""name"": ""Percentage"", - ""parameters"": { - ""Value"": 100 - } - }, - { - ""name"": ""Percentage"", - ""parameters"": { - ""Value"": 0 - } - } - ] - } - } - ] - } - }"; - - var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); - - IConfiguration config = new ConfigurationBuilder().AddJsonStream(stream).Build(); - - var services = new ServiceCollection(); - - services.AddSingleton(config) - .AddFeatureManagement(); - - ServiceProvider serviceProvider = services.BuildServiceProvider(); - - IFeatureManager featureManager = serviceProvider.GetRequiredService(); - - Assert.False(await featureManager.IsEnabledAsync("MyFeature")); - - Assert.True(await featureManager.IsEnabledAsync("Alpha")); - - Assert.True(await featureManager.IsEnabledAsync("Beta", new TargetingContext - { - UserId = "Jeff" - })); - - Assert.False(await featureManager.IsEnabledAsync("Beta", new TargetingContext - { - UserId = "Sam" - })); - - Assert.False(await featureManager.IsEnabledAsync("Sigma")); - - Assert.True(await featureManager.IsEnabledAsync("Omega")); - - json = @" - { - ""AllowedHosts"": ""*"", - ""FeatureManagement"": { - ""MyFeature"": true, - ""FeatureFlags"": [ - { - ""id"": ""Alpha"", - ""enabled"": true - } - ] - } - }"; - - stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); - - config = new ConfigurationBuilder().AddJsonStream(stream).Build(); - - services = new ServiceCollection(); - - services.AddFeatureManagement(config.GetSection("FeatureManagement")); - - serviceProvider = services.BuildServiceProvider(); - - featureManager = serviceProvider.GetRequiredService(); - - Assert.False(await featureManager.IsEnabledAsync("MyFeature")); - - Assert.True(await featureManager.IsEnabledAsync("Alpha")); - - json = @" - { - ""AllowedHosts"": ""*"", - ""FeatureManagement"": { - ""MyFeature"": true, - ""FeatureFlags"": true - } - }"; - - stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); - - config = new ConfigurationBuilder().AddJsonStream(stream).Build(); - - services = new ServiceCollection(); - - services.AddFeatureManagement(config.GetSection("FeatureManagement")); - - serviceProvider = services.BuildServiceProvider(); - - featureManager = serviceProvider.GetRequiredService(); - - Assert.True(await featureManager.IsEnabledAsync("MyFeature")); - - Assert.True(await featureManager.IsEnabledAsync("FeatureFlags")); - - json = @" - { - ""AllowedHosts"": ""*"", - ""FeatureManagement"": { - ""MyFeature"": true, - ""FeatureFlags"": { - ""EnabledFor"": [ - { - ""Name"": ""AlwaysOn"" - } - ] - } - } - }"; - - stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); - - config = new ConfigurationBuilder().AddJsonStream(stream).Build(); - - services = new ServiceCollection(); - - services.AddFeatureManagement(config.GetSection("FeatureManagement")); - - serviceProvider = services.BuildServiceProvider(); - - featureManager = serviceProvider.GetRequiredService(); - - Assert.True(await featureManager.IsEnabledAsync("MyFeature")); - - Assert.True(await featureManager.IsEnabledAsync("FeatureFlags")); - } [Fact] public void AddsScopedFeatureManagement() diff --git a/tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json b/tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json new file mode 100644 index 00000000..b3c8db10 --- /dev/null +++ b/tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json @@ -0,0 +1,77 @@ +{ + "feature_management": { + "feature_flags": [ + { + "id": "OnTestFeature", + "enabled": true + }, + { + "id": "OffTestFeature", + "enabled": false, + "conditions": { + "client_filters": [ + { + "name": "AlwaysOn" + } + ] + } + }, + { + "id": "ConditionalFeature", + "enabled": true, + "conditions": { + "client_filters": [ + { + "name": "Test", + "parameters": { + "P1": "V1" + } + } + ] + } + }, + { + "id": "AnyFilterFeature", + "enabled": true, + "conditions": { + "requirement_type": "any", + "client_filters": [ + { + "name": "Test", + "parameters": { + "Id": "1" + } + }, + { + "name": "Test", + "parameters": { + "Id": "2" + } + } + ] + } + }, + { + "id": "AllFilterFeature", + "enabled": true, + "conditions": { + "requirement_type": "all", + "client_filters": [ + { + "name": "Test", + "parameters": { + "Id": "1" + } + }, + { + "name": "Test", + "parameters": { + "Id": "2" + } + } + ] + } + } + ] + } +} diff --git a/tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs b/tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs new file mode 100644 index 00000000..852e1dda --- /dev/null +++ b/tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.FeatureManagement; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Tests.FeatureManagement +{ + public class MicrosoftFeatureFlagSchemaTest + { + [Fact] + public async Task ReadsFeatureDefinition() + { + IConfiguration config = new ConfigurationBuilder().AddJsonFile("MicrosoftFeatureManagement.json").Build(); + + var featureDefinitionProvider = new ConfigurationFeatureDefinitionProvider(config); + + FeatureDefinition featureDefinition = await featureDefinitionProvider.GetFeatureDefinitionAsync(Features.OnTestFeature); + + Assert.NotNull(featureDefinition); + + Assert.NotEmpty(featureDefinition.EnabledFor); + + FeatureFilterConfiguration filterConfig = featureDefinition.EnabledFor.First(); + + Assert.Equal("AlwaysOn", filterConfig.Name); + + Assert.Equal(RequirementType.Any, featureDefinition.RequirementType); + + featureDefinition = await featureDefinitionProvider.GetFeatureDefinitionAsync(Features.OffTestFeature); + + Assert.NotNull(featureDefinition); + + Assert.Empty(featureDefinition.EnabledFor); + + featureDefinition = await featureDefinitionProvider.GetFeatureDefinitionAsync(Features.AnyFilterFeature); + + Assert.NotNull(featureDefinition); + + Assert.NotEmpty(featureDefinition.EnabledFor); + + Assert.Equal(RequirementType.Any, featureDefinition.RequirementType); + + featureDefinition = await featureDefinitionProvider.GetFeatureDefinitionAsync(Features.AllFilterFeature); + + Assert.NotNull(featureDefinition); + + Assert.NotEmpty(featureDefinition.EnabledFor); + + Assert.Equal(RequirementType.All, featureDefinition.RequirementType); + + featureDefinition = await featureDefinitionProvider.GetFeatureDefinitionAsync(Features.ConditionalFeature); + + Assert.NotNull(featureDefinition); + + Assert.NotEmpty(featureDefinition.EnabledFor); + + filterConfig = featureDefinition.EnabledFor.First(); + + Assert.Equal("Test", filterConfig.Name); + + Assert.Equal("V1", filterConfig.Parameters["P1"]); + } + + [Fact] + public async Task ReadsMicrosoftFeatureManagementSchemaIfAny() + { + string json = @" + { + ""AllowedHosts"": ""*"", + ""feature_management"": { + ""feature_flags"": [ + { + ""id"": ""FeatureX"", + ""enabled"": true + } + ] + }, + ""FeatureManagement"": { + ""FeatureY"": true + } + }"; + + var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + + IConfiguration config = new ConfigurationBuilder().AddJsonStream(stream).Build(); + + var services = new ServiceCollection(); + + services.AddSingleton(config) + .AddFeatureManagement(); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + IFeatureManager featureManager = serviceProvider.GetRequiredService(); + + Assert.True(await featureManager.IsEnabledAsync("FeatureX")); + + Assert.False(await featureManager.IsEnabledAsync("FeatureY")); + } + + [Fact] + public async Task ReadsTopLevelConfiguration() + { + string json = @" + { + ""AllowedHosts"": ""*"", + ""FeatureManagement"": { + ""feature_flags"": [ + { + ""id"": ""FeatureX"", + ""enabled"": true + } + ] + } + }"; + + var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + + IConfiguration config = new ConfigurationBuilder().AddJsonStream(stream).Build(); + + var services = new ServiceCollection(); + + services.AddFeatureManagement(config.GetSection("FeatureManagement")); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + IFeatureManager featureManager = serviceProvider.GetRequiredService(); + + Assert.False(await featureManager.IsEnabledAsync("feature_flags")); + + Assert.True(await featureManager.IsEnabledAsync("FeatureX")); + + json = @" + { + ""AllowedHosts"": ""*"", + ""FeatureManagement"": { + ""feature_flags"": true + } + }"; + + stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + + config = new ConfigurationBuilder().AddJsonStream(stream).Build(); + + services = new ServiceCollection(); + + services.AddFeatureManagement(config.GetSection("FeatureManagement")); + + serviceProvider = services.BuildServiceProvider(); + + featureManager = serviceProvider.GetRequiredService(); + + Assert.True(await featureManager.IsEnabledAsync("feature_flags")); + + json = @" + { + ""AllowedHosts"": ""*"", + ""FeatureManagement"": { + ""feature_flags"": { + ""EnabledFor"": [ + { + ""Name"": ""AlwaysOn"" + } + ] + } + } + }"; + + stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + + config = new ConfigurationBuilder().AddJsonStream(stream).Build(); + + services = new ServiceCollection(); + + services.AddFeatureManagement(config.GetSection("FeatureManagement")); + + serviceProvider = services.BuildServiceProvider(); + + featureManager = serviceProvider.GetRequiredService(); + + Assert.True(await featureManager.IsEnabledAsync("feature_flags")); + } + } +} + diff --git a/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj b/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj index 60ba9ba4..ebf99ad6 100644 --- a/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj +++ b/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj @@ -46,6 +46,9 @@ Always + + Always + From e64f2c8639833655f3de2873f4b0f4b630d75943 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Wed, 21 Feb 2024 13:06:27 +0800 Subject: [PATCH 2/6] do not support root fall back for MS schema & remove EnsureInit --- .../ConfigurationFeatureDefinitionProvider.cs | 93 +++---------------- .../MicrosoftFeatureManagementSchema.cs | 83 ----------------- 2 files changed, 13 insertions(+), 163 deletions(-) diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 9d079e05..6686b694 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -26,9 +26,7 @@ public sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionP private readonly ConcurrentDictionary _definitions; private IDisposable _changeSubscription; private int _stale = 0; - private long _initialized = 0; private bool _microsoftFeatureFlagSchemaEnabled; - private readonly object _lock = new object(); /// /// Creates a configuration feature definition provider. @@ -42,6 +40,19 @@ public ConfigurationFeatureDefinitionProvider(IConfiguration configuration) _changeSubscription = ChangeToken.OnChange( () => _configuration.GetReloadToken(), () => _stale = 1); + + IConfiguration MicrosoftFeatureManagementConfigurationSection = _configuration + .GetChildren() + .FirstOrDefault(section => + string.Equals( + section.Key, + MicrosoftFeatureManagementFields.FeatureManagementSectionName, + StringComparison.OrdinalIgnoreCase)); + + if (MicrosoftFeatureManagementConfigurationSection != null) + { + _microsoftFeatureFlagSchemaEnabled = true; + } } /// @@ -81,8 +92,6 @@ public Task GetFeatureDefinitionAsync(string featureName) throw new ArgumentException($"The value '{ConfigurationPath.KeyDelimiter}' is not allowed in the feature name.", nameof(featureName)); } - EnsureInit(); - if (Interlocked.Exchange(ref _stale, 0) != 0) { _definitions.Clear(); @@ -106,8 +115,6 @@ public Task GetFeatureDefinitionAsync(string featureName) public async IAsyncEnumerable GetAllFeatureDefinitionsAsync() #pragma warning restore CS1998 { - EnsureInit(); - if (Interlocked.Exchange(ref _stale, 0) != 0) { _definitions.Clear(); @@ -130,55 +137,6 @@ public async IAsyncEnumerable GetAllFeatureDefinitionsAsync() } } - private void EnsureInit() - { - if (_initialized == 0) - { - bool hasMicrosoftFeatureFlagSchema = false; - - IConfiguration featureManagementConfigurationSection = _configuration - .GetChildren() - .FirstOrDefault(section => - string.Equals( - section.Key, - MicrosoftFeatureManagementFields.FeatureManagementSectionName, - StringComparison.OrdinalIgnoreCase)); - - if (featureManagementConfigurationSection != null) - { - hasMicrosoftFeatureFlagSchema = true; - } - else - { - featureManagementConfigurationSection = _configuration - .GetChildren() - .FirstOrDefault(section => - string.Equals( - section.Key, - ConfigurationFields.FeatureManagementSectionName, - StringComparison.OrdinalIgnoreCase)); - } - - if (featureManagementConfigurationSection == null && RootConfigurationFallbackEnabled) - { - featureManagementConfigurationSection = _configuration; - - hasMicrosoftFeatureFlagSchema = featureManagementConfigurationSection != null && - HasMicrosoftFeatureFlagSchema(featureManagementConfigurationSection); - } - - lock (_lock) - { - if (Interlocked.Read(ref _initialized) == 0) - { - _microsoftFeatureFlagSchemaEnabled = hasMicrosoftFeatureFlagSchema; - - Interlocked.Exchange(ref _initialized, 1); - } - } - } - } - private FeatureDefinition ReadFeatureDefinition(string featureName) { IConfigurationSection configuration = GetFeatureDefinitionSections() @@ -452,30 +410,5 @@ private IEnumerable GetFeatureDefinitionSections() return featureManagementConfigurationSection.GetChildren(); } - - private static bool HasMicrosoftFeatureFlagSchema(IConfiguration featureManagementConfiguration) - { - IConfigurationSection featureFlagsConfigurationSection = featureManagementConfiguration - .GetChildren() - .FirstOrDefault(section => - string.Equals( - section.Key, - MicrosoftFeatureManagementFields.FeatureFlagsSectionName, - StringComparison.OrdinalIgnoreCase)); - - if (featureFlagsConfigurationSection != null) - { - if (!string.IsNullOrEmpty(featureFlagsConfigurationSection.Value)) - { - return false; - } - - IEnumerable featureFlagsChildren = featureFlagsConfigurationSection.GetChildren(); - - return featureFlagsChildren.Any() && featureFlagsChildren.All(section => int.TryParse(section.Key, out int _)); - } - - return false; - } } } diff --git a/tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs b/tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs index 852e1dda..869d3399 100644 --- a/tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs +++ b/tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs @@ -104,89 +104,6 @@ public async Task ReadsMicrosoftFeatureManagementSchemaIfAny() Assert.False(await featureManager.IsEnabledAsync("FeatureY")); } - - [Fact] - public async Task ReadsTopLevelConfiguration() - { - string json = @" - { - ""AllowedHosts"": ""*"", - ""FeatureManagement"": { - ""feature_flags"": [ - { - ""id"": ""FeatureX"", - ""enabled"": true - } - ] - } - }"; - - var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); - - IConfiguration config = new ConfigurationBuilder().AddJsonStream(stream).Build(); - - var services = new ServiceCollection(); - - services.AddFeatureManagement(config.GetSection("FeatureManagement")); - - ServiceProvider serviceProvider = services.BuildServiceProvider(); - - IFeatureManager featureManager = serviceProvider.GetRequiredService(); - - Assert.False(await featureManager.IsEnabledAsync("feature_flags")); - - Assert.True(await featureManager.IsEnabledAsync("FeatureX")); - - json = @" - { - ""AllowedHosts"": ""*"", - ""FeatureManagement"": { - ""feature_flags"": true - } - }"; - - stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); - - config = new ConfigurationBuilder().AddJsonStream(stream).Build(); - - services = new ServiceCollection(); - - services.AddFeatureManagement(config.GetSection("FeatureManagement")); - - serviceProvider = services.BuildServiceProvider(); - - featureManager = serviceProvider.GetRequiredService(); - - Assert.True(await featureManager.IsEnabledAsync("feature_flags")); - - json = @" - { - ""AllowedHosts"": ""*"", - ""FeatureManagement"": { - ""feature_flags"": { - ""EnabledFor"": [ - { - ""Name"": ""AlwaysOn"" - } - ] - } - } - }"; - - stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); - - config = new ConfigurationBuilder().AddJsonStream(stream).Build(); - - services = new ServiceCollection(); - - services.AddFeatureManagement(config.GetSection("FeatureManagement")); - - serviceProvider = services.BuildServiceProvider(); - - featureManager = serviceProvider.GetRequiredService(); - - Assert.True(await featureManager.IsEnabledAsync("feature_flags")); - } } } From 6ae3362d6019c13c90eb6c002eee4172765e503d Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Wed, 21 Feb 2024 13:21:15 +0800 Subject: [PATCH 3/6] update --- .../ConfigurationFeatureDefinitionProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 6686b694..4814b7bf 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -389,13 +389,13 @@ private IEnumerable GetFeatureDefinitionSections() if (featureManagementConfigurationSection == null) { - if (RootConfigurationFallbackEnabled) + if (RootConfigurationFallbackEnabled && !_microsoftFeatureFlagSchemaEnabled) { featureManagementConfigurationSection = _configuration; } else { - Logger?.LogDebug($"No configuration section named '{ConfigurationFields.FeatureManagementSectionName}' was found."); + Logger?.LogDebug($"No feature management configuration section was found."); return Enumerable.Empty(); } From 91385f0b09bd727910af899e0004be9d834c4c47 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Wed, 21 Feb 2024 13:40:48 +0800 Subject: [PATCH 4/6] rename variable --- .../ConfigurationFeatureDefinitionProvider.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 4814b7bf..0995018d 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -26,7 +26,7 @@ public sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionP private readonly ConcurrentDictionary _definitions; private IDisposable _changeSubscription; private int _stale = 0; - private bool _microsoftFeatureFlagSchemaEnabled; + private readonly bool _microsoftFeatureManagementSchemaEnabled; /// /// Creates a configuration feature definition provider. @@ -51,7 +51,7 @@ public ConfigurationFeatureDefinitionProvider(IConfiguration configuration) if (MicrosoftFeatureManagementConfigurationSection != null) { - _microsoftFeatureFlagSchemaEnabled = true; + _microsoftFeatureManagementSchemaEnabled = true; } } @@ -152,7 +152,7 @@ private FeatureDefinition ReadFeatureDefinition(string featureName) private FeatureDefinition ReadFeatureDefinition(IConfigurationSection configurationSection) { - if (_microsoftFeatureFlagSchemaEnabled) + if (_microsoftFeatureManagementSchemaEnabled) { return ParseMicrosoftFeatureDefinition(configurationSection); } @@ -360,7 +360,7 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection private string GetFeatureName(IConfigurationSection section) { - if (_microsoftFeatureFlagSchemaEnabled) + if (_microsoftFeatureManagementSchemaEnabled) { return section[MicrosoftFeatureManagementFields.Id]; } @@ -382,14 +382,14 @@ private IEnumerable GetFeatureDefinitionSections() .FirstOrDefault(section => string.Equals( section.Key, - _microsoftFeatureFlagSchemaEnabled ? + _microsoftFeatureManagementSchemaEnabled ? MicrosoftFeatureManagementFields.FeatureManagementSectionName : ConfigurationFields.FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)); if (featureManagementConfigurationSection == null) { - if (RootConfigurationFallbackEnabled && !_microsoftFeatureFlagSchemaEnabled) + if (RootConfigurationFallbackEnabled && !_microsoftFeatureManagementSchemaEnabled) { featureManagementConfigurationSection = _configuration; } @@ -401,7 +401,7 @@ private IEnumerable GetFeatureDefinitionSections() } } - if (_microsoftFeatureFlagSchemaEnabled) + if (_microsoftFeatureManagementSchemaEnabled) { IConfigurationSection featureFlagsSection = featureManagementConfigurationSection.GetSection(MicrosoftFeatureManagementFields.FeatureFlagsSectionName); From 43437fe921af69699a206832281482060a00725c Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Thu, 22 Feb 2024 09:34:34 +0800 Subject: [PATCH 5/6] update .NET FM schema --- ...eatureManagement.Dotnet.v1.0.0.schema.json | 180 +++++++++--------- 1 file changed, 95 insertions(+), 85 deletions(-) diff --git a/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json b/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json index 430a68b5..9da8112c 100644 --- a/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json +++ b/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json @@ -3,100 +3,110 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "title": "The .NET Feature Management Schema", - "required": [], - "patternProperties": { - "^[^:]*$": { - "description": "Declares a feature flag.", - "anyOf": [ - { - "type": "boolean", - "title": "On/Off Feature Flag", - "description": "A feature flag that always returns the same value." - }, - { - "type": "object", - "title": "Conditional Feature Flag", - "description": "A feature flag which value is dynamic based on a set of feature filters", - "required": [ - "EnabledFor" - ], - "properties": { - "RequirementType": { - "type": "string", - "title": "Requirement Type", - "description": "Determines whether any or all registered feature filters must be enabled for the feature to be considered enabled.", - "enum": [ - "Any", - "All" - ], - "default": "Any" + "required": [ + "FeatureManagement" + ], + "properties":{ + "FeatureManagement": { + "type": "object", + "title": "Feature Management", + "description": "Declares feature management configuration.", + "required": [], + "patternProperties": { + "^[^:]*$": { + "description": "Declares a feature flag.", + "anyOf": [ + { + "type": "boolean", + "title": "On/Off Feature Flag", + "description": "A feature flag that always returns the same value." }, - "EnabledFor": { - "oneOf": [ - { - "type": "array", - "title": "Feature Filter Collection", - "description": "Feature filters that are evaluated to conditionally enable the flag.", - "items": { - "type": "object", - "title": "Feature Filter Declaration", - "required": [ - "Name" - ], - "properties": { - "Name": { - "type": "string", - "title": "Feature Filter Name", - "description": "The name used to refer to and require a feature filter.", - "default": "", - "examples": [ - "Percentage", - "TimeWindow" - ], - "pattern": "^[^:]*$" - }, - "Parameters": { + { + "type": "object", + "title": "Conditional Feature Flag", + "description": "A feature flag which value is dynamic based on a set of feature filters", + "required": [ + "EnabledFor" + ], + "properties": { + "RequirementType": { + "type": "string", + "title": "Requirement Type", + "description": "Determines whether any or all registered feature filters must be enabled for the feature to be considered enabled.", + "enum": [ + "Any", + "All" + ], + "default": "Any" + }, + "EnabledFor": { + "oneOf": [ + { + "type": "array", + "title": "Feature Filter Collection", + "description": "Feature filters that are evaluated to conditionally enable the flag.", + "items": { "type": "object", - "title": "Feature Filter Parameters", - "description": "Custom parameters for a given feature filter. A feature filter can require any set of parameters of any type.", - "required": [], - "patternProperties": { - "^.*$": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - }, - { - "type": "object" - }, - { - "type": "number" - }, - { - "type": "array" - }, - { - "type": "boolean" + "title": "Feature Filter Declaration", + "required": [ + "Name" + ], + "properties": { + "Name": { + "type": "string", + "title": "Feature Filter Name", + "description": "The name used to refer to and require a feature filter.", + "default": "", + "examples": [ + "Percentage", + "TimeWindow" + ], + "pattern": "^[^:]*$" + }, + "Parameters": { + "type": "object", + "title": "Feature Filter Parameters", + "description": "Custom parameters for a given feature filter. A feature filter can require any set of parameters of any type.", + "required": [], + "patternProperties": { + "^.*$": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + }, + { + "type": "object" + }, + { + "type": "number" + }, + { + "type": "array" + }, + { + "type": "boolean" + } + ] } - ] + } } } } + }, + { + "type": "boolean" } - } + ] }, - { - "type": "boolean" - } - ] - }, - "additionalProperties": false - } + "additionalProperties": false + } + } + ] } - ] + } } } } \ No newline at end of file From ee84ec9bc474c7622fcd5076727767596e23823d Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 23 Feb 2024 13:03:10 +0800 Subject: [PATCH 6/6] re-add schema link --- schemas/FeatureManagement.Dotnet.v1.0.0.schema.json | 2 +- .../MicrosoftFeatureManagementFields.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json b/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json index 1047ee58..3b879f0b 100644 --- a/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json +++ b/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json @@ -109,4 +109,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs b/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs index ce776897..50351526 100644 --- a/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs +++ b/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs @@ -4,6 +4,8 @@ namespace Microsoft.FeatureManagement { + // + // Microsoft Feature Management schema: https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureManagement.v1.0.0.schema.json internal static class MicrosoftFeatureManagementFields { public const string FeatureManagementSectionName = "feature_management";