From 101723a07651dcd84851e92f577af52cb54c3132 Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Tue, 6 Feb 2024 14:21:35 +0800
Subject: [PATCH 1/7] Add a constructor without FeatureManagementOption
parameter for FeatureManager (#363)
* add new constructor
* update
---
src/Microsoft.FeatureManagement/FeatureManager.cs | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs
index 55dde3f5..ef850a03 100644
--- a/src/Microsoft.FeatureManagement/FeatureManager.cs
+++ b/src/Microsoft.FeatureManagement/FeatureManager.cs
@@ -40,15 +40,14 @@ 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)
{
_filterMetadataCache = new ConcurrentDictionary();
_contextualFeatureFilterCache = new ConcurrentDictionary();
_featureDefinitionProvider = featureDefinitionProvider ?? throw new ArgumentNullException(nameof(featureDefinitionProvider));
- _options = options ?? throw new ArgumentNullException(nameof(options));
+ _options = options ?? new FeatureManagementOptions();
_featureFilters = Enumerable.Empty();
_sessionManagers = Enumerable.Empty();
}
From e968d52b1cbcd8ec19ee603c7aa421d7a6e4e1da Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Fri, 9 Feb 2024 15:27:31 +0800
Subject: [PATCH 2/7] Target on .NET 8.0 (#365)
* target on .NET 8.0
* remove file
* add net8.0 for Microsoft.FeatureManagement.AspNetCore
* update package version
* update
---
build/install-dotnet.ps1 | 6 ++--
...rosoft.FeatureManagement.AspNetCore.csproj | 2 +-
.../Tests.FeatureManagement.AspNetCore.csproj | 21 +++++++++-----
.../Tests.FeatureManagement.csproj | 28 ++++++++++++-------
4 files changed, 37 insertions(+), 20 deletions(-)
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/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj
index 99ebd339..69cacacf 100644
--- a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj
+++ b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj
@@ -11,7 +11,7 @@
- net6.0;net7.0
+ net6.0;net7.0;net8.0
true
false
..\..\build\Microsoft.FeatureManagement.snk
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/Tests.FeatureManagement.csproj b/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj
index 4d8697ad..60ba9ba4 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 @@
-
-
-
-
+
+
+
-
-
+
+
+
-
+
+
-
-
+
+
+
+
+
+
+
+
+
From 8c2dbc9c42282363d57a136135e40c118fdc8ffc Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Wed, 21 Feb 2024 11:27:03 +0800
Subject: [PATCH 3/7] Add feature management schema for main branch (#362)
* schema file added
* README update
* update
* update
* update README & remove the Microsoft schema file
* update readme
* update
---
README.md | 34 +++++-
...eatureManagement.Dotnet.v1.0.0.schema.json | 102 ++++++++++++++++++
.../ConfigurationFeatureDefinitionProvider.cs | 16 +--
3 files changed, 139 insertions(+), 13 deletions(-)
create mode 100644 schemas/FeatureManagement.Dotnet.v1.0.0.schema.json
diff --git a/README.md b/README.md
index f7434116..0081da9a 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,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
@@ -121,7 +123,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": [
@@ -144,6 +146,36 @@ A `RequirementType` of `All` changes the traversal. First, if there are no filte
In the above example, `FeatureW` specifies a `RequirementType` of `All`, meaning all of its filters must evaluate to true for the feature to be enabled. In this case, the feature will be enabled for 50% of users during the specified time window.
+#### 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/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json b/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json
new file mode 100644
index 00000000..430a68b5
--- /dev/null
+++ b/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json
@@ -0,0 +1,102 @@
+{
+ "definitions": {},
+ "$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"
+ },
+ "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
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
index 6e940760..4ec5d69c 100644
--- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
+++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
@@ -192,19 +192,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: {
@@ -214,9 +206,9 @@ We support
enabledFor: false
},
myAllRequiredFilterFeature: {
- requirementType: "all"
- enabledFor: [ "myFeatureFilter1", "myFeatureFilter2" ],
- },
+ requirementType: "all",
+ enabledFor: [{name: "myFeatureFilter1"}, {name: "myFeatureFilter2"}]
+ }
*/
From 0deeaa79cd2747c100920ae414652e125e441fdb Mon Sep 17 00:00:00 2001
From: Ross Grambo
Date: Wed, 21 Feb 2024 17:42:48 -0800
Subject: [PATCH 4/7] Updates test schema and adjusts title (#372)
---
schemas/FeatureManagement.Dotnet.v1.0.0.schema.json | 2 +-
.../ConfigurationFeatureDefinitionProvider.cs | 2 +-
tests/Tests.FeatureManagement/appsettings.json | 4 ++--
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json b/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json
index 430a68b5..8ea0f613 100644
--- a/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json
+++ b/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json
@@ -2,7 +2,7 @@
"definitions": {},
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
- "title": "The .NET Feature Management Schema",
+ "title": "A .NET Feature Management Configuration",
"required": [],
"patternProperties": {
"^[^:]*$": {
diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
index 4ec5d69c..de180158 100644
--- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
+++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
@@ -206,7 +206,7 @@ We support
enabledFor: false
},
myAllRequiredFilterFeature: {
- requirementType: "all",
+ requirementType: "All",
enabledFor: [{name: "myFeatureFilter1"}, {name: "myFeatureFilter2"}]
}
diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json
index 77aa687e..509d8ba8 100644
--- a/tests/Tests.FeatureManagement/appsettings.json
+++ b/tests/Tests.FeatureManagement/appsettings.json
@@ -10,7 +10,7 @@
"OnTestFeature": true,
"OffTestFeature": false,
"FeatureUsesFiltersWithDuplicatedAlias": {
- "RequirementType": "all",
+ "RequirementType": "All",
"EnabledFor": [
{
"Name": "DuplicatedFilterName"
@@ -145,7 +145,7 @@
]
},
"AllFilterFeature": {
- "RequirementType": "all",
+ "RequirementType": "All",
"EnabledFor": [
{
"Name": "Test",
From 2bae5aa5488b828f9b6dacda2d44f7033c9a5e5d Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Fri, 23 Feb 2024 13:10:54 +0800
Subject: [PATCH 5/7] Support Microsoft Feature Management schema for main
branch (#370)
* use snake case
* do not support root fall back for MS schema & remove EnsureInit
* update
* rename variable
* update .NET FM schema
* re-add schema link
---
...eatureManagement.Dotnet.v1.0.0.schema.json | 182 ++++++++--------
.../ConfigurationFeatureDefinitionProvider.cs | 132 ++++--------
...cs => MicrosoftFeatureManagementFields.cs} | 9 +-
.../FeatureManagement.cs | 196 ------------------
.../MicrosoftFeatureManagement.json | 77 +++++++
.../MicrosoftFeatureManagementSchema.cs | 109 ++++++++++
.../Tests.FeatureManagement.csproj | 3 +
7 files changed, 332 insertions(+), 376 deletions(-)
rename src/Microsoft.FeatureManagement/{MicrosoftFeatureFlagFields.cs => MicrosoftFeatureManagementFields.cs} (58%)
create mode 100644 tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json
create mode 100644 tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs
diff --git a/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json b/schemas/FeatureManagement.Dotnet.v1.0.0.schema.json
index 8ea0f613..3b879f0b 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": "A .NET 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"
+ "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
+}
diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
index de180158..c86113e0 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();
+ private readonly bool _microsoftFeatureManagementSchemaEnabled;
///
/// 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)
+ {
+ _microsoftFeatureManagementSchemaEnabled = 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,38 +137,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()
@@ -177,7 +152,7 @@ private FeatureDefinition ReadFeatureDefinition(string featureName)
private FeatureDefinition ReadFeatureDefinition(IConfigurationSection configurationSection)
{
- if (_microsoftFeatureFlagSchemaEnabled)
+ if (_microsoftFeatureManagementSchemaEnabled)
{
return ParseMicrosoftFeatureDefinition(configurationSection);
}
@@ -313,20 +288,9 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection
RequirementType requirementType = RequirementType.Any;
- IConfigurationSection conditions = configurationSection.GetSection(MicrosoftFeatureFlagFields.Conditions);
+ IConfigurationSection conditions = configurationSection.GetSection(MicrosoftFeatureManagementFields.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}'.");
- }
-
- string rawEnabled = configurationSection[MicrosoftFeatureFlagFields.Enabled];
+ string rawEnabled = configurationSection[MicrosoftFeatureManagementFields.Enabled];
bool enabled = false;
@@ -334,12 +298,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())
{
@@ -348,12 +323,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))
});
}
}
@@ -377,9 +352,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;
@@ -399,56 +374,33 @@ 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();
}
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;
- }
}
}
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/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..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 60ba9ba4..ebf99ad6 100644
--- a/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj
+++ b/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj
@@ -46,6 +46,9 @@
Always
+
+ Always
+
From b1c5f32141a04578c5ca5c2aa51f01cb47b0d41d Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Fri, 23 Feb 2024 13:55:19 +0800
Subject: [PATCH 6/7] add whitespace (#374)
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0081da9a..61e63032 100644
--- a/README.md
+++ b/README.md
@@ -150,7 +150,7 @@ In the above example, `FeatureW` specifies a `RequirementType` of `All`, meaning
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
+``` JavaScript
{
"feature_management": {
"feature_flags": [
From 7ebeb154f59da14c11f407d045c608efa9919610 Mon Sep 17 00:00:00 2001
From: zhiyuanliang
Date: Fri, 23 Feb 2024 14:01:08 +0800
Subject: [PATCH 7/7] update
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index b719d23d..538ac741 100644
--- a/README.md
+++ b/README.md
@@ -156,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": [