diff --git a/README.md b/README.md index 75a7ea6a..538ac741 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,8 @@ The feature management library supports appsettings.json as a feature flag sourc The `FeatureManagement` section of the json document is used by convention to load feature flag settings. In the section above, we see that we have provided three different features. Features define their feature filters using the `EnabledFor` property. In the feature filters for `FeatureT` we see `AlwaysOn`. This feature filter is built-in and if specified will always enable the feature. The `AlwaysOn` feature filter does not require any configuration, so it only has the `Name` property. `FeatureU` has no filters in its `EnabledFor` property and thus will never be enabled. Any functionality that relies on this feature being enabled will not be accessible as long as the feature filters remain empty. However, as soon as a feature filter is added that enables the feature it can begin working. `FeatureV` specifies a feature filter named `TimeWindow`. This is an example of a configurable feature filter. We can see in the example that the filter has a `Parameters` property. This is used to configure the filter. In this case, the start and end times for the feature to be active are configured. +The detailed schema of the `FeatureManagement` section can be found [here](./schemas/FeatureManagement.Dotnet.v1.0.0.schema.json). + **Advanced:** The usage of colon ':' in feature flag names is forbidden. #### On/Off Declaration @@ -126,7 +128,7 @@ The `RequirementType` property of a feature flag is used to determine if the fil A `RequirementType` of `All` changes the traversal. First, if there are no filters, the feature will be disabled. Then, the feature-filters are traversed until one of the filters decides that the feature should be disabled. If no filter indicates that the feature should be disabled, then it will be considered enabled. -``` +``` JavaScript "FeatureW": { "RequirementType": "All", "EnabledFor": [ @@ -154,7 +156,7 @@ In the above example, `FeatureW` specifies a `RequirementType` of `All`, meaning `Status` is an optional property of a feature flag that controls how a flag's enabled state is evaluated. By default, the status of a flag is `Conditional`, meaning that feature filters should be evaluated to determine if the flag is enabled. If the `Status` of a flag is set to `Disabled` then feature filters are not evaluated and the flag is always considered to be disabled. -``` +``` JavaScript "FeatureX": { "Status": "Disabled", "EnabledFor": [ @@ -167,6 +169,36 @@ In the above example, `FeatureW` specifies a `RequirementType` of `All`, meaning In this example, even though the `AlwaysOn` filter would normally always make the feature enabled, the `Status` property is set to `Disabled`, so this feature will always be disabled. +#### Microsoft Feature Management Schema + +The feature management library also supports the usage of the [`Microsoft Feature Management schema`](https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureManagement.v1.0.0.schema.json) to declare feature flags. This schema is language agnostic in origin and is supported by all Microsoft feature management libraries. + +``` JavaScript +{ + "feature_management": { + "feature_flags": [ + { + "id": "FeatureT", + "enabled": true, + "conditions": { + "client_filters": [ + { + "name": "Microsoft.TimeWindow", + "parameters": { + "Start": "Mon, 01 May 2023 13:59:59 GMT", + "End": "Sat, 01 July 2023 00:00:00 GMT" + } + } + ] + } + } + ] + } +} +``` + +**Note:** If the `feature_management` section can be found in the configuration, the `FeatureManagement` section will be ignored. + ## Consumption The basic form of feature management is checking if a feature flag is enabled and then performing actions based on the result. This is done through the `IFeatureManager`'s `IsEnabledAsync` method. diff --git a/build/install-dotnet.ps1 b/build/install-dotnet.ps1 index 11552a78..b5fb7a5d 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 \ No newline at end of file diff --git a/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json b/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json new file mode 100644 index 00000000..3b879f0b --- /dev/null +++ b/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json @@ -0,0 +1,112 @@ +{ + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "A .NET Feature Management Configuration", + "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." + }, + { + "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 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" + } + ] + }, + "additionalProperties": false + } + } + ] + } + } + } + } +} diff --git a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj index b41a9e3d..8c14ebec 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj +++ b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj @@ -12,7 +12,7 @@ - net6.0;net7.0 + net6.0;net7.0;net8.0 true false ..\..\build\Microsoft.FeatureManagement.snk diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index d87a135b..c823a1d0 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -27,9 +27,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(); + private readonly bool _microsoftFeatureManagementSchemaEnabled; const string ParseValueErrorString = "Invalid setting '{0}' with value '{1}' for feature '{2}'."; @@ -45,6 +43,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) + { + _microsoftFeatureManagementSchemaEnabled = true; + } } /// @@ -84,8 +95,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(); @@ -109,8 +118,6 @@ public Task GetFeatureDefinitionAsync(string featureName) public async IAsyncEnumerable GetAllFeatureDefinitionsAsync() #pragma warning restore CS1998 { - EnsureInit(); - if (Interlocked.Exchange(ref _stale, 0) != 0) { _definitions.Clear(); @@ -133,38 +140,6 @@ public async IAsyncEnumerable GetAllFeatureDefinitionsAsync() } } - private void EnsureInit() - { - if (_initialized == 0) - { - IConfiguration 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); - - lock (_lock) - { - if (Interlocked.Read(ref _initialized) == 0) - { - _microsoftFeatureFlagSchemaEnabled = hasMicrosoftFeatureFlagSchema; - - Interlocked.Exchange(ref _initialized, 1); - } - } - } - } - private FeatureDefinition ReadFeatureDefinition(string featureName) { IConfigurationSection configuration = GetFeatureDefinitionSections() @@ -180,7 +155,7 @@ private FeatureDefinition ReadFeatureDefinition(string featureName) private FeatureDefinition ReadFeatureDefinition(IConfigurationSection configurationSection) { - if (_microsoftFeatureFlagSchemaEnabled) + if (_microsoftFeatureManagementSchemaEnabled) { return ParseMicrosoftFeatureDefinition(configurationSection); } @@ -195,19 +170,11 @@ private FeatureDefinition ParseFeatureDefinition(IConfigurationSection configura We support myFeature: { - enabledFor: [ "myFeatureFilter1", "myFeatureFilter2" ] + enabledFor: [{name: "myFeatureFilter1"}, {name: "myFeatureFilter2"}] }, myDisabledFeature: { enabledFor: [ ] }, - myFeature2: { - enabledFor: "myFeatureFilter1;myFeatureFilter2" - }, - myDisabledFeature2: { - enabledFor: "" - }, - myFeature3: "myFeatureFilter1;myFeatureFilter2", - myDisabledFeature3: "", myAlwaysEnabledFeature: true, myAlwaysDisabledFeature: false // removing this line would be the same as setting it to false myAlwaysEnabledFeature2: { @@ -217,9 +184,9 @@ We support enabledFor: false }, myAllRequiredFilterFeature: { - requirementType: "all" - enabledFor: [ "myFeatureFilter1", "myFeatureFilter2" ], - }, + requirementType: "All", + enabledFor: [{name: "myFeatureFilter1"}, {name: "myFeatureFilter2"}] + } */ @@ -452,25 +419,29 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection bool enabled = false; - IConfigurationSection conditions = configurationSection.GetSection(MicrosoftFeatureFlagFields.Conditions); - - string rawRequirementType = conditions[MicrosoftFeatureFlagFields.RequirementType]; + IConfigurationSection conditions = configurationSection.GetSection(MicrosoftFeatureManagementFields.Conditions); - string rawEnabled = configurationSection[MicrosoftFeatureFlagFields.Enabled]; + string rawEnabled = configurationSection[MicrosoftFeatureManagementFields.Enabled]; if (!string.IsNullOrEmpty(rawEnabled)) { - enabled = ParseBool(featureName, rawEnabled, MicrosoftFeatureFlagFields.Enabled); - } - - if (!string.IsNullOrEmpty(rawRequirementType)) - { - requirementType = ParseEnum(featureName, rawRequirementType, MicrosoftFeatureFlagFields.RequirementType); + enabled = ParseBool(featureName, rawEnabled, MicrosoftFeatureManagementFields.Enabled); } 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()) { @@ -479,12 +450,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)) }); } } @@ -508,9 +479,9 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection private string GetFeatureName(IConfigurationSection section) { - if (_microsoftFeatureFlagSchemaEnabled) + if (_microsoftFeatureManagementSchemaEnabled) { - return section[MicrosoftFeatureFlagFields.Id]; + return section[MicrosoftFeatureManagementFields.Id]; } return section.Key; @@ -530,26 +501,28 @@ private IEnumerable GetFeatureDefinitionSections() .FirstOrDefault(section => string.Equals( section.Key, - ConfigurationFields.FeatureManagementSectionName, + _microsoftFeatureManagementSchemaEnabled ? + MicrosoftFeatureManagementFields.FeatureManagementSectionName : + ConfigurationFields.FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)); if (featureManagementConfigurationSection == null) { - if (RootConfigurationFallbackEnabled) + if (RootConfigurationFallbackEnabled && !_microsoftFeatureManagementSchemaEnabled) { 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(); } } - if (_microsoftFeatureFlagSchemaEnabled) + if (_microsoftFeatureManagementSchemaEnabled) { - IConfigurationSection featureFlagsSection = featureManagementConfigurationSection.GetSection(MicrosoftFeatureFlagFields.FeatureFlagsSectionName); + IConfigurationSection featureFlagsSection = featureManagementConfigurationSection.GetSection(MicrosoftFeatureManagementFields.FeatureFlagsSectionName); return featureFlagsSection.GetChildren(); } @@ -557,31 +530,6 @@ private IEnumerable GetFeatureDefinitionSections() return featureManagementConfigurationSection.GetChildren(); } - private static bool HasMicrosoftFeatureFlagSchema(IConfiguration featureManagementConfiguration) - { - IConfigurationSection featureFlagsConfigurationSection = featureManagementConfiguration - .GetChildren() - .FirstOrDefault(section => - string.Equals( - section.Key, - MicrosoftFeatureFlagFields.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; - } - private T ParseEnum(string feature, string rawValue, string fieldKeyword) where T: struct, Enum { diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 0c13eea9..23599eb0 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -48,13 +48,12 @@ private class ConfigurationCacheItem /// The provider of feature flag definitions. /// Options controlling the behavior of the feature manager. /// Thrown if is null. - /// Thrown if is null. public FeatureManager( IFeatureDefinitionProvider featureDefinitionProvider, - FeatureManagementOptions options) + FeatureManagementOptions options = null) { _featureDefinitionProvider = featureDefinitionProvider ?? throw new ArgumentNullException(nameof(featureDefinitionProvider)); - _options = options ?? throw new ArgumentNullException(nameof(options)); + _options = options ?? new FeatureManagementOptions(); _filterMetadataCache = new ConcurrentDictionary(); _contextualFeatureFilterCache = new ConcurrentDictionary(); _featureFilters = Enumerable.Empty(); diff --git a/src/Microsoft.FeatureManagement/MicrosoftFeatureFlagFields.cs b/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs similarity index 58% rename from src/Microsoft.FeatureManagement/MicrosoftFeatureFlagFields.cs rename to src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs index 21286e5c..50351526 100644 --- a/src/Microsoft.FeatureManagement/MicrosoftFeatureFlagFields.cs +++ b/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs @@ -5,13 +5,14 @@ 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 + // 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 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.AspNetCore/Tests.FeatureManagement.AspNetCore.csproj b/tests/Tests.FeatureManagement.AspNetCore/Tests.FeatureManagement.AspNetCore.csproj index c798e1ad..b14bbfdb 100644 --- a/tests/Tests.FeatureManagement.AspNetCore/Tests.FeatureManagement.AspNetCore.csproj +++ b/tests/Tests.FeatureManagement.AspNetCore/Tests.FeatureManagement.AspNetCore.csproj @@ -1,7 +1,7 @@  - net6.0;net7.0 + net6.0;net7.0;net8.0 false 8.0 True @@ -14,20 +14,27 @@ - - - - + + + + - + + - + + + + + + + diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index 97c13e68..850dae60 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -109,202 +109,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..869d3399 --- /dev/null +++ b/tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs @@ -0,0 +1,109 @@ +// 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")); + } + } +} + diff --git a/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj b/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj index 4d8697ad..ebf99ad6 100644 --- a/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj +++ b/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj @@ -1,7 +1,7 @@  - net48;net6.0;net7.0 + net48;net6.0;net7.0;net8.0 false 9.0 True @@ -9,25 +9,33 @@ - - - - + + + - - + + + - + + - - + + + + + + + + + @@ -38,6 +46,9 @@ Always + + Always + diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index 612f052f..241353e1 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -50,7 +50,7 @@ ] }, "FeatureUsesFiltersWithDuplicatedAlias": { - "RequirementType": "all", + "RequirementType": "All", "EnabledFor": [ { "Name": "DuplicatedFilterName" @@ -185,7 +185,7 @@ ] }, "AllFilterFeature": { - "RequirementType": "all", + "RequirementType": "All", "EnabledFor": [ { "Name": "Test",