diff --git a/examples/FeatureFlagDemo/appsettings.json b/examples/FeatureFlagDemo/appsettings.json
index d0a37270..73c707a8 100644
--- a/examples/FeatureFlagDemo/appsettings.json
+++ b/examples/FeatureFlagDemo/appsettings.json
@@ -5,7 +5,7 @@
}
},
"AllowedHosts": "*",
-
+
// Define feature flags in config file
"FeatureManagement": {
@@ -36,7 +36,7 @@
}
]
},
- "CustomViewData": {
+ "CustomViewData": {
"EnabledFor": [
{
"Name": "Browser",
diff --git a/src/Microsoft.FeatureManagement/Allocation/Allocation.cs b/src/Microsoft.FeatureManagement/Allocation/Allocation.cs
new file mode 100644
index 00000000..b10ce33d
--- /dev/null
+++ b/src/Microsoft.FeatureManagement/Allocation/Allocation.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using System.Collections.Generic;
+
+namespace Microsoft.FeatureManagement
+{
+ ///
+ /// The definition of how variants are allocated for a feature.
+ ///
+ public class Allocation
+ {
+ ///
+ /// The default variant used if the feature is enabled and no variant is assigned.
+ ///
+ public string DefaultWhenEnabled { get; set; }
+
+ ///
+ /// The default variant used if the feature is disabled.
+ ///
+ public string DefaultWhenDisabled { get; set; }
+
+ ///
+ /// Describes a mapping of user ids to variants.
+ ///
+ public IEnumerable User { get; set; }
+
+ ///
+ /// Describes a mapping of group names to variants.
+ ///
+ public IEnumerable Group { get; set; }
+
+ ///
+ /// Allocates percentiles of user base to variants.
+ ///
+ public IEnumerable Percentile { get; set; }
+
+ ///
+ /// Maps users to the same percentile across multiple feature flags.
+ ///
+ public string Seed { get; set; }
+ }
+}
diff --git a/src/Microsoft.FeatureManagement/Allocation/GroupAllocation.cs b/src/Microsoft.FeatureManagement/Allocation/GroupAllocation.cs
new file mode 100644
index 00000000..6787f8c8
--- /dev/null
+++ b/src/Microsoft.FeatureManagement/Allocation/GroupAllocation.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+
+using System.Collections.Generic;
+
+namespace Microsoft.FeatureManagement
+{
+ ///
+ /// The definition of a group allocation.
+ ///
+ public class GroupAllocation
+ {
+ ///
+ /// The name of the variant.
+ ///
+ public string Variant { get; set; }
+
+ ///
+ /// A list of groups that can be assigned this variant.
+ ///
+ public IEnumerable Groups { get; set; }
+ }
+}
diff --git a/src/Microsoft.FeatureManagement/Allocation/PercentileAllocation.cs b/src/Microsoft.FeatureManagement/Allocation/PercentileAllocation.cs
new file mode 100644
index 00000000..341d7d5d
--- /dev/null
+++ b/src/Microsoft.FeatureManagement/Allocation/PercentileAllocation.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+
+namespace Microsoft.FeatureManagement
+{
+ ///
+ /// The definition of a percentile allocation.
+ ///
+ public class PercentileAllocation
+ {
+ ///
+ /// The name of the variant.
+ ///
+ public string Variant { get; set; }
+
+ ///
+ /// The inclusive lower bound of the percentage to which the variant will be assigned.
+ ///
+ public double From { get; set; }
+
+ ///
+ /// The exclusive upper bound of the percentage to which the variant will be assigned.
+ ///
+ public double To { get; set; }
+ }
+}
diff --git a/src/Microsoft.FeatureManagement/Allocation/UserAllocation.cs b/src/Microsoft.FeatureManagement/Allocation/UserAllocation.cs
new file mode 100644
index 00000000..9443de7a
--- /dev/null
+++ b/src/Microsoft.FeatureManagement/Allocation/UserAllocation.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+
+using System.Collections.Generic;
+
+namespace Microsoft.FeatureManagement
+{
+ ///
+ /// The definition of a user allocation.
+ ///
+ public class UserAllocation
+ {
+ ///
+ /// The name of the variant.
+ ///
+ public string Variant { get; set; }
+
+ ///
+ /// A list of users that will be assigned this variant.
+ ///
+ public IEnumerable Users { get; set; }
+ }
+}
diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
index 4774fd5e..8e84aecb 100644
--- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
+++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -21,13 +22,13 @@ sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionProvider
// 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 readonly IConfiguration _configuration;
private readonly ConcurrentDictionary _definitions;
private IDisposable _changeSubscription;
private int _stale = 0;
+ const string ParseValueErrorString = "Invalid setting '{0}' with value '{1}' for feature '{2}'.";
+
public ConfigurationFeatureDefinitionProvider(IConfiguration configuration)
{
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
@@ -136,13 +137,19 @@ We support
RequirementType requirementType = RequirementType.Any;
+ FeatureStatus featureStatus = FeatureStatus.Conditional;
+
+ Allocation allocation = null;
+
+ List variants = null;
+
var enabledFor = new List();
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)
@@ -160,57 +167,173 @@ 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))
+ string rawFeatureStatus = configurationSection[ConfigurationFields.FeatureStatus];
+
+ if (!string.IsNullOrEmpty(rawRequirementType))
{
- throw new FeatureManagementException(
- FeatureManagementError.InvalidConfigurationSetting,
- $"Invalid requirement type '{rawRequirementType}' for feature '{configurationSection.Key}'.");
+ requirementType = ParseEnum(configurationSection.Key, rawRequirementType, ConfigurationFields.RequirementType);
}
- IEnumerable filterSections = configurationSection.GetSection(FeatureFiltersSectionName).GetChildren();
+ if (!string.IsNullOrEmpty(rawFeatureStatus))
+ {
+ featureStatus = ParseEnum(configurationSection.Key, rawFeatureStatus, ConfigurationFields.FeatureStatus);
+ }
+
+ IEnumerable 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))
});
}
}
+
+ IConfigurationSection allocationSection = configurationSection.GetSection(ConfigurationFields.AllocationSectionName);
+
+ if (allocationSection.Exists())
+ {
+ allocation = new Allocation()
+ {
+ DefaultWhenDisabled = allocationSection[ConfigurationFields.AllocationDefaultWhenDisabled],
+ DefaultWhenEnabled = allocationSection[ConfigurationFields.AllocationDefaultWhenEnabled],
+ User = allocationSection.GetSection(ConfigurationFields.UserAllocationSectionName).GetChildren().Select(userAllocation =>
+ {
+ return new UserAllocation()
+ {
+ Variant = userAllocation[ConfigurationFields.AllocationVariantKeyword],
+ Users = userAllocation.GetSection(ConfigurationFields.UserAllocationUsers).Get>()
+ };
+ }),
+ Group = allocationSection.GetSection(ConfigurationFields.GroupAllocationSectionName).GetChildren().Select(groupAllocation =>
+ {
+ return new GroupAllocation()
+ {
+ Variant = groupAllocation[ConfigurationFields.AllocationVariantKeyword],
+ Groups = groupAllocation.GetSection(ConfigurationFields.GroupAllocationGroups).Get>()
+ };
+ }),
+ Percentile = allocationSection.GetSection(ConfigurationFields.PercentileAllocationSectionName).GetChildren().Select(percentileAllocation =>
+ {
+ double from = 0;
+
+ double to = 0;
+
+ string rawFrom = percentileAllocation[ConfigurationFields.PercentileAllocationFrom];
+
+ string rawTo = percentileAllocation[ConfigurationFields.PercentileAllocationTo];
+
+ if (!string.IsNullOrEmpty(rawFrom))
+ {
+ from = ParseDouble(configurationSection.Key, rawFrom, ConfigurationFields.PercentileAllocationFrom);
+ }
+
+ if (!string.IsNullOrEmpty(rawTo))
+ {
+ to = ParseDouble(configurationSection.Key, rawTo, ConfigurationFields.PercentileAllocationTo);
+ }
+
+ return new PercentileAllocation()
+ {
+ Variant = percentileAllocation[ConfigurationFields.AllocationVariantKeyword],
+ From = from,
+ To = to
+ };
+ }),
+ Seed = allocationSection[ConfigurationFields.AllocationSeed]
+ };
+ }
+
+ 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]))
+ {
+ StatusOverride statusOverride = StatusOverride.None;
+
+ string rawStatusOverride = section[ConfigurationFields.VariantDefinitionStatusOverride];
+
+ if (!string.IsNullOrEmpty(rawStatusOverride))
+ {
+ statusOverride = ParseEnum(configurationSection.Key, rawStatusOverride, ConfigurationFields.VariantDefinitionStatusOverride);
+ }
+
+ VariantDefinition variant = new VariantDefinition()
+ {
+ Name = section[ConfigurationFields.NameKeyword],
+ ConfigurationValue = section.GetSection(ConfigurationFields.VariantDefinitionConfigurationValue),
+ ConfigurationReference = section[ConfigurationFields.VariantDefinitionConfigurationReference],
+ StatusOverride = statusOverride
+ };
+
+ variants.Add(variant);
+ }
+ }
}
return new FeatureDefinition()
{
Name = configurationSection.Key,
EnabledFor = enabledFor,
- RequirementType = requirementType
+ RequirementType = requirementType,
+ Status = featureStatus,
+ Allocation = allocation,
+ Variants = variants
};
}
private IEnumerable GetFeatureDefinitionSections()
{
- const string FeatureManagementSectionName = "FeatureManagement";
-
- if (_configuration.GetChildren().Any(s => s.Key.Equals(FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)))
+ if (_configuration.GetChildren().Any(s => s.Key.Equals(ConfigurationFields.FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)))
{
//
// Look for feature definitions under the "FeatureManagement" section
- return _configuration.GetSection(FeatureManagementSectionName).GetChildren();
+ return _configuration.GetSection(ConfigurationFields.FeatureManagementSectionName).GetChildren();
}
else
{
return _configuration.GetChildren();
}
}
+
+ private T ParseEnum(string feature, string rawValue, string fieldKeyword)
+ where T: struct, Enum
+ {
+ Debug.Assert(!string.IsNullOrEmpty(rawValue));
+
+ if (!Enum.TryParse(rawValue, ignoreCase: true, out T value))
+ {
+ throw new FeatureManagementException(
+ FeatureManagementError.InvalidConfigurationSetting,
+ string.Format(ParseValueErrorString, fieldKeyword, rawValue, feature));
+ }
+
+ return value;
+ }
+
+ private double ParseDouble(string feature, string rawValue, string fieldKeyword)
+ {
+ Debug.Assert(!string.IsNullOrEmpty(rawValue));
+
+ if (!double.TryParse(rawValue, out double value))
+ {
+ throw new FeatureManagementException(
+ FeatureManagementError.InvalidConfigurationSetting,
+ string.Format(ParseValueErrorString, fieldKeyword, rawValue, feature));
+ }
+
+ return value;
+ }
}
}
diff --git a/src/Microsoft.FeatureManagement/ConfigurationFields.cs b/src/Microsoft.FeatureManagement/ConfigurationFields.cs
new file mode 100644
index 00000000..3642da21
--- /dev/null
+++ b/src/Microsoft.FeatureManagement/ConfigurationFields.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+
+namespace Microsoft.FeatureManagement
+{
+ internal static class ConfigurationFields
+ {
+ // Enum keywords
+ public const string RequirementType = "RequirementType";
+ public const string FeatureStatus = "Status";
+
+ // Feature filters keywords
+ public const string FeatureFiltersSectionName = "EnabledFor";
+ public const string FeatureFilterConfigurationParameters = "Parameters";
+
+ // Allocation keywords
+ 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 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";
+
+ // Variants keywords
+ public const string VariantsSectionName = "Variants";
+ public const string VariantDefinitionConfigurationValue = "ConfigurationValue";
+ public const string VariantDefinitionConfigurationReference = "ConfigurationReference";
+ public const string VariantDefinitionStatusOverride = "StatusOverride";
+
+ // Other keywords
+ public const string NameKeyword = "Name";
+ public const string FeatureManagementSectionName = "FeatureManagement";
+ }
+}
diff --git a/src/Microsoft.FeatureManagement/FeatureDefinition.cs b/src/Microsoft.FeatureManagement/FeatureDefinition.cs
index 6736314f..53819fc6 100644
--- a/src/Microsoft.FeatureManagement/FeatureDefinition.cs
+++ b/src/Microsoft.FeatureManagement/FeatureDefinition.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license.
//
using System.Collections.Generic;
+using System.Linq;
namespace Microsoft.FeatureManagement
{
@@ -21,9 +22,25 @@ public class FeatureDefinition
public IEnumerable EnabledFor { get; set; } = new List();
///
- /// Determines whether any or all registered feature filters must be enabled for the feature to be considered enabled
+ /// Determines whether any or all registered feature filters must be enabled for the feature to be considered enabled.
/// The default value is .
///
public RequirementType RequirementType { get; set; } = RequirementType.Any;
+
+ ///
+ /// When set to , this feature will always be considered disabled regardless of the rest of the feature definition.
+ /// The default value is .
+ ///
+ public FeatureStatus Status { get; set; } = FeatureStatus.Conditional;
+
+ ///
+ /// Describes how variants should be allocated.
+ ///
+ public Allocation Allocation { get; set; }
+
+ ///
+ /// A list of variant definitions that specify a configuration to return when assigned.
+ ///
+ public IEnumerable Variants { get; set; } = Enumerable.Empty();
}
}
diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs
index 17b94c47..76a30859 100644
--- a/src/Microsoft.FeatureManagement/FeatureManager.cs
+++ b/src/Microsoft.FeatureManagement/FeatureManager.cs
@@ -4,11 +4,16 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
+using Microsoft.FeatureManagement.FeatureFilters;
+using Microsoft.FeatureManagement.Targeting;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.FeatureManagement
@@ -16,7 +21,7 @@ namespace Microsoft.FeatureManagement
///
/// Used to evaluate whether a feature is enabled or disabled.
///
- class FeatureManager : IFeatureManager, IDisposable
+ class FeatureManager : IFeatureManager, IDisposable, IVariantFeatureManager
{
private readonly TimeSpan ParametersCacheSlidingExpiration = TimeSpan.FromMinutes(5);
private readonly TimeSpan ParametersCacheAbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1);
@@ -28,8 +33,9 @@ class FeatureManager : IFeatureManager, IDisposable
private readonly ConcurrentDictionary _filterMetadataCache;
private readonly ConcurrentDictionary _contextualFeatureFilterCache;
private readonly FeatureManagementOptions _options;
+ private readonly TargetingEvaluationOptions _assignerOptions;
private readonly IMemoryCache _parametersCache;
-
+
private class ConfigurationCacheItem
{
public IConfiguration Parameters { get; set; }
@@ -42,26 +48,101 @@ public FeatureManager(
IEnumerable featureFilters,
IEnumerable sessionManagers,
ILoggerFactory loggerFactory,
- IOptions options)
+ IOptions options,
+ IOptions assignerOptions)
{
_featureDefinitionProvider = featureDefinitionProvider;
_featureFilters = featureFilters ?? throw new ArgumentNullException(nameof(featureFilters));
_sessionManagers = sessionManagers ?? throw new ArgumentNullException(nameof(sessionManagers));
_logger = loggerFactory.CreateLogger();
+ _assignerOptions = assignerOptions?.Value ?? throw new ArgumentNullException(nameof(assignerOptions));
_filterMetadataCache = new ConcurrentDictionary();
_contextualFeatureFilterCache = new ConcurrentDictionary();
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_parametersCache = new MemoryCache(new MemoryCacheOptions());
}
+ public IConfiguration Configuration { get; init; }
+
+ public ITargetingContextAccessor TargetingContextAccessor { get; init; }
+
public Task IsEnabledAsync(string feature)
{
- return IsEnabledAsync