Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Microsoft Feature Management schema for main branch #370

Merged
merged 8 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -134,21 +134,38 @@ private void EnsureInit()
{
if (_initialized == 0)
{
bool hasMicrosoftFeatureFlagSchema = false;

IConfiguration featureManagementConfigurationSection = _configuration
.GetChildren()
.FirstOrDefault(section =>
string.Equals(
section.Key,
MicrosoftFeatureManagementFields.FeatureManagementSectionName,
StringComparison.OrdinalIgnoreCase));

if (featureManagementConfigurationSection != null)
{
hasMicrosoftFeatureFlagSchema = true;
}
else
{
featureManagementConfigurationSection = _configuration
.GetChildren()
.FirstOrDefault(section =>
string.Equals(
section.Key,
ConfigurationFields.FeatureManagementSectionName,
StringComparison.OrdinalIgnoreCase));
}

if (featureManagementConfigurationSection == null && RootConfigurationFallbackEnabled)
{
featureManagementConfigurationSection = _configuration;
}

zhiyuanliang-ms marked this conversation as resolved.
Show resolved Hide resolved
bool hasMicrosoftFeatureFlagSchema = featureManagementConfigurationSection != null &&
HasMicrosoftFeatureFlagSchema(featureManagementConfigurationSection);
hasMicrosoftFeatureFlagSchema = featureManagementConfigurationSection != null &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to fallback for Microsoft Feature Flag schema? I don't think we would ever have it without the feature_management wrappers. But I suppose we could support customers who want to do that? I prefer we don't support it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jimmyca15
I am kind of convinced by Ross. I agree that we might never have it without the "feature_management" wrappers.

In the Microsoft Feature Management schema, "feature_management" is a required property. So I think we should not support fallback for it.

Previously, "FeatureFlags" array was under "FeatureManagement" section. With the complexity brought by the RootConfigurationFallBack behavior, we have to use the "EnsureInit" pattern. Now, the Microsoft Feature Management schema is under "feature_management", we can tell which schema is being used in the constructor. We can simplify the code if we don't support the RootConfigurationFallBack for Microsoft Feature Management schema.

HasMicrosoftFeatureFlagSchema(featureManagementConfigurationSection);
}

lock (_lock)
{
Expand Down Expand Up @@ -321,33 +338,33 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection

RequirementType requirementType = RequirementType.Any;

IConfigurationSection conditions = configurationSection.GetSection(MicrosoftFeatureFlagFields.Conditions);

string rawRequirementType = conditions[MicrosoftFeatureFlagFields.RequirementType];

//
// If requirement type is specified, parse it and set the requirementType variable
if (!string.IsNullOrEmpty(rawRequirementType) && !Enum.TryParse(rawRequirementType, ignoreCase: true, out requirementType))
{
throw new FeatureManagementException(
FeatureManagementError.InvalidConfigurationSetting,
$"Invalid value '{rawRequirementType}' for field '{MicrosoftFeatureFlagFields.RequirementType}' of feature '{featureName}'.");
}
IConfigurationSection conditions = configurationSection.GetSection(MicrosoftFeatureManagementFields.Conditions);

string rawEnabled = configurationSection[MicrosoftFeatureFlagFields.Enabled];
string rawEnabled = configurationSection[MicrosoftFeatureManagementFields.Enabled];

bool enabled = false;

if (!string.IsNullOrEmpty(rawEnabled) && !bool.TryParse(rawEnabled, out enabled))
{
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<IConfigurationSection> 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<IConfigurationSection> filterSections = conditions.GetSection(MicrosoftFeatureManagementFields.ClientFilters).GetChildren();

if (filterSections.Any())
{
Expand All @@ -356,12 +373,12 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection
//
// Arrays in json such as "myKey": [ "some", "values" ]
// Are accessed through the configuration system by using the array index as the property name, e.g. "myKey": { "0": "some", "1": "values" }
if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[MicrosoftFeatureFlagFields.Name]))
if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[MicrosoftFeatureManagementFields.Name]))
{
enabledFor.Add(new FeatureFilterConfiguration()
{
Name = section[MicrosoftFeatureFlagFields.Name],
Parameters = new ConfigurationWrapper(section.GetSection(MicrosoftFeatureFlagFields.Parameters))
Name = section[MicrosoftFeatureManagementFields.Name],
Parameters = new ConfigurationWrapper(section.GetSection(MicrosoftFeatureManagementFields.Parameters))
});
}
}
Expand All @@ -387,7 +404,7 @@ private string GetFeatureName(IConfigurationSection section)
{
if (_microsoftFeatureFlagSchemaEnabled)
{
return section[MicrosoftFeatureFlagFields.Id];
return section[MicrosoftFeatureManagementFields.Id];
}

return section.Key;
Expand All @@ -407,7 +424,9 @@ private IEnumerable<IConfigurationSection> GetFeatureDefinitionSections()
.FirstOrDefault(section =>
string.Equals(
section.Key,
ConfigurationFields.FeatureManagementSectionName,
_microsoftFeatureFlagSchemaEnabled ?
MicrosoftFeatureManagementFields.FeatureManagementSectionName :
ConfigurationFields.FeatureManagementSectionName,
StringComparison.OrdinalIgnoreCase));

if (featureManagementConfigurationSection == null)
Expand All @@ -426,7 +445,7 @@ private IEnumerable<IConfigurationSection> GetFeatureDefinitionSections()

if (_microsoftFeatureFlagSchemaEnabled)
{
IConfigurationSection featureFlagsSection = featureManagementConfigurationSection.GetSection(MicrosoftFeatureFlagFields.FeatureFlagsSectionName);
IConfigurationSection featureFlagsSection = featureManagementConfigurationSection.GetSection(MicrosoftFeatureManagementFields.FeatureFlagsSectionName);

return featureFlagsSection.GetChildren();
}
Expand All @@ -441,7 +460,7 @@ private static bool HasMicrosoftFeatureFlagSchema(IConfiguration featureManageme
.FirstOrDefault(section =>
string.Equals(
section.Key,
MicrosoftFeatureFlagFields.FeatureFlagsSectionName,
MicrosoftFeatureManagementFields.FeatureFlagsSectionName,
StringComparison.OrdinalIgnoreCase));

if (featureFlagsConfigurationSection != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@

namespace Microsoft.FeatureManagement
{
//
// Microsoft feature flag schema: https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureFlag.v1.1.0.schema.json
internal static class MicrosoftFeatureFlagFields
internal static class MicrosoftFeatureManagementFields
{
zhiyuanliang-ms marked this conversation as resolved.
Show resolved Hide resolved
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";
Expand Down
196 changes: 0 additions & 196 deletions tests/Tests.FeatureManagement/FeatureManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IFeatureManager>();

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<IFeatureManager>();

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<IFeatureManager>();

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<IFeatureManager>();

Assert.True(await featureManager.IsEnabledAsync("MyFeature"));

Assert.True(await featureManager.IsEnabledAsync("FeatureFlags"));
}

[Fact]
public void AddsScopedFeatureManagement()
Expand Down
Loading