From a9f9a834f8caecb4f5da7d92ff1013a3e6f99002 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 23 Feb 2024 15:07:06 +0800 Subject: [PATCH 1/5] support microsoft feature flag schema v2 --- .../ConfigurationFeatureDefinitionProvider.cs | 129 +++++++++++++++++- .../ConfigurationFields.cs | 2 +- .../MicrosoftFeatureManagementFields.cs | 25 ++++ tests/Tests.FeatureManagement/Features.cs | 1 + .../MicrosoftFeatureManagement.json | 68 +++++++++ .../MicrosoftFeatureManagementSchema.cs | 54 ++++++++ 6 files changed, 271 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index c823a1d0..955efa6c 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,116 @@ 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/Features.cs b/tests/Tests.FeatureManagement/Features.cs index c0819716..b898bb0d 100644 --- a/tests/Tests.FeatureManagement/Features.cs +++ b/tests/Tests.FeatureManagement/Features.cs @@ -31,5 +31,6 @@ static class Features public const string VariantFeatureInvalidStatusOverride = "VariantFeatureInvalidStatusOverride"; public const string VariantFeatureInvalidFromTo = "VariantFeatureInvalidFromTo"; public const string VariantImplementationFeature = "VariantImplementationFeature"; + public const string VariantTestFeature = "VariantTestFeature"; } } diff --git a/tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json b/tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json index b3c8db10..c1cb2de1 100644 --- a/tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json +++ b/tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json @@ -1,4 +1,10 @@ { + "ShoppingCart": { + "Big": { + "Size": 600, + "Color": "green" + } + }, "feature_management": { "feature_flags": [ { @@ -71,6 +77,68 @@ } ] } + }, + { + "id": "AlwaysOnTestFeature", + "enabled": true, + "telemetry": { + "enabled": true, + "metadata": { + "Tags.Tag1": "Tag1Value", + "Tags.Tag2": "Tag2Value", + "Etag": "EtagValue", + "Label": "LabelValue" + } + }, + "conditions": { + "client_filters": [ + { + "name": "AlwaysOn" + } + ] + } + }, + { + "id": "VariantTestFeature", + "variants": [ + { + "name": "Small", + "configuration_value": "300px" + }, + { + "name": "Big", + "configuration_reference": "ShoppingCart:Big", + "status_override": "Disabled" + } + ], + "allocation": { + "default_when_enabled": "Small", + "default_when_disabled": "Big", + "percentile": [ + { + "variant": "Small", + "from": 0, + "to": 50 + } + ], + "user": [ + { + "variant": "Small", + "users": [ + "Jeff" + ] + } + ], + "group": [ + { + "variant": "Big", + "groups": [ + "Group1" + ] + } + ], + "seed": 12345 + } } ] } diff --git a/tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs b/tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs index 869d3399..b215136e 100644 --- a/tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs +++ b/tests/Tests.FeatureManagement/MicrosoftFeatureManagementSchema.cs @@ -66,6 +66,60 @@ 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.VariantTestFeature); + + Assert.NotNull(featureDefinition); + + Assert.Equal("Small", featureDefinition.Allocation.DefaultWhenEnabled); + + Assert.Equal("Big", featureDefinition.Allocation.DefaultWhenDisabled); + + Assert.Equal("Small", featureDefinition.Allocation.User.First().Variant); + + Assert.Equal("Jeff", featureDefinition.Allocation.User.First().Users.First()); + + Assert.Equal("Big", featureDefinition.Allocation.Group.First().Variant); + + Assert.Equal("Group1", featureDefinition.Allocation.Group.First().Groups.First()); + + Assert.Equal("Small", featureDefinition.Allocation.Percentile.First().Variant); + + Assert.Equal(0, featureDefinition.Allocation.Percentile.First().From); + + Assert.Equal(50, featureDefinition.Allocation.Percentile.First().To); + + Assert.Equal("12345", featureDefinition.Allocation.Seed); + + 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); + + 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); } [Fact] From 46f625b97e3efc96ba21c8eeaa9f43efdbd6b8f5 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 23 Feb 2024 16:42:53 +0800 Subject: [PATCH 2/5] add more testcases --- .../FeatureManagement.cs | 1 - tests/Tests.FeatureManagement/Features.cs | 1 - .../MicrosoftFeatureManagement.json | 238 ++++++++++++++- .../MicrosoftFeatureManagementSchema.cs | 275 +++++++++++++++++- .../Tests.FeatureManagement/appsettings.json | 8 - 5 files changed, 483 insertions(+), 40 deletions(-) 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/Features.cs b/tests/Tests.FeatureManagement/Features.cs index b898bb0d..c0819716 100644 --- a/tests/Tests.FeatureManagement/Features.cs +++ b/tests/Tests.FeatureManagement/Features.cs @@ -31,6 +31,5 @@ static class Features public const string VariantFeatureInvalidStatusOverride = "VariantFeatureInvalidStatusOverride"; public const string VariantFeatureInvalidFromTo = "VariantFeatureInvalidFromTo"; public const string VariantImplementationFeature = "VariantImplementationFeature"; - public const string VariantTestFeature = "VariantTestFeature"; } } diff --git a/tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json b/tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json index c1cb2de1..24b4329c 100644 --- a/tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json +++ b/tests/Tests.FeatureManagement/MicrosoftFeatureManagement.json @@ -3,6 +3,10 @@ "Big": { "Size": 600, "Color": "green" + }, + "Small": { + "Size": 300, + "Color": "gray" } }, "feature_management": { @@ -89,56 +93,258 @@ "Etag": "EtagValue", "Label": "LabelValue" } + } + }, + { + "id": "VariantFeatureDefaultEnabled", + "enabled": true, + "telemetry": { + "enabled": true }, - "conditions": { - "client_filters": [ + "allocation": { + "default_when_enabled": "Medium", + "user": [ { - "name": "AlwaysOn" + "variant": "Small", + "users": [ + "Jeff" + ] } ] - } + }, + "variants": [ + { + "name": "Medium", + "configuration_value": { + "Size": "450px", + "Color": "Purple" + } + }, + { + "name": "Small", + "configuration_value": "300px" + } + ] }, { - "id": "VariantTestFeature", + "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": { - "default_when_enabled": "Small", - "default_when_disabled": "Big", "percentile": [ { - "variant": "Small", + "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": [ - "Jeff" + "Marsha" ] } - ], + ] + }, + "variants": [ + { + "name": "Small", + "configuration_value": "300px" + } + ] + }, + { + "id": "VariantFeatureGroup", + "enabled": true, + "telemetry": { + "enabled": true + }, + "allocation": { "group": [ { - "variant": "Big", + "variant": "Small", "groups": [ "Group1" ] } - ], - "seed": 12345 - } + ] + }, + "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 b215136e..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; @@ -81,37 +86,41 @@ public async Task ReadsFeatureDefinition() Assert.Equal("LabelValue", featureDefinition.Telemetry.Metadata["Label"]); - featureDefinition = await featureDefinitionProvider.GetFeatureDefinitionAsync(Features.VariantTestFeature); + featureDefinition = await featureDefinitionProvider.GetFeatureDefinitionAsync(Features.VariantFeatureDefaultEnabled); Assert.NotNull(featureDefinition); - Assert.Equal("Small", featureDefinition.Allocation.DefaultWhenEnabled); - - Assert.Equal("Big", featureDefinition.Allocation.DefaultWhenDisabled); + Assert.Equal("Medium", featureDefinition.Allocation.DefaultWhenEnabled); Assert.Equal("Small", featureDefinition.Allocation.User.First().Variant); Assert.Equal("Jeff", featureDefinition.Allocation.User.First().Users.First()); - Assert.Equal("Big", featureDefinition.Allocation.Group.First().Variant); + VariantDefinition smallVariant = featureDefinition.Variants.FirstOrDefault(variant => string.Equals(variant.Name, "Small")); - Assert.Equal("Group1", featureDefinition.Allocation.Group.First().Groups.First()); + Assert.NotNull(smallVariant); - Assert.Equal("Small", featureDefinition.Allocation.Percentile.First().Variant); + Assert.Equal("300px", smallVariant.ConfigurationValue.Value); - Assert.Equal(0, featureDefinition.Allocation.Percentile.First().From); + Assert.Equal(StatusOverride.None, smallVariant.StatusOverride); - Assert.Equal(50, featureDefinition.Allocation.Percentile.First().To); + featureDefinition = await featureDefinitionProvider.GetFeatureDefinitionAsync(Features.VariantFeatureStatusDisabled); - Assert.Equal("12345", featureDefinition.Allocation.Seed); + Assert.NotNull(featureDefinition); - VariantDefinition smallVariant = featureDefinition.Variants.FirstOrDefault(variant => string.Equals(variant.Name, "Small")); + Assert.Equal("Small", featureDefinition.Allocation.DefaultWhenDisabled); - Assert.NotNull(smallVariant); + featureDefinition = await featureDefinitionProvider.GetFeatureDefinitionAsync(Features.VariantFeaturePercentileOn); - Assert.Equal("300px", smallVariant.ConfigurationValue.Value); + Assert.NotNull(featureDefinition); - Assert.Equal(StatusOverride.None, smallVariant.StatusOverride); + 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")); @@ -120,6 +129,14 @@ public async Task ReadsFeatureDefinition() 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] @@ -158,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 cea3cdddabef77ec48d84d33ecbf1d5f92c03241 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Wed, 28 Feb 2024 12:29:15 +0800 Subject: [PATCH 3/5] add empty lines --- .../ConfigurationFeatureDefinitionProvider.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 955efa6c..db08fae2 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -506,17 +506,23 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection 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 6f69885c388d3389ef4eee194f11228a212800fb Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 29 Feb 2024 16:20:29 +0800 Subject: [PATCH 4/5] add schema & update README --- README.md | 100 ++++-- ...eatureManagement.Dotnet.v2.0.0.schema.json | 303 ++++++++++++++++++ 2 files changed, 381 insertions(+), 22 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..75b8c4c4 --- /dev/null +++ b/schemas/FeatureManagement.Dotnet.v2.0.0.schema.json @@ -0,0 +1,303 @@ +{ + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "A .NET Feature Management Configuration", + "required": [ + "FeatureManagement" + ], + "properties":{ + "FeatureManagement": { + "type": "object", + "title": "Feature Management", + "description": "Declares feature management configuration.", + "required": [], + "patternProperties": { + "^[^:]*$": { + "description": "Declares a feature flag.", + "anyOf": [ + { + "type": "boolean", + "title": "On/Off Feature Flag", + "description": "A feature flag that always returns the same value." + }, + { + "type": "object", + "title": "Conditional Feature Flag", + "description": "A feature flag which value is dynamic based on a set of feature filters", + "required": [ + "EnabledFor" + ], + "properties": { + "RequirementType": { + "type": "string", + "title": "Requirement Type", + "description": "Determines whether any or all registered feature filters must be enabled for the feature to be considered enabled.", + "enum": [ + "Any", + "All" + ], + "default": "Any" + }, + "EnabledFor": { + "oneOf": [ + { + "type": "array", + "title": "Feature Filter Collection", + "description": "Feature filters that are evaluated to conditionally enable the flag.", + "items": { + "type": "object", + "title": "Feature Filter Declaration", + "required": [ + "Name" + ], + "properties": { + "Name": { + "type": "string", + "title": "Feature Filter Name", + "description": "The name used to refer to and require a feature filter.", + "default": "", + "examples": [ + "Percentage", + "TimeWindow" + ], + "pattern": "^[^:]*$" + }, + "Parameters": { + "type": "object", + "title": "Feature Filter Parameters", + "description": "Custom parameters for a given feature filter. A feature filter can require any set of parameters of any type.", + "required": [], + "patternProperties": { + "^.*$": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + }, + { + "type": "object" + }, + { + "type": "number" + }, + { + "type": "array" + }, + { + "type": "boolean" + } + ] + } + } + } + } + } + }, + { + "type": "boolean" + } + ] + }, + "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 + } + } + } + } + } + ] + } + } + } + } +} From c26d21221f4e353d4833d07bb353e5117ab888d6 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 1 Mar 2024 16:35:26 +0800 Subject: [PATCH 5/5] update schema --- schemas/FeatureManagement.Dotnet.v2.0.0.schema.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/schemas/FeatureManagement.Dotnet.v2.0.0.schema.json b/schemas/FeatureManagement.Dotnet.v2.0.0.schema.json index 75b8c4c4..7d25cf06 100644 --- a/schemas/FeatureManagement.Dotnet.v2.0.0.schema.json +++ b/schemas/FeatureManagement.Dotnet.v2.0.0.schema.json @@ -29,6 +29,16 @@ "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",