Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add variants for feature flags #250

Merged
merged 85 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
d28bf88
in progress, add new classes for variants and define methods in featu…
amerjusupovic Jul 3, 2023
c1451d3
Revert "Revert "Add cancellation token parameter to async feature man…
amerjusupovic Jul 3, 2023
d087e7b
Revert "Revert "Added default value for cancellation token in interfa…
amerjusupovic Jul 3, 2023
87aa5a8
fix any conflicts left from adding cancellationToken back
amerjusupovic Jul 3, 2023
55e2264
add in progress changes to allocation and featuredefinitionprovider
amerjusupovic Jul 3, 2023
1918bf9
add examples for testing
amerjusupovic Jul 3, 2023
3e3ab2e
fix adding new featuredefinition properties from featuremanagement de…
amerjusupovic Jul 5, 2023
703f89a
progress adding getvariant logic classes
amerjusupovic Jul 5, 2023
e225d22
continued
amerjusupovic Jul 5, 2023
c233ec6
remove repeated code in contextual targeting
amerjusupovic Jul 5, 2023
3a8bac1
fix version of contextual filter
amerjusupovic Jul 5, 2023
fa2133c
more progress on getting the contextual allocator to work
amerjusupovic Jul 6, 2023
86ff346
about to test getvariant
amerjusupovic Jul 6, 2023
c445e46
add example to test
amerjusupovic Jul 10, 2023
cd09ec2
Merge branch 'ajusupovic/add-variants' of https://github.com/microsof…
amerjusupovic Jul 10, 2023
0a4dcfb
add snapshot changes
amerjusupovic Jul 10, 2023
82055ad
Merge branch 'ajusupovic/add-variants' of https://github.com/microsof…
amerjusupovic Jul 10, 2023
34375e6
variant can be detected and retrieved from getvariantasync
amerjusupovic Jul 10, 2023
1f6318b
progress on allocation logic, add comments where consideration needed
amerjusupovic Jul 10, 2023
1f76adc
add use of optionsresolver for reference, todo work on isenabledasync…
amerjusupovic Jul 24, 2023
ee98bbe
All working except couple TODOs, need to add unit tests
amerjusupovic Jul 26, 2023
42a7cbc
remove some comments, add null check where needed
amerjusupovic Jul 26, 2023
3df6f32
update todo comments
amerjusupovic Jul 26, 2023
cbc322e
fix line eols
amerjusupovic Jul 26, 2023
7e801d9
add unit test, in progress
amerjusupovic Jul 26, 2023
ee3fdb0
TODOs in progress, need to restructure featurevariantassigner design
amerjusupovic Jul 27, 2023
76ad51f
fix seed logic
amerjusupovic Jul 28, 2023
b7a97a7
update comments, status logic
amerjusupovic Jul 31, 2023
370228e
remove unnecessary files for custom assigners, fix featuremanager met…
amerjusupovic Jul 31, 2023
a259e23
fix naming from allocator to assigner for classes and files
amerjusupovic Jul 31, 2023
2850421
cleanup extra methods, todo config section logic
amerjusupovic Jul 31, 2023
96b636d
in progress adding configurationsection returned when using configura…
amerjusupovic Jul 31, 2023
43e1b13
continuation of last commit
amerjusupovic Jul 31, 2023
5f6328e
working return for configvalue
amerjusupovic Aug 1, 2023
fbc20fe
move logic to featuremanager for assigning
amerjusupovic Aug 1, 2023
46a263e
remove unused assigner classes
amerjusupovic Aug 1, 2023
baac628
add new configurationsection to handle return for variant
amerjusupovic Aug 1, 2023
6672af6
null error, in progress new configurationsection class
amerjusupovic Aug 1, 2023
4d6064b
fix old bug
amerjusupovic Aug 1, 2023
6012ec3
progress on unit tests
amerjusupovic Aug 2, 2023
d2543ea
more null check changes, test fixes
amerjusupovic Aug 3, 2023
8d59200
reset examples changes
amerjusupovic Aug 3, 2023
77fc24b
Revert "Revert "Revert "Added default value for cancellation token in…
amerjusupovic Aug 3, 2023
a286d43
Revert "Revert "Revert "Add cancellation token parameter to async fea…
amerjusupovic Aug 3, 2023
9bbd115
add comments for new classes
amerjusupovic Aug 3, 2023
3ea099b
fix comments for public classes again
amerjusupovic Aug 4, 2023
8306eb4
update comments, default values
amerjusupovic Aug 4, 2023
8765c93
fix variantconfigurationsection, comments in definitionprovider
amerjusupovic Aug 4, 2023
a3466c7
fix using statements, null checks
amerjusupovic Aug 4, 2023
eb80694
fix unit test failures with servicecollectionextensions
amerjusupovic Aug 8, 2023
0b8f9e9
add revisions: fix namepaces, add exceptions tests, combine percentag…
amerjusupovic Aug 10, 2023
1da4d2a
change context accessor logic
amerjusupovic Aug 10, 2023
dff6044
fix comments for default variants
amerjusupovic Aug 10, 2023
adbf40f
PR revisions
amerjusupovic Aug 11, 2023
875a422
change class names, PR fixes
amerjusupovic Aug 14, 2023
05b31b1
Merge branch 'main' of https://github.com/microsoft/FeatureManagement…
amerjusupovic Aug 14, 2023
c50c96e
fix edge case percentage targeting
amerjusupovic Aug 14, 2023
95e6459
rename allocation classes, remove exceptions and add warning logs, pr…
amerjusupovic Aug 15, 2023
744b966
refactor isenabled to remove boolean param
amerjusupovic Aug 16, 2023
02f6e55
change configurationvalue to IConfigurationSection instead of string
amerjusupovic Aug 16, 2023
efbab17
fix enabledwithvariants logic
amerjusupovic Aug 16, 2023
e4cddae
PR revisions, fix logic in new methods from last commit
amerjusupovic Aug 17, 2023
e8a640b
set session managers last in flow
amerjusupovic Aug 21, 2023
dc49a2f
make false explicit for status disabled or missing definition
amerjusupovic Aug 21, 2023
d184882
fix constructor default params, move session managers logic, pr revis…
amerjusupovic Aug 22, 2023
49aa2fb
fix comment
amerjusupovic Aug 22, 2023
a0a787a
fix resolvedefaultvariant, isexternalinit error
amerjusupovic Aug 22, 2023
6ef1fce
add back 3.1
amerjusupovic Aug 22, 2023
2067d27
Apply suggestions from code review
amerjusupovic Aug 23, 2023
3fa5f53
isexternalinit comments, remove resolvedefault helper
amerjusupovic Aug 23, 2023
c745f59
remove binding, fix featuredefinitionprovider issues
amerjusupovic Aug 28, 2023
de67697
Merge branch 'ajusupovic/add-variants' of https://github.com/microsof…
amerjusupovic Aug 28, 2023
29bce04
change to Debug.Assert from Assert
amerjusupovic Aug 28, 2023
9c7765d
update method name
amerjusupovic Aug 28, 2023
1fb1c67
remove parseenum, add ConfigurationFields class
amerjusupovic Aug 31, 2023
6dfb3ec
test failing, fixed PR revisions
amerjusupovic Sep 7, 2023
827c0ae
fix invalid scenarios test
amerjusupovic Sep 7, 2023
656ec67
simplify context in test
amerjusupovic Sep 7, 2023
bed8093
remove unused using
amerjusupovic Sep 7, 2023
03e8e47
remove unused param
amerjusupovic Sep 7, 2023
e1cb0d2
Clarify how From and To bounds work in PercentileAllocation
amerjusupovic Sep 8, 2023
23ff1ef
fix error messages
amerjusupovic Sep 8, 2023
2340b8c
Merge branch 'ajusupovic/add-variants' of https://github.com/microsof…
amerjusupovic Sep 8, 2023
3827494
add feature name as default seed with allocation prefix
amerjusupovic Sep 11, 2023
755687e
Update src/Microsoft.FeatureManagement/FeatureManager.cs
amerjusupovic Sep 12, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/FeatureFlagDemo/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
}
},
"AllowedHosts": "*",

// Define feature flags in config file
"FeatureManagement": {

Expand Down Expand Up @@ -36,7 +36,7 @@
}
]
},
"CustomViewData": {
"CustomViewData": {
"EnabledFor": [
{
"Name": "Browser",
Expand Down
43 changes: 43 additions & 0 deletions src/Microsoft.FeatureManagement/Allocation/Allocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using System.Collections.Generic;

namespace Microsoft.FeatureManagement
{
/// <summary>
/// The definition of how variants are allocated for a feature.
/// </summary>
public class Allocation
{
/// <summary>
/// The default variant used if the feature is enabled and no variant is assigned.
/// </summary>
public string DefaultWhenEnabled { get; set; }

/// <summary>
/// The default variant used if the feature is disabled.
/// </summary>
public string DefaultWhenDisabled { get; set; }

/// <summary>
/// Describes a mapping of user ids to variants.
/// </summary>
public IEnumerable<UserAllocation> User { get; set; }

/// <summary>
/// Describes a mapping of group names to variants.
/// </summary>
public IEnumerable<GroupAllocation> Group { get; set; }

/// <summary>
/// Allocates percentiles of user base to variants.
/// </summary>
public IEnumerable<PercentileAllocation> Percentile { get; set; }

/// <summary>
/// Maps users to the same percentile across multiple feature flags.
/// </summary>
public string Seed { get; set; }
jimmyca15 marked this conversation as resolved.
Show resolved Hide resolved
}
}
24 changes: 24 additions & 0 deletions src/Microsoft.FeatureManagement/Allocation/GroupAllocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//

using System.Collections.Generic;

namespace Microsoft.FeatureManagement
{
/// <summary>
/// The definition of a group allocation.
/// </summary>
public class GroupAllocation
{
/// <summary>
/// The name of the variant.
/// </summary>
public string Variant { get; set; }

/// <summary>
/// A list of groups that can be assigned this variant.
/// </summary>
public IEnumerable<string> Groups { get; set; }
}
}
27 changes: 27 additions & 0 deletions src/Microsoft.FeatureManagement/Allocation/PercentileAllocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//

namespace Microsoft.FeatureManagement
{
/// <summary>
/// The definition of a percentile allocation.
/// </summary>
public class PercentileAllocation
{
/// <summary>
/// The name of the variant.
/// </summary>
public string Variant { get; set; }

/// <summary>
/// The inclusive lower bound of the percentage to which the variant will be assigned.
/// </summary>
public double From { get; set; }

/// <summary>
/// The exclusive upper bound of the percentage to which the variant will be assigned.
/// </summary>
public double To { get; set; }
}
}
24 changes: 24 additions & 0 deletions src/Microsoft.FeatureManagement/Allocation/UserAllocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//

using System.Collections.Generic;

namespace Microsoft.FeatureManagement
{
/// <summary>
/// The definition of a user allocation.
/// </summary>
public class UserAllocation
{
/// <summary>
/// The name of the variant.
/// </summary>
public string Variant { get; set; }

/// <summary>
/// A list of users that will be assigned this variant.
/// </summary>
public IEnumerable<string> Users { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<string, FeatureDefinition> _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));
Expand Down Expand Up @@ -136,13 +137,19 @@ We support

RequirementType requirementType = RequirementType.Any;

FeatureStatus featureStatus = FeatureStatus.Conditional;

Allocation allocation = null;

List<VariantDefinition> variants = null;

var enabledFor = new List<FeatureFilterConfiguration>();

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)
Expand All @@ -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<RequirementType>(configurationSection.Key, rawRequirementType, ConfigurationFields.RequirementType);
}

IEnumerable<IConfigurationSection> filterSections = configurationSection.GetSection(FeatureFiltersSectionName).GetChildren();
if (!string.IsNullOrEmpty(rawFeatureStatus))
{
featureStatus = ParseEnum<FeatureStatus>(configurationSection.Key, rawFeatureStatus, ConfigurationFields.FeatureStatus);
}

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))
});
}
}

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<IEnumerable<string>>()
};
}),
Group = allocationSection.GetSection(ConfigurationFields.GroupAllocationSectionName).GetChildren().Select(groupAllocation =>
{
return new GroupAllocation()
{
Variant = groupAllocation[ConfigurationFields.AllocationVariantKeyword],
Groups = groupAllocation.GetSection(ConfigurationFields.GroupAllocationGroups).Get<IEnumerable<string>>()
};
}),
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<IConfigurationSection> variantsSections = configurationSection.GetSection(ConfigurationFields.VariantsSectionName).GetChildren();
variants = new List<VariantDefinition>();

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<StatusOverride>(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<IConfigurationSection> 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<T>(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,
Copy link
Member

Choose a reason for hiding this comment

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

The knowledge that configurationSection.Key is feature name is a bit of odd encapsulation. I'd suggest updating the method signature.

private T ParseEnum<T>(
    string feature,
    string value,
    T enumValue)

string.Format(ParseValueErrorString, fieldKeyword, rawValue, feature));
}

return value;
}
}
}
Loading