-
Notifications
You must be signed in to change notification settings - Fork 126
Support Microsoft Feature Flag schema feature flag schema v1.1.0 by default #319
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
Changes from 27 commits
9069aad
1547ee5
8a8ca97
e4f89f7
b5b859d
7933989
cd3b3f5
1270e37
da89f81
bae3270
8c13231
f54b66a
c4cfcf3
8baa4c7
7d73b06
dca69d3
9d21987
729af66
1e41ac1
66e3841
646be65
ed5ba39
fb75dfa
5048f80
a4805a3
421eb69
0f4a889
1a565a1
29a6aed
c0e9db6
b54142c
c12feef
ebf9bd2
05dd60a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT license. | ||
| // | ||
|
|
||
| namespace Microsoft.FeatureManagement | ||
| { | ||
| // | ||
| // Azure App Configuration feature flag schema: https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureFlag.v1.1.0.schema.json | ||
| internal static class AzureAppConfigurationFeatureFlagFields | ||
| { | ||
| public const string FeatureFlagsSectionName = "FeatureFlags"; | ||
|
|
||
| // | ||
| // Feature flag keywords | ||
| public const string Id = "id"; | ||
| public const string Enabled = "enabled"; | ||
| public const string Conditions = "conditions"; | ||
| public const string ClientFilters = "client_filters"; | ||
| public const string RequirementType = "requirement_type"; | ||
|
|
||
| // | ||
| // Client filter keywords | ||
| public const string Name = "name"; | ||
| public const string Parameters = "parameters"; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -22,13 +22,13 @@ public sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionP | |||||||||
| // IFeatureDefinitionProviderCacheable interface is only used to mark this provider as cacheable. This allows our test suite's | ||||||||||
| // provider to be marked for caching as well. | ||||||||||
|
|
||||||||||
| private const string FeatureFiltersSectionName = "EnabledFor"; | ||||||||||
| private const string RequirementTypeKeyword = "RequirementType"; | ||||||||||
| private const string FeatureManagementSectionName = "FeatureManagement"; | ||||||||||
| private readonly IConfiguration _configuration; | ||||||||||
| private readonly ConcurrentDictionary<string, FeatureDefinition> _definitions; | ||||||||||
| private IDisposable _changeSubscription; | ||||||||||
| private int _stale = 0; | ||||||||||
| private long _initialized = 0; | ||||||||||
| private bool _azureAppConfigurationFeatureFlagSchemaEnabled; | ||||||||||
| private readonly object _lock = new object(); | ||||||||||
|
|
||||||||||
| /// <summary> | ||||||||||
| /// Creates a configuration feature definition provider. | ||||||||||
|
|
@@ -81,6 +81,8 @@ public Task<FeatureDefinition> 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(); | ||||||||||
|
|
@@ -104,6 +106,8 @@ public Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName) | |||||||||
| public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync() | ||||||||||
| #pragma warning restore CS1998 | ||||||||||
| { | ||||||||||
| EnsureInit(); | ||||||||||
|
|
||||||||||
| if (Interlocked.Exchange(ref _stale, 0) != 0) | ||||||||||
| { | ||||||||||
| _definitions.Clear(); | ||||||||||
|
|
@@ -113,16 +117,58 @@ public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync() | |||||||||
| // Iterate over all features registered in the system at initial invocation time | ||||||||||
| foreach (IConfigurationSection featureSection in GetFeatureDefinitionSections()) | ||||||||||
| { | ||||||||||
| string featureName = GetFeatureFlagSectionName(featureSection); | ||||||||||
|
|
||||||||||
| if (string.IsNullOrEmpty(featureName)) | ||||||||||
| { | ||||||||||
| continue; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // | ||||||||||
| // Underlying IConfigurationSection data is dynamic so latest feature definitions are returned | ||||||||||
| yield return _definitions.GetOrAdd(featureSection.Key, (_) => ReadFeatureDefinition(featureSection)); | ||||||||||
| yield return _definitions.GetOrAdd(featureName, (_) => ReadFeatureDefinition(featureSection)); | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private void EnsureInit() | ||||||||||
| { | ||||||||||
| if (!_configuration.GetChildren().Any()) | ||||||||||
| { | ||||||||||
| Logger?.LogDebug($"Configuration is empty."); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| bool hasFeatureManagementSection = _configuration.GetChildren() | ||||||||||
| .Any(section => string.Equals(section.Key, ConfigurationFields.FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)); | ||||||||||
|
|
||||||||||
| if (!hasFeatureManagementSection && !RootConfigurationFallbackEnabled) | ||||||||||
| { | ||||||||||
| Logger?.LogDebug($"No configuration section named '{ConfigurationFields.FeatureManagementSectionName}' was found."); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (_initialized == 0) | ||||||||||
| { | ||||||||||
| IConfiguration featureManagementConfigurationSection = hasFeatureManagementSection ? | ||||||||||
| _configuration.GetSection(ConfigurationFields.FeatureManagementSectionName) : | ||||||||||
| _configuration; | ||||||||||
|
|
||||||||||
| bool hasAzureAppConfigurationFeatureFlagSchema = HasAzureAppConfigurationFeatureFlagSchema(featureManagementConfigurationSection); | ||||||||||
|
|
||||||||||
| lock (_lock) | ||||||||||
| { | ||||||||||
| if (Interlocked.Read(ref _initialized) == 0) | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lock can sure only one thread can enter this block, what is the reason for using Mark int32 type There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or use is also fine.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interlocked exchange does perform an atomic swap, but it also has the added benefit of forcing a read from memory. Here, interlocked avoids cached read from CPU. Marking the variable as volatile will have lasting effect throughout the lifetime of the application whereas interlocked usage in the init check only happens during initialization. |
||||||||||
| { | ||||||||||
| _azureAppConfigurationFeatureFlagSchemaEnabled = hasAzureAppConfigurationFeatureFlagSchema; | ||||||||||
|
|
||||||||||
| Interlocked.Exchange(ref _initialized, 1); | ||||||||||
| } | ||||||||||
|
zhiyuanliang-ms marked this conversation as resolved.
|
||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private FeatureDefinition ReadFeatureDefinition(string featureName) | ||||||||||
| { | ||||||||||
| IConfigurationSection configuration = GetFeatureDefinitionSections() | ||||||||||
| .FirstOrDefault(section => section.Key.Equals(featureName, StringComparison.OrdinalIgnoreCase)); | ||||||||||
| .FirstOrDefault(section => string.Equals(GetFeatureFlagSectionName(section), featureName, StringComparison.OrdinalIgnoreCase)); | ||||||||||
|
|
||||||||||
| if (configuration == null) | ||||||||||
| { | ||||||||||
|
|
@@ -133,6 +179,16 @@ private FeatureDefinition ReadFeatureDefinition(string featureName) | |||||||||
| } | ||||||||||
|
|
||||||||||
| private FeatureDefinition ReadFeatureDefinition(IConfigurationSection configurationSection) | ||||||||||
| { | ||||||||||
| if (_azureAppConfigurationFeatureFlagSchemaEnabled) | ||||||||||
| { | ||||||||||
| return ParseAppConfigurationFeatureDefinition(configurationSection); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return ParseFeatureDefinition(configurationSection); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private FeatureDefinition ParseFeatureDefinition(IConfigurationSection configurationSection) | ||||||||||
| { | ||||||||||
| /* | ||||||||||
|
|
||||||||||
|
|
@@ -167,15 +223,17 @@ We support | |||||||||
|
|
||||||||||
| */ | ||||||||||
|
|
||||||||||
| RequirementType requirementType = RequirementType.Any; | ||||||||||
| string featureName = GetFeatureFlagSectionName(configurationSection); | ||||||||||
|
|
||||||||||
| var enabledFor = new List<FeatureFilterConfiguration>(); | ||||||||||
|
|
||||||||||
| RequirementType requirementType = RequirementType.Any; | ||||||||||
|
|
||||||||||
| string val = configurationSection.Value; // configuration[$"{featureName}"]; | ||||||||||
|
|
||||||||||
| if (string.IsNullOrEmpty(val)) | ||||||||||
| { | ||||||||||
| val = configurationSection[FeatureFiltersSectionName]; | ||||||||||
| val = configurationSection[ConfigurationFields.FeatureFiltersSectionName]; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (!string.IsNullOrEmpty(val) && bool.TryParse(val, out bool result) && result) | ||||||||||
|
|
@@ -193,64 +251,197 @@ We support | |||||||||
| } | ||||||||||
| else | ||||||||||
| { | ||||||||||
| string rawRequirementType = configurationSection[RequirementTypeKeyword]; | ||||||||||
| string rawRequirementType = configurationSection[ConfigurationFields.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 requirement type '{rawRequirementType}' for feature '{configurationSection.Key}'."); | ||||||||||
| FeatureManagementError.InvalidConfigurationSetting, | ||||||||||
| $"Invalid value '{rawRequirementType}' for field '{ConfigurationFields.RequirementType}' of feature '{featureName}'."); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| IEnumerable<IConfigurationSection> filterSections = configurationSection.GetSection(FeatureFiltersSectionName).GetChildren(); | ||||||||||
| IEnumerable<IConfigurationSection> filterSections = configurationSection.GetSection(ConfigurationFields.FeatureFiltersSectionName).GetChildren(); | ||||||||||
|
|
||||||||||
| foreach (IConfigurationSection section in filterSections) | ||||||||||
| { | ||||||||||
| // | ||||||||||
| // 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 i) && !string.IsNullOrEmpty(section[nameof(FeatureFilterConfiguration.Name)])) | ||||||||||
| if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[ConfigurationFields.NameKeyword])) | ||||||||||
| { | ||||||||||
| enabledFor.Add(new FeatureFilterConfiguration() | ||||||||||
| { | ||||||||||
| Name = section[nameof(FeatureFilterConfiguration.Name)], | ||||||||||
| Parameters = new ConfigurationWrapper(section.GetSection(nameof(FeatureFilterConfiguration.Parameters))) | ||||||||||
| Name = section[ConfigurationFields.NameKeyword], | ||||||||||
| Parameters = new ConfigurationWrapper(section.GetSection(ConfigurationFields.FeatureFilterConfigurationParameters)) | ||||||||||
| }); | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return new FeatureDefinition() | ||||||||||
| { | ||||||||||
| Name = configurationSection.Key, | ||||||||||
| Name = featureName, | ||||||||||
| EnabledFor = enabledFor, | ||||||||||
| RequirementType = requirementType | ||||||||||
| }; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private IEnumerable<IConfigurationSection> GetFeatureDefinitionSections() | ||||||||||
| private FeatureDefinition ParseAppConfigurationFeatureDefinition(IConfigurationSection configurationSection) | ||||||||||
| { | ||||||||||
| /* | ||||||||||
|
|
||||||||||
| If Azure App Configuration feature flag schema is enabled, we support | ||||||||||
|
|
||||||||||
| FeatureFlags: [ | ||||||||||
| { | ||||||||||
| id: "myFeature", | ||||||||||
| enabled: true, | ||||||||||
| conditions: { | ||||||||||
| client_filters: ["myFeatureFilter1", "myFeatureFilter2"], | ||||||||||
| requirement_type: "All", | ||||||||||
| } | ||||||||||
| }, | ||||||||||
| { | ||||||||||
| id: "myAlwaysEnabledFeature", | ||||||||||
| enabled: true, | ||||||||||
| conditions: { | ||||||||||
| client_filters: [], | ||||||||||
| } | ||||||||||
| }, | ||||||||||
| { | ||||||||||
| id: "myAlwaysDisabledFeature", | ||||||||||
| enabled: false, | ||||||||||
| } | ||||||||||
| ] | ||||||||||
|
|
||||||||||
| */ | ||||||||||
|
|
||||||||||
| string featureName = GetFeatureFlagSectionName(configurationSection); | ||||||||||
|
|
||||||||||
| var enabledFor = new List<FeatureFilterConfiguration>(); | ||||||||||
|
|
||||||||||
| RequirementType requirementType = RequirementType.Any; | ||||||||||
|
|
||||||||||
| IConfigurationSection conditions = configurationSection.GetSection(AzureAppConfigurationFeatureFlagFields.Conditions); | ||||||||||
|
|
||||||||||
| string rawRequirementType = conditions[AzureAppConfigurationFeatureFlagFields.RequirementType]; | ||||||||||
|
|
||||||||||
| // | ||||||||||
| // Look for feature definitions under the "FeatureManagement" section | ||||||||||
| IConfigurationSection featureManagementConfigurationSection = _configuration.GetSection(FeatureManagementSectionName); | ||||||||||
| // 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 '{AzureAppConfigurationFeatureFlagFields.RequirementType}' of feature '{featureName}'."); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| string rawEnabled = configurationSection[AzureAppConfigurationFeatureFlagFields.Enabled]; | ||||||||||
|
|
||||||||||
| bool enabled = false; | ||||||||||
|
jimmyca15 marked this conversation as resolved.
|
||||||||||
|
|
||||||||||
| if (featureManagementConfigurationSection.Exists()) | ||||||||||
| if (!string.IsNullOrEmpty(rawEnabled) && !bool.TryParse(rawEnabled, out enabled)) | ||||||||||
| { | ||||||||||
| return featureManagementConfigurationSection.GetChildren(); | ||||||||||
| throw new FeatureManagementException( | ||||||||||
| FeatureManagementError.InvalidConfigurationSetting, | ||||||||||
| $"Invalid value '{rawEnabled}' for field '{AzureAppConfigurationFeatureFlagFields.Enabled}' of feature '{featureName}'."); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // | ||||||||||
| // There is no "FeatureManagement" section in the configuration | ||||||||||
| if (RootConfigurationFallbackEnabled) | ||||||||||
| if (enabled) | ||||||||||
| { | ||||||||||
| IEnumerable<IConfigurationSection> filterSections = conditions.GetSection(AzureAppConfigurationFeatureFlagFields.ClientFilters).GetChildren(); | ||||||||||
|
|
||||||||||
| if (filterSections.Any()) | ||||||||||
| { | ||||||||||
| foreach (IConfigurationSection section in filterSections) | ||||||||||
| { | ||||||||||
| // | ||||||||||
| // 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[AzureAppConfigurationFeatureFlagFields.Name])) | ||||||||||
| { | ||||||||||
| enabledFor.Add(new FeatureFilterConfiguration() | ||||||||||
| { | ||||||||||
| Name = section[AzureAppConfigurationFeatureFlagFields.Name], | ||||||||||
| Parameters = new ConfigurationWrapper(section.GetSection(AzureAppConfigurationFeatureFlagFields.Parameters)) | ||||||||||
| }); | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
| else | ||||||||||
| { | ||||||||||
| enabledFor.Add(new FeatureFilterConfiguration | ||||||||||
| { | ||||||||||
| Name = "AlwaysOn" | ||||||||||
| }); | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return new FeatureDefinition() | ||||||||||
| { | ||||||||||
| Name = featureName, | ||||||||||
| EnabledFor = enabledFor, | ||||||||||
| RequirementType = requirementType | ||||||||||
| }; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private string GetFeatureFlagSectionName(IConfigurationSection section) | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or
Suggested change
Given the caller uses a variable named |
||||||||||
| { | ||||||||||
| if (_azureAppConfigurationFeatureFlagSchemaEnabled) | ||||||||||
| { | ||||||||||
| return _configuration.GetChildren(); | ||||||||||
| return section[AzureAppConfigurationFeatureFlagFields.Id]; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| Logger?.LogDebug($"No configuration section named '{FeatureManagementSectionName}' was found."); | ||||||||||
| return section.Key; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private IEnumerable<IConfigurationSection> GetFeatureDefinitionSections() | ||||||||||
| { | ||||||||||
| if (!_configuration.GetChildren().Any()) | ||||||||||
| { | ||||||||||
| return Enumerable.Empty<IConfigurationSection>(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| bool hasFeatureManagementSection = _configuration.GetChildren() | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better to get the section with FirstOrDefault, rather than just check for it's existence, else you will just need to get it again later. |
||||||||||
| .Any(section => string.Equals(section.Key, ConfigurationFields.FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)); | ||||||||||
|
|
||||||||||
| if (!hasFeatureManagementSection && !RootConfigurationFallbackEnabled) | ||||||||||
| { | ||||||||||
| return Enumerable.Empty<IConfigurationSection>(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| IConfiguration featureManagementConfigurationSection = hasFeatureManagementSection ? | ||||||||||
| _configuration.GetSection(ConfigurationFields.FeatureManagementSectionName) : | ||||||||||
| _configuration; | ||||||||||
|
|
||||||||||
| if (_azureAppConfigurationFeatureFlagSchemaEnabled) | ||||||||||
| { | ||||||||||
| IConfigurationSection featureFlagsSection = featureManagementConfigurationSection.GetSection(AzureAppConfigurationFeatureFlagFields.FeatureFlagsSectionName); | ||||||||||
|
|
||||||||||
| return featureFlagsSection.GetChildren(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return featureManagementConfigurationSection.GetChildren(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private static bool HasAzureAppConfigurationFeatureFlagSchema(IConfiguration featureManagementConfiguration) | ||||||||||
| { | ||||||||||
| bool hasFeatureFlagsSection = featureManagementConfiguration.GetChildren() | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A few suggestions in this section. Get the section while checking for it's existence. Check if feature flags section has any children while doing an all check. |
||||||||||
| .Any(section => string.Equals(section.Key, AzureAppConfigurationFeatureFlagFields.FeatureFlagsSectionName, StringComparison.OrdinalIgnoreCase)); | ||||||||||
|
|
||||||||||
| if (hasFeatureFlagsSection) | ||||||||||
| { | ||||||||||
| IConfigurationSection featureFlagsConfigurationSection = featureManagementConfiguration.GetSection(AzureAppConfigurationFeatureFlagFields.FeatureFlagsSectionName); | ||||||||||
|
|
||||||||||
| // | ||||||||||
| // FeatureFlags section is an array | ||||||||||
| return string.IsNullOrEmpty(featureFlagsConfigurationSection.Value) && | ||||||||||
| featureFlagsConfigurationSection.GetChildren() | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is intended. If there is "FeatureFlags" section, either it is empty or an array, we consider it as using the App Config schema. |
||||||||||
| .All(section => int.TryParse(section.Key, out int _)); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return Enumerable.Empty<IConfigurationSection>(); | ||||||||||
| return false; | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT license. | ||
| // | ||
|
|
||
| namespace Microsoft.FeatureManagement | ||
| { | ||
| internal static class ConfigurationFields | ||
| { | ||
| // Enum keywords | ||
| public const string RequirementType = "RequirementType"; | ||
|
|
||
| // Feature filters keywords | ||
| public const string FeatureFiltersSectionName = "EnabledFor"; | ||
| public const string FeatureFilterConfigurationParameters = "Parameters"; | ||
|
|
||
| // Other keywords | ||
| public const string NameKeyword = "Name"; | ||
| public const string FeatureManagementSectionName = "FeatureManagement"; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.