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 01/25] 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 02/25] 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 03/25] 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 04/25] 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 05/25] 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 06/25] 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 4b2fc26ead89bd0c3b9661b714be5ef9972fc605 Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Wed, 28 Feb 2024 10:20:51 +0800
Subject: [PATCH 07/25] version bump to 3.2.0 (#377)
---
.../Microsoft.FeatureManagement.AspNetCore.csproj | 4 ++--
.../Microsoft.FeatureManagement.csproj | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj
index 69cacacf..7c65cfbe 100644
--- a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj
+++ b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj
@@ -4,8 +4,8 @@
3
- 1
- 1
+ 2
+ 0
diff --git a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj
index 88ce8621..adbb2168 100644
--- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj
+++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj
@@ -4,8 +4,8 @@
3
- 1
- 1
+ 2
+ 0
From 7f5c0956d17efb7d4da1261348811a853affc8d9 Mon Sep 17 00:00:00 2001
From: Ross Grambo
Date: Wed, 6 Mar 2024 14:05:19 -0800
Subject: [PATCH 08/25] Adds .Telemetry.ApplicationInsights.AspNetCore to build
packages
---
pack.ps1 | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/pack.ps1 b/pack.ps1
index efe1835e..12274bbd 100644
--- a/pack.ps1
+++ b/pack.ps1
@@ -22,7 +22,8 @@ $targetProjects = @(
"Microsoft.FeatureManagement",
"Microsoft.FeatureManagement.AspNetCore",
- "Microsoft.FeatureManagement.Telemetry.ApplicationInsights"
+ "Microsoft.FeatureManagement.Telemetry.ApplicationInsights",
+ "Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore"
)
# Create the log directory.
From d0c50061a926a0d0548e9e7e0887e79f169a899d Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Sun, 10 Mar 2024 22:17:42 +0800
Subject: [PATCH 09/25] Support Microsoft Feature Management schema for preview
branch (#375)
* support microsoft feature flag schema v2
* add more testcases
* add empty lines
* add schema & update README
* update schema
---
README.md | 100 ++++--
...eatureManagement.Dotnet.v2.0.0.schema.json | 313 ++++++++++++++++++
.../ConfigurationFeatureDefinitionProvider.cs | 135 +++++++-
.../ConfigurationFields.cs | 2 +-
.../MicrosoftFeatureManagementFields.cs | 25 ++
.../FeatureManagement.cs | 1 -
.../MicrosoftFeatureManagement.json | 274 +++++++++++++++
.../MicrosoftFeatureManagementSchema.cs | 301 +++++++++++++++++
.../Tests.FeatureManagement/appsettings.json | 8 -
9 files changed, 1120 insertions(+), 39 deletions(-)
create mode 100644 schemas/FeatureManagement.Dotnet.v2.0.0.schema.json
diff --git a/README.md b/README.md
index 538ac741..7298e9c7 100644
--- a/README.md
+++ b/README.md
@@ -96,7 +96,7 @@ 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).
+The detailed schema of the `FeatureManagement` section can be found [here](./schemas/FeatureManagement.Dotnet.v2.0.0.schema.json).
**Advanced:** The usage of colon ':' in feature flag names is forbidden.
@@ -784,17 +784,60 @@ variantConfiguration.Bind(settings);
The variant returned is dependent on the user currently being evaluated, and that information is obtained from an instance of `TargetingContext`. This context can either be passed in when calling `GetVariantAsync` or it can be automatically retrieved from an implementation of [`ITargetingContextAccessor`](#itargetingcontextaccessor) if one is registered.
-### Defining Variants
+### Variant Feature Flag Declaration
+
+Compared to normal feature flags, variant feature flags have two additional properties: `Variants` and `Allocation`. The `Variants` property is an array that contains the variants defined for this feature. The `Allocation` property defines how these variants should be allocated for the feature. Just like declaring normal feature flags, you can set up variant feature flags in a json file. Here is an example of a variant feature flag.
+
+``` javascript
+
+{
+ "FeatureManagement":
+ {
+ "MyVariantFeatureFlag":
+ {
+ "Allocation": {
+ "DefaultWhenEnabled": "Small",
+ "Group": [
+ {
+ "Variant": "Big",
+ "Groups": [
+ "Ring1"
+ ]
+ }
+ ]
+ },
+ "Variants": [
+ {
+ "Name": "Big"
+ },
+ {
+ "Name": "Small"
+ }
+ ],
+ "EnabledFor": [
+ {
+ "Name": "AlwaysOn"
+ }
+ ]
+ }
+ }
+}
+
+```
+
+For more details about how to configure variant feature flags, please see [here](./schemas/FeatureManagement.Dotnet.v2.0.0.schema.json).
+
+#### Defining Variants
Each variant has two properties: a name and a configuration. The name is used to refer to a specific variant, and the configuration is the value of that variant. The configuration can be set using either the `ConfigurationReference` or `ConfigurationValue` properties. `ConfigurationReference` is a string path that references a section of the current configuration that contains the feature flag declaration. `ConfigurationValue` is an inline configuration that can be a string, number, boolean, or configuration object. If both are specified, `ConfigurationValue` is used. If neither are specified, the returned variant's `Configuration` property will be null.
A list of all possible variants is defined for each feature under the `Variants` property.
-```
+``` javascript
{
"FeatureManagement":
{
- "MyFlag":
+ "MyVariantFeatureFlag":
{
"Variants": [
{
@@ -815,14 +858,25 @@ A list of all possible variants is defined for each feature under the `Variants`
]
}
}
+
+ "ShoppingCart": {
+ "Big": {
+ "Size": 600,
+ "Color": "green"
+ },
+ "Small": {
+ "Size": 300,
+ "Color": "gray"
+ }
+ }
}
```
-### Allocating Variants
+#### Allocating Variants
The process of allocating a feature's variants is determined by the `Allocation` property of the feature.
-```
+``` javascript
"Allocation": {
"DefaultWhenEnabled": "Small",
"DefaultWhenDisabled": "Small",
@@ -880,36 +934,38 @@ If the feature is enabled, the feature manager will check the `User`, `Group`, a
Allocation logic is similar to the [Microsoft.Targeting](./README.md#MicrosoftTargeting) feature filter, but there are some parameters that are present in targeting that aren't in allocation, and vice versa. The outcomes of targeting and allocation are not related.
-### Overriding Enabled State with a Variant
+#### Overriding Enabled State with a Variant
You can use variants to override the enabled state of a feature flag. This gives variants an opportunity to extend the evaluation of a feature flag. If a caller is checking whether a flag that has variants is enabled, the feature manager will check if the variant assigned to the current user is set up to override the result. This is done using the optional variant property `StatusOverride`. By default, this property is set to `None`, which means the variant doesn't affect whether the flag is considered enabled or disabled. Setting `StatusOverride` to `Enabled` allows the variant, when chosen, to override a flag to be enabled. Setting `StatusOverride` to `Disabled` provides the opposite functionality, therefore disabling the flag when the variant is chosen. A feature with a `Status` of `Disabled` cannot be overridden.
If you are using a feature flag with binary variants, the `StatusOverride` property can be very helpful. It allows you to continue using APIs like `IsEnabledAsync` and `FeatureGateAttribute` in your application, all while benefiting from the new features that come with variants, such as percentile allocation and seed.
-```
+``` javascript
"Allocation": {
- "Percentile": [{
- "Variant": "On",
- "From": 10,
- "To": 20
- }],
+ "Percentile": [
+ {
+ "Variant": "On",
+ "From": 10,
+ "To": 20
+ }
+ ],
"DefaultWhenEnabled": "Off",
"Seed": "Enhanced-Feature-Group"
},
"Variants": [
- {
+ {
"Name": "On"
},
- {
+ {
"Name": "Off",
"StatusOverride": "Disabled"
- }
+ }
],
-"EnabledFor": [
- {
- "Name": "AlwaysOn"
- }
-]
+"EnabledFor": [
+ {
+ "Name": "AlwaysOn"
+ }
+]
```
In the above example, the feature is enabled by the `AlwaysOn` filter. If the current user is in the calculated percentile range of 10 to 20, then the `On` variant is returned. Otherwise, the `Off` variant is returned and because `StatusOverride` is equal to `Disabled`, the feature will now be considered disabled.
@@ -931,7 +987,7 @@ By default, feature flags will not have telemetry emitted. To publish telemetry
For flags defined in `appsettings.json`, that is done by using the `Telemetry` property on feature flags.
-```
+``` javascript
{
"FeatureManagement":
{
diff --git a/schemas/FeatureManagement.Dotnet.v2.0.0.schema.json b/schemas/FeatureManagement.Dotnet.v2.0.0.schema.json
new file mode 100644
index 00000000..7d25cf06
--- /dev/null
+++ b/schemas/FeatureManagement.Dotnet.v2.0.0.schema.json
@@ -0,0 +1,313 @@
+{
+ "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": {
+ "Status": {
+ "type": "string",
+ "title": "Feature Status",
+ "description": "Describes how a feature's state will be evaluated. When set to Conditional, the state of the feature is conditional upon the feature evaluation pipeline. When set to Disabled, the state of feature is always disabled.",
+ "enum": [
+ "Conditional",
+ "Disabled"
+ ],
+ "default": "Conditional"
+ },
+ "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"
+ }
+ ]
+ },
+ "Variants": {
+ "type": "array",
+ "title": "Variant Collection",
+ "description": "The list of variants defined for this feature. A variant represents a configuration value of a feature flag that can be a string, a number, a boolean, or a JSON object.",
+ "items": {
+ "type": "object",
+ "title": "Variant",
+ "required": [
+ "Name"
+ ],
+ "properties":{
+ "Name": {
+ "type": "string",
+ "title": "Variant Name",
+ "description": "The name used to refer to a feature variant.",
+ "pattern": "^(.*)$"
+ },
+ "ConfigurationValue": {
+ "type": ["string", "null", "number", "object", "array", "boolean"],
+ "title": "Variant Configuration Value",
+ "description": "The configuration value for this feature variant.",
+ "default": null
+ },
+ "ConfigurationReference": {
+ "type": "string",
+ "title": "Variant Configuration Reference",
+ "description": "The path to a configuration section used as the configuration value for this feature variant.",
+ "pattern": "^[a-zA-Z0-9]+(:[a-zA-Z0-9]+)*$"
+ },
+ "StatusOverride": {
+ "type": "string",
+ "title": "Variant Status Override",
+ "description": "Overrides the enabled state of the feature if the given variant is assigned. Does not override the state if value is None.",
+ "enum": [
+ "None",
+ "Enabled",
+ "Disabled"
+ ],
+ "default": "None"
+ }
+ }
+ }
+ },
+ "Allocation": {
+ "type": "object",
+ "title": "Variant Allocation",
+ "description": "Determines how variants should be allocated for the feature to various users.",
+ "required": [],
+ "properties": {
+ "DefaultWhenDisabled": {
+ "type": "string",
+ "title": "Default Variant Allocation When Disabled",
+ "description": "Specifies which variant should be used when the feature is considered disabled.",
+ "default": "",
+ "pattern": "^(.*)$"
+ },
+ "DefaultWhenEnabled": {
+ "type": "string",
+ "title": "Default Variant Allocation When Enabled",
+ "description": "Specifies which variant should be used when the feature is considered enabled and no other allocation rules are applicable.",
+ "default": "",
+ "pattern": "^(.*)$"
+ },
+ "User": {
+ "type": "array",
+ "title": "User Allocation Collection",
+ "description": "A list of objects, each containing a variant name and list of users for whom that variant should be used.",
+ "items": {
+ "type": "object",
+ "title": "User Allocation",
+ "required": [
+ "Variant",
+ "Users"
+ ],
+ "properties": {
+ "Variant": {
+ "type": "string",
+ "title": "User Allocation Variant",
+ "description": "The name of the variant to use if the user allocation matches the current user.",
+ "pattern": "^(.*)$"
+ },
+ "Users": {
+ "type": "array",
+ "title": "User Allocation Users Collection",
+ "description": "Collection of users where if any match the current user, the variant specified in the user allocation is used.",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "Group": {
+ "type": "array",
+ "title": "Group Allocation Collection",
+ "description": "A list of objects, each containing a variant name and list of groups for which that variant should be used.",
+ "items": {
+ "type": "object",
+ "title": "Group Allocation",
+ "required": [
+ "Variant",
+ "Groups"
+ ],
+ "properties": {
+ "Variant": {
+ "type": "string",
+ "title": "Group Allocation Variant",
+ "description": "The name of the variant to use if the group allocation matches a group the current user is in.",
+ "pattern": "^(.*)$"
+ },
+ "Groups": {
+ "type": "array",
+ "title": "Group Allocation Groups Collection",
+ "description": "Collection of groups where if the current user is in any of these groups, the variant specified in the group allocation is used.",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "Percentile": {
+ "type": "array",
+ "title": "Percentile Allocation Collection",
+ "description": "A list of objects, each containing a variant name and percentage range for which that variant should be used.",
+ "items": {
+ "type": "object",
+ "title": "Percentile Allocation",
+ "required": [
+ "Variant",
+ "From",
+ "To"
+ ],
+ "properties": {
+ "Variant": {
+ "type": "string",
+ "title": "Percentile Allocation Variant",
+ "description": "The name of the variant to use if the calculated percentile for the current user falls in the provided range.",
+ "pattern": "^(.*)$"
+ },
+ "From": {
+ "type": "number",
+ "title": "Percentile Allocation From",
+ "description": "The lower end of the percentage range for which this variant will be used.",
+ "minimum": 0,
+ "maximum": 100
+ },
+ "To": {
+ "type": "number",
+ "title": "Percentile Allocation To",
+ "description": "The upper end of the percentage range for which this variant will be used.",
+ "minimum": 0,
+ "maximum": 100
+ }
+ }
+ }
+ },
+ "Seed": {
+ "type": "string",
+ "title": "Percentile Allocation Seed",
+ "description": "The value percentile calculations are based on. The calculated percentile is consistent across features for a given user if the same nonempty seed is used.",
+ "default": "",
+ "pattern": "^(.*)$"
+ }
+ }
+ },
+ "Telemetry": {
+ "type": "object",
+ "title": "Telemetry Options",
+ "description": "The declaration of options used to configure telemetry for this feature.",
+ "required": [],
+ "properties": {
+ "Enabled": {
+ "type": "boolean",
+ "title": "Telemetry Enabled State",
+ "description": "Indicates if telemetry is enabled.",
+ "default": false
+ },
+ "Metadata": {
+ "type": "object",
+ "title": "Telemetry Metadata",
+ "description": "A container for metadata that should be bundled with flag telemetry.",
+ "required": [],
+ "patternProperties": {
+ "^.*$": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
index c823a1d0..db08fae2 100644
--- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
+++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
@@ -200,7 +200,7 @@ We support
Allocation allocation = null;
- List variants = null;
+ var variants = new List();
bool telemetryEnabled = false;
@@ -316,8 +316,6 @@ We support
IEnumerable variantsSections = configurationSection.GetSection(ConfigurationFields.VariantsSectionName).GetChildren();
- variants = new List();
-
foreach (IConfigurationSection section in variantsSections)
{
if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[ConfigurationFields.NameKeyword]))
@@ -419,7 +417,17 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection
bool enabled = false;
- IConfigurationSection conditions = configurationSection.GetSection(MicrosoftFeatureManagementFields.Conditions);
+ FeatureStatus featureStatus = FeatureStatus.Disabled;
+
+ Allocation allocation = null;
+
+ var variants = new List();
+
+ bool telemetryEnabled = false;
+
+ Dictionary telemetryMetadata = null;
+
+ IConfigurationSection conditionsSection = configurationSection.GetSection(MicrosoftFeatureManagementFields.Conditions);
string rawEnabled = configurationSection[MicrosoftFeatureManagementFields.Enabled];
@@ -430,7 +438,7 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection
if (enabled)
{
- string rawRequirementType = conditions[MicrosoftFeatureManagementFields.RequirementType];
+ string rawRequirementType = conditionsSection[MicrosoftFeatureManagementFields.RequirementType];
//
// If requirement type is specified, parse it and set the requirementType variable
@@ -441,7 +449,9 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection
$"Invalid value '{rawRequirementType}' for field '{MicrosoftFeatureManagementFields.RequirementType}' of feature '{featureName}'.");
}
- IEnumerable filterSections = conditions.GetSection(MicrosoftFeatureManagementFields.ClientFilters).GetChildren();
+ featureStatus = FeatureStatus.Conditional;
+
+ IEnumerable filterSections = conditionsSection.GetSection(MicrosoftFeatureManagementFields.ClientFilters).GetChildren();
if (filterSections.Any())
{
@@ -469,11 +479,122 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection
}
}
+ IConfigurationSection allocationSection = configurationSection.GetSection(MicrosoftFeatureManagementFields.AllocationSectionName);
+
+ if (allocationSection.Exists())
+ {
+ allocation = new Allocation()
+ {
+ DefaultWhenDisabled = allocationSection[MicrosoftFeatureManagementFields.AllocationDefaultWhenDisabled],
+ DefaultWhenEnabled = allocationSection[MicrosoftFeatureManagementFields.AllocationDefaultWhenEnabled],
+ User = allocationSection.GetSection(MicrosoftFeatureManagementFields.UserAllocationSectionName).GetChildren().Select(userAllocation =>
+ {
+ return new UserAllocation()
+ {
+ Variant = userAllocation[MicrosoftFeatureManagementFields.AllocationVariantKeyword],
+ Users = userAllocation.GetSection(MicrosoftFeatureManagementFields.UserAllocationUsers).Get>()
+ };
+ }),
+ Group = allocationSection.GetSection(MicrosoftFeatureManagementFields.GroupAllocationSectionName).GetChildren().Select(groupAllocation =>
+ {
+ return new GroupAllocation()
+ {
+ Variant = groupAllocation[MicrosoftFeatureManagementFields.AllocationVariantKeyword],
+ Groups = groupAllocation.GetSection(MicrosoftFeatureManagementFields.GroupAllocationGroups).Get>()
+ };
+ }),
+ Percentile = allocationSection.GetSection(MicrosoftFeatureManagementFields.PercentileAllocationSectionName).GetChildren().Select(percentileAllocation =>
+ {
+ double from = 0;
+
+ double to = 0;
+
+ string rawFrom = percentileAllocation[MicrosoftFeatureManagementFields.PercentileAllocationFrom];
+
+ string rawTo = percentileAllocation[MicrosoftFeatureManagementFields.PercentileAllocationTo];
+
+ if (!string.IsNullOrEmpty(rawFrom))
+ {
+ from = ParseDouble(featureName, rawFrom, MicrosoftFeatureManagementFields.PercentileAllocationFrom);
+ }
+
+ if (!string.IsNullOrEmpty(rawTo))
+ {
+ to = ParseDouble(featureName, rawTo, MicrosoftFeatureManagementFields.PercentileAllocationTo);
+ }
+
+ return new PercentileAllocation()
+ {
+ Variant = percentileAllocation[MicrosoftFeatureManagementFields.AllocationVariantKeyword],
+ From = from,
+ To = to
+ };
+ }),
+ Seed = allocationSection[MicrosoftFeatureManagementFields.AllocationSeed]
+ };
+ }
+
+ IEnumerable variantsSections = configurationSection.GetSection(MicrosoftFeatureManagementFields.VariantsSectionName).GetChildren();
+
+ foreach (IConfigurationSection section in variantsSections)
+ {
+ if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[MicrosoftFeatureManagementFields.Name]))
+ {
+ StatusOverride statusOverride = StatusOverride.None;
+
+ string rawStatusOverride = section[MicrosoftFeatureManagementFields.VariantDefinitionStatusOverride];
+
+ if (!string.IsNullOrEmpty(rawStatusOverride))
+ {
+ statusOverride = ParseEnum(configurationSection.Key, rawStatusOverride, MicrosoftFeatureManagementFields.VariantDefinitionStatusOverride);
+ }
+
+ var variant = new VariantDefinition()
+ {
+ Name = section[MicrosoftFeatureManagementFields.Name],
+ ConfigurationValue = section.GetSection(MicrosoftFeatureManagementFields.VariantDefinitionConfigurationValue),
+ ConfigurationReference = section[MicrosoftFeatureManagementFields.VariantDefinitionConfigurationReference],
+ StatusOverride = statusOverride
+ };
+
+ variants.Add(variant);
+ }
+ }
+
+ IConfigurationSection telemetrySection = configurationSection.GetSection(MicrosoftFeatureManagementFields.Telemetry);
+
+ if (telemetrySection.Exists())
+ {
+ string rawTelemetryEnabled = telemetrySection[MicrosoftFeatureManagementFields.Enabled];
+
+ if (!string.IsNullOrEmpty(rawTelemetryEnabled))
+ {
+ telemetryEnabled = ParseBool(featureName, rawTelemetryEnabled, MicrosoftFeatureManagementFields.Enabled);
+ }
+
+ IConfigurationSection telemetryMetadataSection = telemetrySection.GetSection(MicrosoftFeatureManagementFields.Metadata);
+
+ if (telemetryMetadataSection.Exists())
+ {
+ telemetryMetadata = new Dictionary();
+
+ telemetryMetadata = telemetryMetadataSection.GetChildren().ToDictionary(x => x.Key, x => x.Value);
+ }
+ }
+
return new FeatureDefinition()
{
Name = featureName,
EnabledFor = enabledFor,
- RequirementType = requirementType
+ RequirementType = requirementType,
+ Status = featureStatus,
+ Allocation = allocation,
+ Variants = variants,
+ Telemetry = new TelemetryConfiguration
+ {
+ Enabled = telemetryEnabled,
+ Metadata = telemetryMetadata
+ }
};
}
diff --git a/src/Microsoft.FeatureManagement/ConfigurationFields.cs b/src/Microsoft.FeatureManagement/ConfigurationFields.cs
index 01a1f698..47d0c6a4 100644
--- a/src/Microsoft.FeatureManagement/ConfigurationFields.cs
+++ b/src/Microsoft.FeatureManagement/ConfigurationFields.cs
@@ -17,8 +17,8 @@ internal static class ConfigurationFields
public const string AllocationSectionName = "Allocation";
public const string AllocationDefaultWhenDisabled = "DefaultWhenDisabled";
public const string AllocationDefaultWhenEnabled = "DefaultWhenEnabled";
- public const string UserAllocationSectionName = "User";
public const string AllocationVariantKeyword = "Variant";
+ public const string UserAllocationSectionName = "User";
public const string UserAllocationUsers = "Users";
public const string GroupAllocationSectionName = "Group";
public const string GroupAllocationGroups = "Groups";
diff --git a/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs b/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs
index 50351526..1272b33c 100644
--- a/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs
+++ b/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs
@@ -19,9 +19,34 @@ internal static class MicrosoftFeatureManagementFields
public const string ClientFilters = "client_filters";
public const string RequirementType = "requirement_type";
+ //
+ // Allocation keywords
+ public const string AllocationSectionName = "allocation";
+ public const string AllocationDefaultWhenDisabled = "default_when_disabled";
+ public const string AllocationDefaultWhenEnabled = "default_when_enabled";
+ public const string AllocationVariantKeyword = "variant";
+ public const string UserAllocationSectionName = "user";
+ public const string UserAllocationUsers = "users";
+ public const string GroupAllocationSectionName = "group";
+ public const string GroupAllocationGroups = "groups";
+ public const string PercentileAllocationSectionName = "percentile";
+ public const string PercentileAllocationFrom = "from";
+ public const string PercentileAllocationTo = "to";
+ public const string AllocationSeed = "seed";
+
//
// Client filter keywords
public const string Name = "name";
public const string Parameters = "parameters";
+
+ // Variants keywords
+ public const string VariantsSectionName = "variants";
+ public const string VariantDefinitionConfigurationValue = "configuration_value";
+ public const string VariantDefinitionConfigurationReference = "configuration_reference";
+ public const string VariantDefinitionStatusOverride = "status_override";
+
+ // Telemetry keywords
+ public const string Telemetry = "telemetry";
+ public const string Metadata = "metadata";
}
}
\ No newline at end of file
diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs
index 850dae60..2c3c5a57 100644
--- a/tests/Tests.FeatureManagement/FeatureManagement.cs
+++ b/tests/Tests.FeatureManagement/FeatureManagement.cs
@@ -1120,7 +1120,6 @@ public async Task TelemetryPublishing()
Assert.Null(variantResult);
Assert.Null(testPublisher.evaluationEventCache.Variant);
Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled, testPublisher.evaluationEventCache.VariantAssignmentReason);
-
}
[Fact]
diff --git a/tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json b/tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json
index b3c8db10..24b4329c 100644
--- a/tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json
+++ b/tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json
@@ -1,4 +1,14 @@
{
+ "ShoppingCart": {
+ "Big": {
+ "Size": 600,
+ "Color": "green"
+ },
+ "Small": {
+ "Size": 300,
+ "Color": "gray"
+ }
+ },
"feature_management": {
"feature_flags": [
{
@@ -71,6 +81,270 @@
}
]
}
+ },
+ {
+ "id": "AlwaysOnTestFeature",
+ "enabled": true,
+ "telemetry": {
+ "enabled": true,
+ "metadata": {
+ "Tags.Tag1": "Tag1Value",
+ "Tags.Tag2": "Tag2Value",
+ "Etag": "EtagValue",
+ "Label": "LabelValue"
+ }
+ }
+ },
+ {
+ "id": "VariantFeatureDefaultEnabled",
+ "enabled": true,
+ "telemetry": {
+ "enabled": true
+ },
+ "allocation": {
+ "default_when_enabled": "Medium",
+ "user": [
+ {
+ "variant": "Small",
+ "users": [
+ "Jeff"
+ ]
+ }
+ ]
+ },
+ "variants": [
+ {
+ "name": "Medium",
+ "configuration_value": {
+ "Size": "450px",
+ "Color": "Purple"
+ }
+ },
+ {
+ "name": "Small",
+ "configuration_value": "300px"
+ }
+ ]
+ },
+ {
+ "id": "VariantFeatureStatusDisabled",
+ "enabled": false,
+ "telemetry": {
+ "enabled": true
+ },
+ "allocation": {
+ "default_when_disabled": "Small"
+ },
+ "variants": [
+ {
+ "name": "Small",
+ "configuration_value": "300px"
+ }
+ ]
+ },
+ {
+ "id": "VariantFeaturePercentileOn",
+ "enabled": true,
+ "telemetry": {
+ "enabled": true
+ },
+ "allocation": {
+ "percentile": [
+ {
+ "variant": "Big",
+ "from": 0,
+ "to": 50
+ }
+ ],
+ "seed": 1234
+ },
+ "variants": [
+ {
+ "name": "Big",
+ "configuration_reference": "ShoppingCart:Big",
+ "status_override": "Disabled"
+ }
+ ]
+ },
+ {
+ "id": "VariantFeaturePercentileOff",
+ "enabled": true,
+ "telemetry": {
+ "enabled": true
+ },
+ "allocation": {
+ "percentile": [
+ {
+ "variant": "Big",
+ "from": 0,
+ "to": 50
+ }
+ ],
+ "seed": 12345
+ },
+ "variants": [
+ {
+ "name": "Big",
+ "configuration_reference": "ShoppingCart:Big"
+ }
+ ]
+ },
+ {
+ "id": "VariantFeatureAlwaysOff",
+ "enabled": false,
+ "telemetry": {
+ "enabled": true
+ },
+ "allocation": {
+ "percentile": [
+ {
+ "variant": "Big",
+ "from": 0,
+ "to": 100
+ }
+ ],
+ "seed": 12345
+ },
+ "variants": [
+ {
+ "name": "Big",
+ "configuration_reference": "ShoppingCart:Big"
+ }
+ ]
+ },
+ {
+ "id": "VariantFeatureUser",
+ "enabled": true,
+ "telemetry": {
+ "enabled": true
+ },
+ "allocation": {
+ "user": [
+ {
+ "variant": "Small",
+ "users": [
+ "Marsha"
+ ]
+ }
+ ]
+ },
+ "variants": [
+ {
+ "name": "Small",
+ "configuration_value": "300px"
+ }
+ ]
+ },
+ {
+ "id": "VariantFeatureGroup",
+ "enabled": true,
+ "telemetry": {
+ "enabled": true
+ },
+ "allocation": {
+ "group": [
+ {
+ "variant": "Small",
+ "groups": [
+ "Group1"
+ ]
+ }
+ ]
+ },
+ "variants": [
+ {
+ "name": "Small",
+ "configuration_value": "300px"
+ }
+ ]
+ },
+ {
+ "id": "VariantFeatureNoAllocation",
+ "enabled": true,
+ "telemetry": {
+ "enabled": true
+ },
+ "variants": [
+ {
+ "name": "Small",
+ "configuration_value": "300px"
+ }
+ ]
+ },
+ {
+ "id": "VariantFeatureAlwaysOffNoAllocation",
+ "enabled": false,
+ "telemetry": {
+ "enabled": true
+ },
+ "variants": [
+ {
+ "name": "Small",
+ "configuration_value": "300px"
+ }
+ ]
+ },
+ {
+ "id": "VariantFeatureNoVariants",
+ "enabled": true,
+ "allocation": {
+ "user": [
+ {
+ "variant": "Small",
+ "users": [
+ "Marsha"
+ ]
+ }
+ ]
+ },
+ "variants": []
+ },
+ {
+ "id": "VariantFeatureBothConfigurations",
+ "enabled": true,
+ "allocation": {
+ "default_when_enabled": "Small"
+ },
+ "variants": [
+ {
+ "name": "Small",
+ "configuration_value": "600px",
+ "configuration_reference": "ShoppingCart:Small"
+ }
+ ]
+ },
+ {
+ "id": "VariantFeatureInvalidStatusOverride",
+ "enabled": true,
+ "allocation": {
+ "defaultWhenEnabled": "Small"
+ },
+ "variants": [
+ {
+ "name": "Small",
+ "configuration_value": "300px",
+ "status_override": "InvalidValue"
+ }
+ ]
+ },
+ {
+ "id": "VariantFeatureInvalidFromTo",
+ "enabled": true,
+ "allocation": {
+ "percentile": [
+ {
+ "variant": "Small",
+ "from": "Invalid",
+ "to": "Invalid"
+ }
+ ]
+ },
+ "variants": [
+ {
+ "name": "Small",
+ "configuration_reference": "ShoppingCart:Small"
+ }
+ ]
}
]
}
diff --git a/tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs b/tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs
index 869d3399..624b53c5 100644
--- a/tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs
+++ b/tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs
@@ -4,9 +4,14 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.FeatureManagement;
+using Microsoft.FeatureManagement.FeatureFilters;
+using Microsoft.FeatureManagement.Telemetry;
+using Microsoft.FeatureManagement.Tests;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using Xunit;
@@ -66,6 +71,72 @@ public async Task ReadsFeatureDefinition()
Assert.Equal("Test", filterConfig.Name);
Assert.Equal("V1", filterConfig.Parameters["P1"]);
+
+ featureDefinition = await featureDefinitionProvider.GetFeatureDefinitionAsync(Features.AlwaysOnTestFeature);
+
+ Assert.NotNull(featureDefinition);
+
+ Assert.True(featureDefinition.Telemetry.Enabled);
+
+ Assert.Equal("Tag1Value", featureDefinition.Telemetry.Metadata["Tags.Tag1"]);
+
+ Assert.Equal("Tag2Value", featureDefinition.Telemetry.Metadata["Tags.Tag2"]);
+
+ Assert.Equal("EtagValue", featureDefinition.Telemetry.Metadata["Etag"]);
+
+ Assert.Equal("LabelValue", featureDefinition.Telemetry.Metadata["Label"]);
+
+ featureDefinition = await featureDefinitionProvider.GetFeatureDefinitionAsync(Features.VariantFeatureDefaultEnabled);
+
+ Assert.NotNull(featureDefinition);
+
+ Assert.Equal("Medium", featureDefinition.Allocation.DefaultWhenEnabled);
+
+ Assert.Equal("Small", featureDefinition.Allocation.User.First().Variant);
+
+ Assert.Equal("Jeff", featureDefinition.Allocation.User.First().Users.First());
+
+ VariantDefinition smallVariant = featureDefinition.Variants.FirstOrDefault(variant => string.Equals(variant.Name, "Small"));
+
+ Assert.NotNull(smallVariant);
+
+ Assert.Equal("300px", smallVariant.ConfigurationValue.Value);
+
+ Assert.Equal(StatusOverride.None, smallVariant.StatusOverride);
+
+ featureDefinition = await featureDefinitionProvider.GetFeatureDefinitionAsync(Features.VariantFeatureStatusDisabled);
+
+ Assert.NotNull(featureDefinition);
+
+ Assert.Equal("Small", featureDefinition.Allocation.DefaultWhenDisabled);
+
+ featureDefinition = await featureDefinitionProvider.GetFeatureDefinitionAsync(Features.VariantFeaturePercentileOn);
+
+ Assert.NotNull(featureDefinition);
+
+ Assert.Equal(0, featureDefinition.Allocation.Percentile.First().From);
+
+ Assert.Equal(50, featureDefinition.Allocation.Percentile.First().To);
+
+ Assert.Equal("Big", featureDefinition.Allocation.Percentile.First().Variant);
+
+ Assert.Equal("1234", featureDefinition.Allocation.Seed);
+
+ VariantDefinition bigVariant = featureDefinition.Variants.FirstOrDefault(variant => string.Equals(variant.Name, "Big"));
+
+ Assert.NotNull(bigVariant);
+
+ Assert.Equal("ShoppingCart:Big", bigVariant.ConfigurationReference);
+
+ Assert.Equal(StatusOverride.Disabled, bigVariant.StatusOverride);
+
+ featureDefinition = await featureDefinitionProvider.GetFeatureDefinitionAsync(Features.VariantFeatureGroup);
+
+ Assert.NotNull(featureDefinition);
+
+ Assert.Equal("Small", featureDefinition.Allocation.Group.First().Variant);
+
+ Assert.Equal("Group1", featureDefinition.Allocation.Group.First().Groups.First());
}
[Fact]
@@ -104,6 +175,236 @@ public async Task ReadsMicrosoftFeatureManagementSchemaIfAny()
Assert.False(await featureManager.IsEnabledAsync("FeatureY"));
}
+
+ [Fact]
+ public async Task TelemetryPublishing()
+ {
+ IConfiguration config = new ConfigurationBuilder().AddJsonFile("MicrosoftFeatureManagement.json").Build();
+
+ var services = new ServiceCollection();
+
+ var targetingContextAccessor = new OnDemandTargetingContextAccessor();
+ services.AddSingleton(targetingContextAccessor)
+ .AddSingleton(config)
+ .AddFeatureManagement()
+ .AddTelemetryPublisher();
+
+ ServiceProvider serviceProvider = services.BuildServiceProvider();
+
+ FeatureManager featureManager = (FeatureManager)serviceProvider.GetRequiredService();
+ TestTelemetryPublisher testPublisher = (TestTelemetryPublisher)featureManager.TelemetryPublishers.First();
+ CancellationToken cancellationToken = CancellationToken.None;
+
+ // Test a feature with telemetry disabled
+ bool result = await featureManager.IsEnabledAsync(Features.OnTestFeature, cancellationToken);
+
+ Assert.True(result);
+ Assert.Null(testPublisher.evaluationEventCache);
+
+ // Test telemetry cases
+ result = await featureManager.IsEnabledAsync(Features.AlwaysOnTestFeature, cancellationToken);
+
+ Assert.True(result);
+ Assert.Equal(Features.AlwaysOnTestFeature, testPublisher.evaluationEventCache.FeatureDefinition.Name);
+ Assert.Equal(result, testPublisher.evaluationEventCache.Enabled);
+ Assert.Equal("EtagValue", testPublisher.evaluationEventCache.FeatureDefinition.Telemetry.Metadata["Etag"]);
+ Assert.Equal("LabelValue", testPublisher.evaluationEventCache.FeatureDefinition.Telemetry.Metadata["Label"]);
+ Assert.Equal("Tag1Value", testPublisher.evaluationEventCache.FeatureDefinition.Telemetry.Metadata["Tags.Tag1"]);
+ Assert.Null(testPublisher.evaluationEventCache.Variant);
+ Assert.Equal(VariantAssignmentReason.None, testPublisher.evaluationEventCache.VariantAssignmentReason);
+
+ // Test variant cases
+ result = await featureManager.IsEnabledAsync(Features.VariantFeatureDefaultEnabled, cancellationToken);
+
+ Assert.True(result);
+ Assert.Equal(Features.VariantFeatureDefaultEnabled, testPublisher.evaluationEventCache.FeatureDefinition.Name);
+ Assert.Equal(result, testPublisher.evaluationEventCache.Enabled);
+ Assert.Equal("Medium", testPublisher.evaluationEventCache.Variant.Name);
+
+ Variant variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureDefaultEnabled, cancellationToken);
+
+ Assert.True(testPublisher.evaluationEventCache.Enabled);
+ Assert.Equal(Features.VariantFeatureDefaultEnabled, testPublisher.evaluationEventCache.FeatureDefinition.Name);
+ Assert.Equal(variantResult.Name, testPublisher.evaluationEventCache.Variant.Name);
+ Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled, testPublisher.evaluationEventCache.VariantAssignmentReason);
+
+ result = await featureManager.IsEnabledAsync(Features.VariantFeatureStatusDisabled, cancellationToken);
+
+ Assert.False(result);
+ Assert.Equal(Features.VariantFeatureStatusDisabled, testPublisher.evaluationEventCache.FeatureDefinition.Name);
+ Assert.Equal(result, testPublisher.evaluationEventCache.Enabled);
+ Assert.Equal("Small", testPublisher.evaluationEventCache.Variant.Name);
+ Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled, testPublisher.evaluationEventCache.VariantAssignmentReason);
+
+ variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureStatusDisabled, cancellationToken);
+
+ Assert.False(testPublisher.evaluationEventCache.Enabled);
+ Assert.Equal(Features.VariantFeatureStatusDisabled, testPublisher.evaluationEventCache.FeatureDefinition.Name);
+ Assert.Equal(variantResult.Name, testPublisher.evaluationEventCache.Variant.Name);
+ Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled, testPublisher.evaluationEventCache.VariantAssignmentReason);
+
+ targetingContextAccessor.Current = new TargetingContext
+ {
+ UserId = "Marsha",
+ Groups = new List { "Group1" }
+ };
+
+ variantResult = await featureManager.GetVariantAsync(Features.VariantFeaturePercentileOn, cancellationToken);
+ Assert.Equal("Big", variantResult.Name);
+ Assert.Equal("Big", testPublisher.evaluationEventCache.Variant.Name);
+ Assert.Equal("Marsha", testPublisher.evaluationEventCache.TargetingContext.UserId);
+ Assert.Equal(VariantAssignmentReason.Percentile, testPublisher.evaluationEventCache.VariantAssignmentReason);
+
+ variantResult = await featureManager.GetVariantAsync(Features.VariantFeaturePercentileOff, cancellationToken);
+ Assert.Null(variantResult);
+ Assert.Null(testPublisher.evaluationEventCache.Variant);
+ Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled, testPublisher.evaluationEventCache.VariantAssignmentReason);
+
+ variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureAlwaysOff, cancellationToken);
+ Assert.Null(variantResult);
+ Assert.Null(testPublisher.evaluationEventCache.Variant);
+ Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled, testPublisher.evaluationEventCache.VariantAssignmentReason);
+
+ variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureUser, cancellationToken);
+ Assert.Equal("Small", variantResult.Name);
+ Assert.Equal("Small", testPublisher.evaluationEventCache.Variant.Name);
+ Assert.Equal(VariantAssignmentReason.User, testPublisher.evaluationEventCache.VariantAssignmentReason);
+
+ variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureGroup, cancellationToken);
+ Assert.Equal("Small", variantResult.Name);
+ Assert.Equal("Small", testPublisher.evaluationEventCache.Variant.Name);
+ Assert.Equal(VariantAssignmentReason.Group, testPublisher.evaluationEventCache.VariantAssignmentReason);
+
+ variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureNoAllocation, cancellationToken);
+ Assert.Null(variantResult);
+ Assert.Null(testPublisher.evaluationEventCache.Variant);
+ Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled, testPublisher.evaluationEventCache.VariantAssignmentReason);
+
+ variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureAlwaysOffNoAllocation, cancellationToken);
+ Assert.Null(variantResult);
+ Assert.Null(testPublisher.evaluationEventCache.Variant);
+ Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled, testPublisher.evaluationEventCache.VariantAssignmentReason);
+ }
+
+ [Fact]
+ public async Task UsesVariants()
+ {
+ IConfiguration config = new ConfigurationBuilder().AddJsonFile("MicrosoftFeatureManagement.json").Build();
+
+ var services = new ServiceCollection();
+
+ var targetingContextAccessor = new OnDemandTargetingContextAccessor();
+ services.AddSingleton(targetingContextAccessor)
+ .AddSingleton(config)
+ .AddFeatureManagement();
+
+ ServiceProvider serviceProvider = services.BuildServiceProvider();
+
+ IVariantFeatureManager featureManager = serviceProvider.GetRequiredService();
+ CancellationToken cancellationToken = CancellationToken.None;
+
+ targetingContextAccessor.Current = new TargetingContext
+ {
+ UserId = "Marsha",
+ Groups = new List { "Group1" }
+ };
+
+ // Test StatusOverride and Percentile with Seed
+ Variant variant = await featureManager.GetVariantAsync(Features.VariantFeaturePercentileOn, cancellationToken);
+
+ Assert.Equal("Big", variant.Name);
+ Assert.Equal("green", variant.Configuration["Color"]);
+ Assert.False(await featureManager.IsEnabledAsync(Features.VariantFeaturePercentileOn, cancellationToken));
+
+ variant = await featureManager.GetVariantAsync(Features.VariantFeaturePercentileOff, cancellationToken);
+
+ Assert.Null(variant);
+ Assert.True(await featureManager.IsEnabledAsync(Features.VariantFeaturePercentileOff, cancellationToken));
+
+ // Test Status = Disabled
+ variant = await featureManager.GetVariantAsync(Features.VariantFeatureStatusDisabled, cancellationToken);
+
+ Assert.Equal("Small", variant.Name);
+ Assert.Equal("300px", variant.Configuration.Value);
+ Assert.False(await featureManager.IsEnabledAsync(Features.VariantFeatureStatusDisabled, cancellationToken));
+
+ // Test DefaultWhenEnabled and ConfigurationValue with inline IConfigurationSection
+ variant = await featureManager.GetVariantAsync(Features.VariantFeatureDefaultEnabled, cancellationToken);
+
+ Assert.Equal("Medium", variant.Name);
+ Assert.Equal("450px", variant.Configuration["Size"]);
+ Assert.True(await featureManager.IsEnabledAsync(Features.VariantFeatureDefaultEnabled, cancellationToken));
+
+ // Test User allocation
+ variant = await featureManager.GetVariantAsync(Features.VariantFeatureUser, cancellationToken);
+
+ Assert.Equal("Small", variant.Name);
+ Assert.Equal("300px", variant.Configuration.Value);
+ Assert.True(await featureManager.IsEnabledAsync(Features.VariantFeatureUser, cancellationToken));
+
+ // Test Group allocation
+ variant = await featureManager.GetVariantAsync(Features.VariantFeatureGroup, cancellationToken);
+
+ Assert.Equal("Small", variant.Name);
+ Assert.Equal("300px", variant.Configuration.Value);
+ Assert.True(await featureManager.IsEnabledAsync(Features.VariantFeatureGroup, cancellationToken));
+ }
+
+ [Fact]
+ public async Task VariantsInvalidScenarios()
+ {
+ IConfiguration config = new ConfigurationBuilder().AddJsonFile("MicrosoftFeatureManagement.json").Build();
+
+ var services = new ServiceCollection();
+
+ var targetingContextAccessor = new OnDemandTargetingContextAccessor();
+ services.AddSingleton(targetingContextAccessor)
+ .AddSingleton(config)
+ .AddFeatureManagement();
+
+ targetingContextAccessor.Current = new TargetingContext
+ {
+ UserId = "Jeff"
+ };
+
+ ServiceProvider serviceProvider = services.BuildServiceProvider();
+
+ IVariantFeatureManager featureManager = serviceProvider.GetRequiredService();
+ CancellationToken cancellationToken = CancellationToken.None;
+
+ // Verify null variant returned if no variants are specified
+ Variant variant = await featureManager.GetVariantAsync(Features.VariantFeatureNoVariants, cancellationToken);
+
+ Assert.Null(variant);
+
+ // Verify null variant returned if no allocation is specified
+ variant = await featureManager.GetVariantAsync(Features.VariantFeatureNoAllocation, cancellationToken);
+
+ Assert.Null(variant);
+
+ // Verify that ConfigurationValue has priority over ConfigurationReference
+ variant = await featureManager.GetVariantAsync(Features.VariantFeatureBothConfigurations, cancellationToken);
+
+ Assert.Equal("600px", variant.Configuration.Value);
+
+ // Verify that an exception is thrown for invalid StatusOverride value
+ FeatureManagementException e = await Assert.ThrowsAsync(async () =>
+ {
+ variant = await featureManager.GetVariantAsync(Features.VariantFeatureInvalidStatusOverride, cancellationToken);
+ });
+
+ Assert.Equal(FeatureManagementError.InvalidConfigurationSetting, e.Error);
+ Assert.Contains(MicrosoftFeatureManagementFields.VariantDefinitionStatusOverride, e.Message);
+
+ // Verify that an exception is thrown for invalid doubles From and To in the Percentile section
+ e = await Assert.ThrowsAsync(async () =>
+ {
+ variant = await featureManager.GetVariantAsync(Features.VariantFeatureInvalidFromTo, cancellationToken);
+ });
+
+ Assert.Equal(FeatureManagementError.InvalidConfigurationSetting, e.Error);
+ Assert.Contains(MicrosoftFeatureManagementFields.PercentileAllocationFrom, e.Message);
+ }
}
}
diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json
index 241353e1..648dbb83 100644
--- a/tests/Tests.FeatureManagement/appsettings.json
+++ b/tests/Tests.FeatureManagement/appsettings.json
@@ -362,14 +362,6 @@
"Enabled": true
},
"Allocation": {
- "User": [
- {
- "Variant": "Small",
- "Users": [
- "Jeff"
- ]
- }
- ],
"Group": [
{
"Variant": "Small",
From 5c34b8c092c9a4f434db810cda07112bf1c79ef2 Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Thu, 14 Mar 2024 14:37:34 +0800
Subject: [PATCH 10/25] Update constructors of built-in feature filters (#386)
* make logger optional
* make TargetingEvaluationOptions optional
---
.../FeatureFilters/PercentageFilter.cs | 9 ++++++---
.../FeatureFilters/TimeWindowFilter.cs | 9 ++++++---
.../Targeting/ContextualTargetingFilter.cs | 6 +++---
.../Targeting/TargetingFilter.cs | 9 ++++++---
4 files changed, 21 insertions(+), 12 deletions(-)
diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs
index c7ea8840..36e6d5e1 100644
--- a/src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs
+++ b/src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs
@@ -21,9 +21,9 @@ public class PercentageFilter : IFeatureFilter, IFilterParametersBinder
/// Creates a percentage based feature filter.
///
/// A logger factory for creating loggers.
- public PercentageFilter(ILoggerFactory loggerFactory)
+ public PercentageFilter(ILoggerFactory loggerFactory = null)
{
- _logger = loggerFactory.CreateLogger();
+ _logger = loggerFactory?.CreateLogger();
}
///
@@ -51,7 +51,10 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context)
if (settings.Value < 0)
{
- _logger.LogWarning($"The '{Alias}' feature filter does not have a valid '{nameof(settings.Value)}' value for feature '{context.FeatureName}'");
+ if (_logger != null)
+ {
+ _logger.LogWarning($"The '{Alias}' feature filter does not have a valid '{nameof(settings.Value)}' value for feature '{context.FeatureName}'");
+ }
result = false;
}
diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs
index 884332b3..6f6d49f4 100644
--- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs
+++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs
@@ -21,9 +21,9 @@ public class TimeWindowFilter : IFeatureFilter, IFilterParametersBinder
/// Creates a time window based feature filter.
///
/// A logger factory for creating loggers.
- public TimeWindowFilter(ILoggerFactory loggerFactory)
+ public TimeWindowFilter(ILoggerFactory loggerFactory = null)
{
- _logger = loggerFactory.CreateLogger();
+ _logger = loggerFactory?.CreateLogger();
}
///
@@ -51,7 +51,10 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context)
if (!settings.Start.HasValue && !settings.End.HasValue)
{
- _logger.LogWarning($"The '{Alias}' feature filter is not valid for feature '{context.FeatureName}'. It must have have specify either '{nameof(settings.Start)}', '{nameof(settings.End)}', or both.");
+ if (_logger != null)
+ {
+ _logger.LogWarning($"The '{Alias}' feature filter is not valid for feature '{context.FeatureName}'. It must have have specify either '{nameof(settings.Start)}', '{nameof(settings.End)}', or both.");
+ }
return Task.FromResult(false);
}
diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs
index d29cafcd..2310d380 100644
--- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs
+++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs
@@ -28,10 +28,10 @@ public class ContextualTargetingFilter : IContextualFeatureFilter
/// Options controlling the behavior of the targeting evaluation performed by the filter.
/// A logger factory for creating loggers.
- public ContextualTargetingFilter(IOptions options, ILoggerFactory loggerFactory)
+ public ContextualTargetingFilter(IOptions options = null, ILoggerFactory loggerFactory = null)
{
- _options = options?.Value ?? throw new ArgumentNullException(nameof(options));
- _logger = loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory));
+ _options = options?.Value ?? new TargetingEvaluationOptions();
+ _logger = loggerFactory?.CreateLogger();
}
private StringComparison ComparisonType => _options.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs
index d57e4b31..77575958 100644
--- a/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs
+++ b/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs
@@ -26,11 +26,11 @@ public class TargetingFilter : IFeatureFilter, IFilterParametersBinder
/// Options controlling the behavior of the targeting evaluation performed by the filter.
/// An accessor used to acquire the targeting context for use in feature evaluation.
/// A logger factory for creating loggers.
- public TargetingFilter(IOptions options, ITargetingContextAccessor contextAccessor, ILoggerFactory loggerFactory)
+ public TargetingFilter(ITargetingContextAccessor contextAccessor, IOptions options = null, ILoggerFactory loggerFactory = null)
{
_contextAccessor = contextAccessor ?? throw new ArgumentNullException(nameof(contextAccessor));
_contextualFilter = new ContextualTargetingFilter(options, loggerFactory);
- _logger = loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory));
+ _logger = loggerFactory?.CreateLogger();
}
///
@@ -64,7 +64,10 @@ public async Task EvaluateAsync(FeatureFilterEvaluationContext context)
// Ensure targeting can be performed
if (targetingContext == null)
{
- _logger.LogWarning("No targeting context available for targeting evaluation.");
+ if (_logger != null)
+ {
+ _logger.LogWarning("No targeting context available for targeting evaluation.");
+ }
return false;
}
From c95e7553a5000c724604a71785760142437ee012 Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Thu, 14 Mar 2024 14:51:09 +0800
Subject: [PATCH 11/25] update (#398)
---
.../TargetingTelemetryInitializer.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs
index 5614a3f3..e01bd9e4 100644
--- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs
+++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs
@@ -34,12 +34,12 @@ protected override void OnInitializeTelemetry(HttpContext httpContext, RequestTe
{
if (telemetry == null)
{
- throw new ArgumentNullException("telemetry");
+ throw new ArgumentNullException(nameof(telemetry));
}
if (httpContext == null)
{
- throw new ArgumentNullException("httpContext");
+ throw new ArgumentNullException(nameof(httpContext));
}
// Extract the targeting id from the http context
From 1f3eca4e157bb10a7d9d403561ed67d2923adecb Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Thu, 14 Mar 2024 16:23:35 +0800
Subject: [PATCH 12/25] update (#400)
---
.../FeatureFilters/PercentageFilter.cs | 5 +----
.../FeatureFilters/TimeWindowFilter.cs | 5 +----
src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs | 5 +----
3 files changed, 3 insertions(+), 12 deletions(-)
diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs
index 36e6d5e1..c1d4b9a5 100644
--- a/src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs
+++ b/src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs
@@ -51,10 +51,7 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context)
if (settings.Value < 0)
{
- if (_logger != null)
- {
- _logger.LogWarning($"The '{Alias}' feature filter does not have a valid '{nameof(settings.Value)}' value for feature '{context.FeatureName}'");
- }
+ _logger?.LogWarning($"The '{Alias}' feature filter does not have a valid '{nameof(settings.Value)}' value for feature '{context.FeatureName}'");
result = false;
}
diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs
index 6f6d49f4..6765830b 100644
--- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs
+++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs
@@ -51,10 +51,7 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context)
if (!settings.Start.HasValue && !settings.End.HasValue)
{
- if (_logger != null)
- {
- _logger.LogWarning($"The '{Alias}' feature filter is not valid for feature '{context.FeatureName}'. It must have have specify either '{nameof(settings.Start)}', '{nameof(settings.End)}', or both.");
- }
+ _logger?.LogWarning($"The '{Alias}' feature filter is not valid for feature '{context.FeatureName}'. It must have have specify either '{nameof(settings.Start)}', '{nameof(settings.End)}', or both.");
return Task.FromResult(false);
}
diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs
index 77575958..05e880f3 100644
--- a/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs
+++ b/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs
@@ -64,10 +64,7 @@ public async Task EvaluateAsync(FeatureFilterEvaluationContext context)
// Ensure targeting can be performed
if (targetingContext == null)
{
- if (_logger != null)
- {
- _logger.LogWarning("No targeting context available for targeting evaluation.");
- }
+ _logger?.LogWarning("No targeting context available for targeting evaluation.");
return false;
}
From 5dfdb23c22eecc6662afc4700966113b20e1f822 Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Thu, 14 Mar 2024 17:21:31 +0800
Subject: [PATCH 13/25] fix typo in warning message in time window filter
(#401)
---
.../FeatureFilters/TimeWindowFilter.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs
index 6765830b..079b8172 100644
--- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs
+++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs
@@ -51,7 +51,7 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context)
if (!settings.Start.HasValue && !settings.End.HasValue)
{
- _logger?.LogWarning($"The '{Alias}' feature filter is not valid for feature '{context.FeatureName}'. It must have have specify either '{nameof(settings.Start)}', '{nameof(settings.End)}', or both.");
+ _logger?.LogWarning($"The '{Alias}' feature filter is not valid for feature '{context.FeatureName}'. It must specify either '{nameof(settings.Start)}', '{nameof(settings.End)}', or both.");
return Task.FromResult(false);
}
From 0b5aff1e399c1763c13bc443c022f82182302855 Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Mon, 18 Mar 2024 13:24:35 +0800
Subject: [PATCH 14/25] Optional Cancellation Token (#395)
* make cancelltation optional for feature manager & add cancellation token for session manager
* revert breaking change in ISessionManager
* remove unused package
---
src/Microsoft.FeatureManagement/FeatureManager.cs | 10 +++++-----
.../IVariantFeatureManager.cs | 10 +++++-----
2 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs
index 23599eb0..8ad9d40c 100644
--- a/src/Microsoft.FeatureManagement/FeatureManager.cs
+++ b/src/Microsoft.FeatureManagement/FeatureManager.cs
@@ -169,7 +169,7 @@ public async Task IsEnabledAsync(string feature, TContext appCon
/// The name of the feature to check.
/// The cancellation token to cancel the operation.
/// True if the feature is enabled, otherwise false.
- public async ValueTask IsEnabledAsync(string feature, CancellationToken cancellationToken)
+ public async ValueTask IsEnabledAsync(string feature, CancellationToken cancellationToken = default)
{
EvaluationEvent evaluationEvent = await EvaluateFeature
/// An enumerator which provides asynchronous iteration over the feature names registered in the feature manager.
- public async IAsyncEnumerable GetFeatureNamesAsync([EnumeratorCancellation] CancellationToken cancellationToken)
+ public async IAsyncEnumerable GetFeatureNamesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (FeatureDefinition featureDefinition in _featureDefinitionProvider.GetAllFeatureDefinitionsAsync().ConfigureAwait(false))
{
@@ -219,7 +219,7 @@ public async IAsyncEnumerable GetFeatureNamesAsync([EnumeratorCancellati
/// The name of the feature to evaluate.
/// The cancellation token to cancel the operation.
/// A variant assigned to the user based on the feature's configured allocation.
- public async ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken)
+ public async ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(feature))
{
@@ -238,7 +238,7 @@ public async ValueTask GetVariantAsync(string feature, CancellationToke
/// An instance of used to evaluate which variant the user will be assigned.
/// The cancellation token to cancel the operation.
/// A variant assigned to the user based on the feature's configured allocation.
- public async ValueTask GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken)
+ public async ValueTask GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(feature))
{
diff --git a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs
index 8c7d2e84..bd8c665b 100644
--- a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs
+++ b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs
@@ -18,7 +18,7 @@ public interface IVariantFeatureManager
///
/// The cancellation token to cancel the operation.
/// An enumerator which provides asynchronous iteration over the feature names registered in the feature manager.
- IAsyncEnumerable GetFeatureNamesAsync(CancellationToken cancellationToken);
+ IAsyncEnumerable GetFeatureNamesAsync(CancellationToken cancellationToken = default);
///
/// Checks whether a given feature is enabled.
@@ -26,7 +26,7 @@ public interface IVariantFeatureManager
/// The name of the feature to check.
/// The cancellation token to cancel the operation.
/// True if the feature is enabled, otherwise false.
- ValueTask IsEnabledAsync(string feature, CancellationToken cancellationToken);
+ ValueTask IsEnabledAsync(string feature, CancellationToken cancellationToken = default);
///
/// Checks whether a given feature is enabled.
@@ -35,7 +35,7 @@ public interface IVariantFeatureManager
/// A context providing information that can be used to evaluate whether a feature should be on or off.
/// The cancellation token to cancel the operation.
/// True if the feature is enabled, otherwise false.
- ValueTask IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken);
+ ValueTask IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken = default);
///
/// Gets the assigned variant for a specific feature.
@@ -43,7 +43,7 @@ public interface IVariantFeatureManager
/// The name of the feature to evaluate.
/// The cancellation token to cancel the operation.
/// A variant assigned to the user based on the feature's configured allocation.
- ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken);
+ ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken = default);
///
/// Gets the assigned variant for a specific feature.
@@ -52,6 +52,6 @@ public interface IVariantFeatureManager
/// An instance of used to evaluate which variant the user will be assigned.
/// The cancellation token to cancel the operation.
/// A variant assigned to the user based on the feature's configured allocation.
- ValueTask GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken);
+ ValueTask GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken = default);
}
}
From 0d243b6724289cb25402ae87016e3b6040453266 Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Tue, 19 Mar 2024 10:10:55 +0800
Subject: [PATCH 15/25] Update FeatureGateAttribute.cs (#402)
---
.../FeatureGateAttribute.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs
index c2573b8c..fb15e5b1 100644
--- a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs
+++ b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs
@@ -90,12 +90,12 @@ public FeatureGateAttribute(RequirementType requirementType, params object[] fea
public IEnumerable Features { get; }
///
- /// Controls whether any or all features in should be enabled to pass.
+ /// Controls whether any or all features in should be enabled to pass.
///
public RequirementType RequirementType { get; }
///
- /// Performs controller action pre-procesing to ensure that at least one of the specified features are enabled.
+ /// Performs controller action pre-procesing to ensure that any or all of the specified features are enabled.
///
/// The context of the MVC action.
/// The action delegate.
From 709bbebd89e2923f820f75e651fd39778df26cbe Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Wed, 20 Mar 2024 16:10:15 +0800
Subject: [PATCH 16/25] update the way to refer feature flag (#404)
---
.../FeatureManagementAspNetCore.cs | 8 ++++----
tests/Tests.FeatureManagement.AspNetCore/Features.cs | 6 +++---
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs b/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs
index 62919797..3e5d6aa0 100644
--- a/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs
+++ b/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs
@@ -36,12 +36,12 @@ public async Task Integrates()
services.AddMvcCore(o =>
{
DisableEndpointRouting(o);
- o.Filters.AddForFeature(Enum.GetName(typeof(Features), Features.ConditionalFeature));
+ o.Filters.AddForFeature(Features.ConditionalFeature);
});
})
.Configure(app =>
{
- app.UseForFeature(Enum.GetName(typeof(Features), Features.ConditionalFeature), a => a.Use(async (ctx, next) =>
+ app.UseForFeature(Features.ConditionalFeature, a => a.Use(async (ctx, next) =>
{
ctx.Response.Headers[nameof(RouterMiddleware)] = bool.TrueString;
@@ -102,7 +102,7 @@ public async Task GatesFeatures()
//
// Enable 1/2 features
- testFeatureFilter.Callback = ctx => Task.FromResult(ctx.FeatureName == Enum.GetName(typeof(Features), Features.ConditionalFeature));
+ testFeatureFilter.Callback = ctx => Task.FromResult(ctx.FeatureName == Features.ConditionalFeature);
gateAllResponse = await testServer.CreateClient().GetAsync("gateAll");
gateAnyResponse = await testServer.CreateClient().GetAsync("gateAny");
@@ -158,7 +158,7 @@ public async Task GatesRazorPageFeatures()
//
// Enable 1/2 features
- testFeatureFilter.Callback = ctx => Task.FromResult(ctx.FeatureName == Enum.GetName(typeof(Features), Features.ConditionalFeature));
+ testFeatureFilter.Callback = ctx => Task.FromResult(ctx.FeatureName == Features.ConditionalFeature);
gateAllResponse = await testServer.CreateClient().GetAsync("RazorTestAll");
gateAnyResponse = await testServer.CreateClient().GetAsync("RazorTestAny");
diff --git a/tests/Tests.FeatureManagement.AspNetCore/Features.cs b/tests/Tests.FeatureManagement.AspNetCore/Features.cs
index 5d26be98..d3397a05 100644
--- a/tests/Tests.FeatureManagement.AspNetCore/Features.cs
+++ b/tests/Tests.FeatureManagement.AspNetCore/Features.cs
@@ -3,9 +3,9 @@
//
namespace Tests.FeatureManagement.AspNetCore
{
- enum Features
+ static class Features
{
- ConditionalFeature,
- ConditionalFeature2
+ public const string ConditionalFeature = "ConditionalFeature";
+ public const string ConditionalFeature2 = "ConditionalFeature2";
}
}
From 597147348cad537bc81347b3350e06733f7b41b8 Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Thu, 21 Mar 2024 11:17:34 +0800
Subject: [PATCH 17/25] Update README to mention the usage of feature-based
injection (variant service) (#388)
* update README
* update
* update
* update
* update
* fix typo
* update
* update
---
README.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 55 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 2de4a3b8..6f6e99c5 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,7 @@ Here are some of the benefits of using this library:
* [Targeting](#targeting)
* [Targeting Exclusion](#targeting-exclusion)
* [Variants](#variants)
+ * [Variants in Dependency Injection](#variants-in-dependency-injection)
* [Telemetry](#telemetry)
* [Enabling Telemetry](#enabling-telemetry)
* [Custom Telemetry Publishers](#custom-telemetry-publishers)
@@ -954,7 +955,7 @@ The `Allocation` setting of a feature flag has the following properties:
| `DefaultWhenDisabled` | Specifies which variant should be used when a variant is requested while the feature is considered disabled. |
| `DefaultWhenEnabled` | Specifies which variant should be used when a variant is requested while the feature is considered enabled and no other variant was assigned to the user. |
| `User` | Specifies a variant and a list of users to whom that variant should be assigned. |
-| `Group` | Specifies a variant and a list of groups the current user has to be in for that variant to be assigned. |
+| `Group` | Specifies a variant and a list of groups. The variant will be assigned if the user is in at least one of the groups. |
| `Percentile` | Specifies a variant and a percentage range the user's calculated percentage has to fit into for that variant to be assigned. |
| `Seed` | The value which percentage calculations for `Percentile` are based on. The percentage calculation for a specific user will be the same across all features if the same `Seed` value is used. If no `Seed` is specified, then a default seed is created based on the feature name. |
@@ -964,7 +965,9 @@ If the feature is enabled, the feature manager will check the `User`, `Group`, a
Allocation logic is similar to the [Microsoft.Targeting](./README.md#MicrosoftTargeting) feature filter, but there are some parameters that are present in targeting that aren't in allocation, and vice versa. The outcomes of targeting and allocation are not related.
-#### Overriding Enabled State with a Variant
+**Note:** To allow allocating feature variants, you need to register `ITargetingContextAccessor`. This can be done by calling the `WithTargeting` method.
+
+### Overriding Enabled State with a Variant
You can use variants to override the enabled state of a feature flag. This gives variants an opportunity to extend the evaluation of a feature flag. If a caller is checking whether a flag that has variants is enabled, the feature manager will check if the variant assigned to the current user is set up to override the result. This is done using the optional variant property `StatusOverride`. By default, this property is set to `None`, which means the variant doesn't affect whether the flag is considered enabled or disabled. Setting `StatusOverride` to `Enabled` allows the variant, when chosen, to override a flag to be enabled. Setting `StatusOverride` to `Disabled` provides the opposite functionality, therefore disabling the flag when the variant is chosen. A feature with a `Status` of `Disabled` cannot be overridden.
@@ -1000,6 +1003,56 @@ If you are using a feature flag with binary variants, the `StatusOverride` prope
In the above example, the feature is enabled by the `AlwaysOn` filter. If the current user is in the calculated percentile range of 10 to 20, then the `On` variant is returned. Otherwise, the `Off` variant is returned and because `StatusOverride` is equal to `Disabled`, the feature will now be considered disabled.
+### Variants in Dependency Injection
+
+Variant feature flags can be used in conjunction with dependency injection to surface different implementations of a service for different users. This is accomplished through the use of the `IVariantServiceProvider` interface.
+
+``` C#
+IVariantServiceProvider algorithmServiceProvider;
+...
+
+IAlgorithm forecastAlgorithm = await algorithmServiceProvider.GetServiceAsync(cancellationToken);
+```
+
+In the snippet above, the `IVariantServiceProvider` will retrieve an implementation of `IAlgorithm` from the dependency injection container. The chosen implementation is dependent upon:
+* The feature flag that the `IAlgorithm` service was registered with.
+* The allocated variant for that feature.
+
+The `IVariantServiceProvider` is made available to the application by calling `IFeatureManagementBuilder.WithVariantService(string featureName)`. See below for an example.
+
+``` C#
+services.AddFeatureManagement()
+ .WithVariantService("ForecastAlgorithm");
+```
+
+The call above makes `IVariantServiceProvider` available in the service collection. Implementation(s) of `IAlgorithm` must be added separately via an add method such as `services.AddSingleton()`. The implementation of `IAlgorithm` that the `IVariantServiceProvider` uses depends on the `ForecastAlgorithm` variant feature flag. If no implementation of `IAlgorithm` is added to the service collection, then the `IVariantServiceProvider.GetServiceAsync()` will return a task with a *null* result.
+
+``` javascript
+{
+ // The example variant feature flag
+ "ForecastAlgorithm": {
+ "Variants": [
+ {
+ "Name": "AlgorithmBeta"
+ },
+ ...
+ ]
+ }
+}
+```
+
+#### Variant Service Alias Attribute
+
+``` C#
+[VariantServiceAlias("Beta")]
+public class AlgorithmBeta : IAlgorithm
+{
+ ...
+}
+```
+
+The variant service provider will use the type names of implementations to match the allocated variant. If a variant service is decorated with the `VariantServiceAliasAttribute`, the name declared in this attribute should be used in configuration to reference this variant service.
+
## Telemetry
When a feature flag change is deployed, it is often important to analyze its effect on an application. For example, here are a few questions that may arise:
From 09faf8d7b7cd59bfec6836f3b77725b53d99f815 Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Fri, 29 Mar 2024 00:05:42 +0800
Subject: [PATCH 18/25] Avoid redundant validation for cached
TargetingFilterSettings (#387)
* do validation during binding parameters
* adjust method order
---
.../Targeting/ContextualTargetingFilter.cs | 9 +-
.../Targeting/TargetingEvaluator.cs | 133 +++++++++---------
.../Targeting/TargetingFilter.cs | 10 +-
3 files changed, 81 insertions(+), 71 deletions(-)
diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs
index 63fefa8a..e0d7fc7b 100644
--- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs
+++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs
@@ -38,7 +38,14 @@ public ContextualTargetingFilter(IOptions options =
/// that can later be used in targeting.
public object BindParameters(IConfiguration filterParameters)
{
- return filterParameters.Get() ?? new TargetingFilterSettings();
+ TargetingFilterSettings settings = filterParameters.Get() ?? new TargetingFilterSettings();
+
+ if (!TargetingEvaluator.TryValidateSettings(settings, out string paramName, out string reason))
+ {
+ throw new ArgumentException(reason, paramName);
+ }
+
+ return settings;
}
///
diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs
index cc809e27..a2249529 100644
--- a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs
+++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs
@@ -20,6 +20,70 @@ private static StringComparison GetComparisonType(bool ignoreCase) =>
const string OutOfRange = "The value is out of the accepted range.";
const string RequiredParameter = "Value cannot be null.";
+ ///
+ /// Performs validation of targeting settings.
+ ///
+ /// The settings to validate.
+ /// The name of the invalid setting, if any.
+ /// The reason that the setting is invalid.
+ /// True if the provided settings are valid. False if the provided settings are invalid.
+ public static bool TryValidateSettings(TargetingFilterSettings targetingSettings, out string paramName, out string reason)
+ {
+ paramName = null;
+
+ reason = null;
+
+ if (targetingSettings == null)
+ {
+ paramName = nameof(targetingSettings);
+
+ reason = RequiredParameter;
+
+ return false;
+ }
+
+ if (targetingSettings.Audience == null)
+ {
+ paramName = nameof(targetingSettings.Audience);
+
+ reason = RequiredParameter;
+
+ return false;
+ }
+
+ if (targetingSettings.Audience.DefaultRolloutPercentage < 0 || targetingSettings.Audience.DefaultRolloutPercentage > 100)
+ {
+ paramName = $"{targetingSettings.Audience}.{targetingSettings.Audience.DefaultRolloutPercentage}";
+
+ reason = OutOfRange;
+
+ return false;
+ }
+
+ if (targetingSettings.Audience.Groups != null)
+ {
+ int index = 0;
+
+ foreach (GroupRollout groupRollout in targetingSettings.Audience.Groups)
+ {
+ index++;
+
+ if (groupRollout.RolloutPercentage < 0 || groupRollout.RolloutPercentage > 100)
+ {
+ //
+ // Audience.Groups[1].RolloutPercentage
+ paramName = $"{targetingSettings.Audience}.{targetingSettings.Audience.Groups}[{index}].{groupRollout.RolloutPercentage}";
+
+ reason = OutOfRange;
+
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
///
/// Checks if a provided targeting context should be targeted given targeting settings.
///
@@ -35,11 +99,6 @@ public static bool IsTargeted(ITargetingContext targetingContext, TargetingFilte
throw new ArgumentNullException(nameof(targetingContext));
}
- if (!TryValidateSettings(settings, out string paramName, out string reason))
- {
- throw new ArgumentException(reason, paramName);
- }
-
if (settings.Audience.Exclusion != null)
{
//
@@ -229,70 +288,6 @@ public static bool IsTargeted(
return IsTargeted(defaultContextId, 0, defaultRolloutPercentage);
}
- ///
- /// Performs validation of targeting settings.
- ///
- /// The settings to validate.
- /// The name of the invalid setting, if any.
- /// The reason that the setting is invalid.
- /// True if the provided settings are valid. False if the provided settings are invalid.
- private static bool TryValidateSettings(TargetingFilterSettings targetingSettings, out string paramName, out string reason)
- {
- paramName = null;
-
- reason = null;
-
- if (targetingSettings == null)
- {
- paramName = nameof(targetingSettings);
-
- reason = RequiredParameter;
-
- return false;
- }
-
- if (targetingSettings.Audience == null)
- {
- paramName = nameof(targetingSettings.Audience);
-
- reason = RequiredParameter;
-
- return false;
- }
-
- if (targetingSettings.Audience.DefaultRolloutPercentage < 0 || targetingSettings.Audience.DefaultRolloutPercentage > 100)
- {
- paramName = $"{targetingSettings.Audience}.{targetingSettings.Audience.DefaultRolloutPercentage}";
-
- reason = OutOfRange;
-
- return false;
- }
-
- if (targetingSettings.Audience.Groups != null)
- {
- int index = 0;
-
- foreach (GroupRollout groupRollout in targetingSettings.Audience.Groups)
- {
- index++;
-
- if (groupRollout.RolloutPercentage < 0 || groupRollout.RolloutPercentage > 100)
- {
- //
- // Audience.Groups[1].RolloutPercentage
- paramName = $"{targetingSettings.Audience}.{targetingSettings.Audience.Groups}[{index}].{groupRollout.RolloutPercentage}";
-
- reason = OutOfRange;
-
- return false;
- }
- }
- }
-
- return true;
- }
-
///
/// Determines if a given context id should be targeted based off the provided percentage range
///
diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs
index 05e880f3..0a07b536 100644
--- a/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs
+++ b/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs
@@ -4,6 +4,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Microsoft.FeatureManagement.Targeting;
using System;
using System.Threading.Tasks;
@@ -40,7 +41,14 @@ public TargetingFilter(ITargetingContextAccessor contextAccessor, IOptions that can later be used in targeting.
public object BindParameters(IConfiguration filterParameters)
{
- return filterParameters.Get() ?? new TargetingFilterSettings();
+ TargetingFilterSettings settings = filterParameters.Get() ?? new TargetingFilterSettings();
+
+ if (!TargetingEvaluator.TryValidateSettings(settings, out string paramName, out string reason))
+ {
+ throw new ArgumentException(reason, paramName);
+ }
+
+ return settings;
}
///
From 526b364ab51ff46f309c2bd19cf5c4aec6d688e5 Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Tue, 2 Apr 2024 10:39:38 +0800
Subject: [PATCH 19/25] Update target framework for telemetry packages (#413)
* update target framework
* update language version
---
...eManagement.Telemetry.ApplicationInsights.AspNetCore.csproj | 2 +-
...soft.FeatureManagement.Telemetry.ApplicationInsights.csproj | 3 ++-
.../Microsoft.FeatureManagement.csproj | 2 +-
3 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj
index b1cfaaf3..bdca8b9a 100644
--- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj
+++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj
@@ -12,7 +12,7 @@
- net6.0
+ net6.0;net7.0;net8.0
enable
true
false
diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj
index 5c892bc9..10da71c0 100644
--- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj
+++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj
@@ -12,11 +12,12 @@
- net6.0
+ netstandard2.0;netstandard2.1
enable
true
false
..\..\build\Microsoft.FeatureManagement.snk
+ 10.0
diff --git a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj
index 01386d2b..46bb4928 100644
--- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj
+++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj
@@ -16,7 +16,7 @@
true
false
..\..\build\Microsoft.FeatureManagement.snk
- 9.0
+ 10.0
From 00bb4f943f1ba740fc4d4fdced45f24c870a63f5 Mon Sep 17 00:00:00 2001
From: Ross Grambo
Date: Thu, 4 Apr 2024 10:44:18 -0700
Subject: [PATCH 20/25] Adjusts namespace to simply ~.Telemetry
---
examples/EvaluationDataToApplicationInsights/Program.cs | 4 ++--
.../TargetingTelemetryInitializer.cs | 2 +-
.../ApplicationInsightsTelemetryPublisher.cs | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/examples/EvaluationDataToApplicationInsights/Program.cs b/examples/EvaluationDataToApplicationInsights/Program.cs
index 4b57a4d1..23bf3ac2 100644
--- a/examples/EvaluationDataToApplicationInsights/Program.cs
+++ b/examples/EvaluationDataToApplicationInsights/Program.cs
@@ -1,11 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
-using Microsoft.FeatureManagement.Telemetry.ApplicationInsights;
+using Microsoft.FeatureManagement.Telemetry;
using Microsoft.FeatureManagement;
using EvaluationDataToApplicationInsights;
using Microsoft.ApplicationInsights.Extensibility;
-using Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore;
+using Microsoft.FeatureManagement.Telemetry.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs
index e01bd9e4..efba932b 100644
--- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs
+++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs
@@ -7,7 +7,7 @@
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.AspNetCore.Http;
-namespace Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore
+namespace Microsoft.FeatureManagement.Telemetry.AspNetCore
{
///
/// Used to add targeting information to outgoing Application Insights telemetry.
diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsTelemetryPublisher.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsTelemetryPublisher.cs
index 450f95e9..20b05299 100644
--- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsTelemetryPublisher.cs
+++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsTelemetryPublisher.cs
@@ -4,7 +4,7 @@
using Microsoft.ApplicationInsights;
using System.Diagnostics;
-namespace Microsoft.FeatureManagement.Telemetry.ApplicationInsights
+namespace Microsoft.FeatureManagement.Telemetry
{
///
/// Used to publish data from evaluation events to Application Insights
From 8ac41a884202790a24a1ce5b5c563950e4ab4d48 Mon Sep 17 00:00:00 2001
From: Ross Grambo
Date: Thu, 4 Apr 2024 10:53:23 -0700
Subject: [PATCH 21/25] Adjusts ApplicationInsights extension methods namespace
---
.../TelemetryClientExtensions.cs | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/TelemetryClientExtensions.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/TelemetryClientExtensions.cs
index b397519b..3fa0b8f7 100644
--- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/TelemetryClientExtensions.cs
+++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/TelemetryClientExtensions.cs
@@ -1,11 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
-using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.FeatureManagement.FeatureFilters;
-namespace Microsoft.FeatureManagement.Telemetry.ApplicationInsights
+namespace Microsoft.ApplicationInsights
{
///
/// Provides extension methods for tracking events with .
From d17f9b9c7c64e54ea1c44f9964e02a6cade4feaf Mon Sep 17 00:00:00 2001
From: Ross Grambo
Date: Fri, 5 Apr 2024 14:26:05 -0700
Subject: [PATCH 22/25] Adjusts class name of TelemetryClientExceptions
---
.../TelemetryClientExtensions.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/TelemetryClientExtensions.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/TelemetryClientExtensions.cs
index 3fa0b8f7..0f837a7d 100644
--- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/TelemetryClientExtensions.cs
+++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/TelemetryClientExtensions.cs
@@ -9,7 +9,7 @@ namespace Microsoft.ApplicationInsights
///
/// Provides extension methods for tracking events with .
///
- public static class TelemetryClientExtensions
+ public static class FeatureManagementTelemetryClientExtensions
{
///
/// Extension method to track an event with .
From 038883cd0b03b671f176ea0a4dd2519f3695d235 Mon Sep 17 00:00:00 2001
From: Ross Grambo
Date: Fri, 5 Apr 2024 14:40:07 -0700
Subject: [PATCH 23/25] Removed changes to
FM.Telemetry.ApplicationInsights.AspNetCore namespace
---
examples/EvaluationDataToApplicationInsights/Program.cs | 2 +-
.../TargetingTelemetryInitializer.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/examples/EvaluationDataToApplicationInsights/Program.cs b/examples/EvaluationDataToApplicationInsights/Program.cs
index 23bf3ac2..84253a85 100644
--- a/examples/EvaluationDataToApplicationInsights/Program.cs
+++ b/examples/EvaluationDataToApplicationInsights/Program.cs
@@ -5,7 +5,7 @@
using Microsoft.FeatureManagement;
using EvaluationDataToApplicationInsights;
using Microsoft.ApplicationInsights.Extensibility;
-using Microsoft.FeatureManagement.Telemetry.AspNetCore;
+using Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs
index efba932b..e01bd9e4 100644
--- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs
+++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs
@@ -7,7 +7,7 @@
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.AspNetCore.Http;
-namespace Microsoft.FeatureManagement.Telemetry.AspNetCore
+namespace Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore
{
///
/// Used to add targeting information to outgoing Application Insights telemetry.
From cd5cfcbff185a9ade71491660a19c0351f59f91d Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Sun, 7 Apr 2024 10:37:06 +0800
Subject: [PATCH 24/25] Ensure the consistency of targeting evaluation across
CPU architectures. (#405)
* check system endianness
* remove period
* update reverse method
* update comment
* adjust comment
---
.../Targeting/TargetingEvaluator.cs | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs
index a2249529..726ac185 100644
--- a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs
+++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs
@@ -338,14 +338,23 @@ private static bool IsTargeted(string contextId, double from, double to)
{
byte[] hash;
+ //
+ // Cryptographic hashing algorithms ensure adequate entropy across hash
using (HashAlgorithm hashAlgorithm = SHA256.Create())
{
hash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(contextId));
}
+
//
- // Use first 4 bytes for percentage calculation
- // Cryptographic hashing algorithms ensure adequate entropy across hash
+ // Endianness check ensures the consistency of targeting evaluation result across different architectures
+ if (!BitConverter.IsLittleEndian)
+ {
+ Array.Reverse(hash, 0, 4);
+ }
+
+ //
+ // The first 4 bytes of the hash will be used for percentage calculation
uint contextMarker = BitConverter.ToUInt32(hash, 0);
double contextPercentage = (contextMarker / (double)uint.MaxValue) * 100;
From 5319701249c7dd706d25ec51ea5f4da4c80798a5 Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Tue, 9 Apr 2024 16:38:16 +0800
Subject: [PATCH 25/25] version bump (#420)
---
.../Microsoft.FeatureManagement.AspNetCore.csproj | 2 +-
...reManagement.Telemetry.ApplicationInsights.AspNetCore.csproj | 2 +-
...osoft.FeatureManagement.Telemetry.ApplicationInsights.csproj | 2 +-
.../Microsoft.FeatureManagement.csproj | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj
index 84a6fd94..c60a6ef7 100644
--- a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj
+++ b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj
@@ -6,7 +6,7 @@
4
0
0
- -preview2
+ -preview3
diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj
index bdca8b9a..a720ef25 100644
--- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj
+++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj
@@ -6,7 +6,7 @@
4
0
0
- -preview2
+ -preview3
diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj
index 10da71c0..708d97c5 100644
--- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj
+++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj
@@ -6,7 +6,7 @@
4
0
0
- -preview2
+ -preview3
diff --git a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj
index 46bb4928..e3bd6025 100644
--- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj
+++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj
@@ -6,7 +6,7 @@
4
0
0
- -preview2
+ -preview3