Skip to content

Commit b1b1562

Browse files
amerjusupovicjimmyca15rossgrambo
authored
Add variants for feature flags (#250)
* in progress, add new classes for variants and define methods in featuremanager * Revert "Revert "Add cancellation token parameter to async feature management interfaces. (#131)" (#139)" This reverts commit e531863. * Revert "Revert "Added default value for cancellation token in interfaces to keep existing usage possible. (#133)" (#138)" This reverts commit 8f9a7e4. * fix any conflicts left from adding cancellationToken back * add in progress changes to allocation and featuredefinitionprovider * add examples for testing * fix adding new featuredefinition properties from featuremanagement definition * progress adding getvariant logic classes * continued * remove repeated code in contextual targeting * fix version of contextual filter * more progress on getting the contextual allocator to work * about to test getvariant * add example to test * add snapshot changes * variant can be detected and retrieved from getvariantasync * progress on allocation logic, add comments where consideration needed * add use of optionsresolver for reference, todo work on isenabledasync between customer use and variant use * All working except couple TODOs, need to add unit tests * remove some comments, add null check where needed * update todo comments * fix line eols * add unit test, in progress * TODOs in progress, need to restructure featurevariantassigner design * fix seed logic * update comments, status logic * remove unnecessary files for custom assigners, fix featuremanager methods and interfaces to match * fix naming from allocator to assigner for classes and files * cleanup extra methods, todo config section logic * in progress adding configurationsection returned when using configurationvalue * continuation of last commit * working return for configvalue * move logic to featuremanager for assigning * remove unused assigner classes * add new configurationsection to handle return for variant * null error, in progress new configurationsection class * fix old bug * progress on unit tests * more null check changes, test fixes * reset examples changes * Revert "Revert "Revert "Added default value for cancellation token in interfaces to keep existing usage possible. (#133)" (#138)"" This reverts commit d087e7b. * Revert "Revert "Revert "Add cancellation token parameter to async feature management interfaces. (#131)" (#139)"" This reverts commit c1451d3. * add comments for new classes * fix comments for public classes again * update comments, default values * fix variantconfigurationsection, comments in definitionprovider * fix using statements, null checks * fix unit test failures with servicecollectionextensions * add revisions: fix namepaces, add exceptions tests, combine percentage logic, fix comments, add cancellationtoken to new interface * change context accessor logic * fix comments for default variants * PR revisions * change class names, PR fixes * fix edge case percentage targeting * rename allocation classes, remove exceptions and add warning logs, prioritize inline value for variant config, more revisions * refactor isenabled to remove boolean param * change configurationvalue to IConfigurationSection instead of string * fix enabledwithvariants logic * PR revisions, fix logic in new methods from last commit * set session managers last in flow * make false explicit for status disabled or missing definition * fix constructor default params, move session managers logic, pr revisions * fix comment * fix resolvedefaultvariant, isexternalinit error * add back 3.1 * Apply suggestions from code review Co-authored-by: Jimmy Campbell <jimmyca@microsoft.com> * isexternalinit comments, remove resolvedefault helper * remove binding, fix featuredefinitionprovider issues * change to Debug.Assert from Assert * update method name * remove parseenum, add ConfigurationFields class * test failing, fixed PR revisions * fix invalid scenarios test * simplify context in test * remove unused using * remove unused param * Clarify how From and To bounds work in PercentileAllocation Co-authored-by: Ross Grambo <rossgrambo@microsoft.com> * fix error messages * add feature name as default seed with allocation prefix * Update src/Microsoft.FeatureManagement/FeatureManager.cs Co-authored-by: Jimmy Campbell <jimmyca@microsoft.com> --------- Co-authored-by: Jimmy Campbell <jimmyca@microsoft.com> Co-authored-by: Ross Grambo <rossgrambo@microsoft.com>
1 parent ca73348 commit b1b1562

25 files changed

+1660
-207
lines changed

examples/FeatureFlagDemo/appsettings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
}
66
},
77
"AllowedHosts": "*",
8-
8+
99
// Define feature flags in config file
1010
"FeatureManagement": {
1111

@@ -36,7 +36,7 @@
3636
}
3737
]
3838
},
39-
"CustomViewData": {
39+
"CustomViewData": {
4040
"EnabledFor": [
4141
{
4242
"Name": "Browser",
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
using System.Collections.Generic;
5+
6+
namespace Microsoft.FeatureManagement
7+
{
8+
/// <summary>
9+
/// The definition of how variants are allocated for a feature.
10+
/// </summary>
11+
public class Allocation
12+
{
13+
/// <summary>
14+
/// The default variant used if the feature is enabled and no variant is assigned.
15+
/// </summary>
16+
public string DefaultWhenEnabled { get; set; }
17+
18+
/// <summary>
19+
/// The default variant used if the feature is disabled.
20+
/// </summary>
21+
public string DefaultWhenDisabled { get; set; }
22+
23+
/// <summary>
24+
/// Describes a mapping of user ids to variants.
25+
/// </summary>
26+
public IEnumerable<UserAllocation> User { get; set; }
27+
28+
/// <summary>
29+
/// Describes a mapping of group names to variants.
30+
/// </summary>
31+
public IEnumerable<GroupAllocation> Group { get; set; }
32+
33+
/// <summary>
34+
/// Allocates percentiles of user base to variants.
35+
/// </summary>
36+
public IEnumerable<PercentileAllocation> Percentile { get; set; }
37+
38+
/// <summary>
39+
/// Maps users to the same percentile across multiple feature flags.
40+
/// </summary>
41+
public string Seed { get; set; }
42+
}
43+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
5+
using System.Collections.Generic;
6+
7+
namespace Microsoft.FeatureManagement
8+
{
9+
/// <summary>
10+
/// The definition of a group allocation.
11+
/// </summary>
12+
public class GroupAllocation
13+
{
14+
/// <summary>
15+
/// The name of the variant.
16+
/// </summary>
17+
public string Variant { get; set; }
18+
19+
/// <summary>
20+
/// A list of groups that can be assigned this variant.
21+
/// </summary>
22+
public IEnumerable<string> Groups { get; set; }
23+
}
24+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
5+
namespace Microsoft.FeatureManagement
6+
{
7+
/// <summary>
8+
/// The definition of a percentile allocation.
9+
/// </summary>
10+
public class PercentileAllocation
11+
{
12+
/// <summary>
13+
/// The name of the variant.
14+
/// </summary>
15+
public string Variant { get; set; }
16+
17+
/// <summary>
18+
/// The inclusive lower bound of the percentage to which the variant will be assigned.
19+
/// </summary>
20+
public double From { get; set; }
21+
22+
/// <summary>
23+
/// The exclusive upper bound of the percentage to which the variant will be assigned.
24+
/// </summary>
25+
public double To { get; set; }
26+
}
27+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
5+
using System.Collections.Generic;
6+
7+
namespace Microsoft.FeatureManagement
8+
{
9+
/// <summary>
10+
/// The definition of a user allocation.
11+
/// </summary>
12+
public class UserAllocation
13+
{
14+
/// <summary>
15+
/// The name of the variant.
16+
/// </summary>
17+
public string Variant { get; set; }
18+
19+
/// <summary>
20+
/// A list of users that will be assigned this variant.
21+
/// </summary>
22+
public IEnumerable<string> Users { get; set; }
23+
}
24+
}

src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs

Lines changed: 142 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System;
77
using System.Collections.Concurrent;
88
using System.Collections.Generic;
9+
using System.Diagnostics;
910
using System.Linq;
1011
using System.Threading;
1112
using System.Threading.Tasks;
@@ -21,13 +22,13 @@ sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionProvider
2122
// IFeatureDefinitionProviderCacheable interface is only used to mark this provider as cacheable. This allows our test suite's
2223
// provider to be marked for caching as well.
2324

24-
private const string FeatureFiltersSectionName = "EnabledFor";
25-
private const string RequirementTypeKeyword = "RequirementType";
2625
private readonly IConfiguration _configuration;
2726
private readonly ConcurrentDictionary<string, FeatureDefinition> _definitions;
2827
private IDisposable _changeSubscription;
2928
private int _stale = 0;
3029

30+
const string ParseValueErrorString = "Invalid setting '{0}' with value '{1}' for feature '{2}'.";
31+
3132
public ConfigurationFeatureDefinitionProvider(IConfiguration configuration)
3233
{
3334
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
@@ -136,13 +137,19 @@ We support
136137

137138
RequirementType requirementType = RequirementType.Any;
138139

140+
FeatureStatus featureStatus = FeatureStatus.Conditional;
141+
142+
Allocation allocation = null;
143+
144+
List<VariantDefinition> variants = null;
145+
139146
var enabledFor = new List<FeatureFilterConfiguration>();
140147

141148
string val = configurationSection.Value; // configuration[$"{featureName}"];
142149

143150
if (string.IsNullOrEmpty(val))
144151
{
145-
val = configurationSection[FeatureFiltersSectionName];
152+
val = configurationSection[ConfigurationFields.FeatureFiltersSectionName];
146153
}
147154

148155
if (!string.IsNullOrEmpty(val) && bool.TryParse(val, out bool result) && result)
@@ -160,57 +167,173 @@ We support
160167
}
161168
else
162169
{
163-
string rawRequirementType = configurationSection[RequirementTypeKeyword];
170+
string rawRequirementType = configurationSection[ConfigurationFields.RequirementType];
164171

165-
//
166-
// If requirement type is specified, parse it and set the requirementType variable
167-
if (!string.IsNullOrEmpty(rawRequirementType) && !Enum.TryParse(rawRequirementType, ignoreCase: true, out requirementType))
172+
string rawFeatureStatus = configurationSection[ConfigurationFields.FeatureStatus];
173+
174+
if (!string.IsNullOrEmpty(rawRequirementType))
168175
{
169-
throw new FeatureManagementException(
170-
FeatureManagementError.InvalidConfigurationSetting,
171-
$"Invalid requirement type '{rawRequirementType}' for feature '{configurationSection.Key}'.");
176+
requirementType = ParseEnum<RequirementType>(configurationSection.Key, rawRequirementType, ConfigurationFields.RequirementType);
172177
}
173178

174-
IEnumerable<IConfigurationSection> filterSections = configurationSection.GetSection(FeatureFiltersSectionName).GetChildren();
179+
if (!string.IsNullOrEmpty(rawFeatureStatus))
180+
{
181+
featureStatus = ParseEnum<FeatureStatus>(configurationSection.Key, rawFeatureStatus, ConfigurationFields.FeatureStatus);
182+
}
183+
184+
IEnumerable<IConfigurationSection> filterSections = configurationSection.GetSection(ConfigurationFields.FeatureFiltersSectionName).GetChildren();
175185

176186
foreach (IConfigurationSection section in filterSections)
177187
{
178188
//
179189
// Arrays in json such as "myKey": [ "some", "values" ]
180190
// Are accessed through the configuration system by using the array index as the property name, e.g. "myKey": { "0": "some", "1": "values" }
181-
if (int.TryParse(section.Key, out int i) && !string.IsNullOrEmpty(section[nameof(FeatureFilterConfiguration.Name)]))
191+
if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[ConfigurationFields.NameKeyword]))
182192
{
183193
enabledFor.Add(new FeatureFilterConfiguration()
184194
{
185-
Name = section[nameof(FeatureFilterConfiguration.Name)],
186-
Parameters = new ConfigurationWrapper(section.GetSection(nameof(FeatureFilterConfiguration.Parameters)))
195+
Name = section[ConfigurationFields.NameKeyword],
196+
Parameters = new ConfigurationWrapper(section.GetSection(ConfigurationFields.FeatureFilterConfigurationParameters))
187197
});
188198
}
189199
}
200+
201+
IConfigurationSection allocationSection = configurationSection.GetSection(ConfigurationFields.AllocationSectionName);
202+
203+
if (allocationSection.Exists())
204+
{
205+
allocation = new Allocation()
206+
{
207+
DefaultWhenDisabled = allocationSection[ConfigurationFields.AllocationDefaultWhenDisabled],
208+
DefaultWhenEnabled = allocationSection[ConfigurationFields.AllocationDefaultWhenEnabled],
209+
User = allocationSection.GetSection(ConfigurationFields.UserAllocationSectionName).GetChildren().Select(userAllocation =>
210+
{
211+
return new UserAllocation()
212+
{
213+
Variant = userAllocation[ConfigurationFields.AllocationVariantKeyword],
214+
Users = userAllocation.GetSection(ConfigurationFields.UserAllocationUsers).Get<IEnumerable<string>>()
215+
};
216+
}),
217+
Group = allocationSection.GetSection(ConfigurationFields.GroupAllocationSectionName).GetChildren().Select(groupAllocation =>
218+
{
219+
return new GroupAllocation()
220+
{
221+
Variant = groupAllocation[ConfigurationFields.AllocationVariantKeyword],
222+
Groups = groupAllocation.GetSection(ConfigurationFields.GroupAllocationGroups).Get<IEnumerable<string>>()
223+
};
224+
}),
225+
Percentile = allocationSection.GetSection(ConfigurationFields.PercentileAllocationSectionName).GetChildren().Select(percentileAllocation =>
226+
{
227+
double from = 0;
228+
229+
double to = 0;
230+
231+
string rawFrom = percentileAllocation[ConfigurationFields.PercentileAllocationFrom];
232+
233+
string rawTo = percentileAllocation[ConfigurationFields.PercentileAllocationTo];
234+
235+
if (!string.IsNullOrEmpty(rawFrom))
236+
{
237+
from = ParseDouble(configurationSection.Key, rawFrom, ConfigurationFields.PercentileAllocationFrom);
238+
}
239+
240+
if (!string.IsNullOrEmpty(rawTo))
241+
{
242+
to = ParseDouble(configurationSection.Key, rawTo, ConfigurationFields.PercentileAllocationTo);
243+
}
244+
245+
return new PercentileAllocation()
246+
{
247+
Variant = percentileAllocation[ConfigurationFields.AllocationVariantKeyword],
248+
From = from,
249+
To = to
250+
};
251+
}),
252+
Seed = allocationSection[ConfigurationFields.AllocationSeed]
253+
};
254+
}
255+
256+
IEnumerable<IConfigurationSection> variantsSections = configurationSection.GetSection(ConfigurationFields.VariantsSectionName).GetChildren();
257+
variants = new List<VariantDefinition>();
258+
259+
foreach (IConfigurationSection section in variantsSections)
260+
{
261+
if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[ConfigurationFields.NameKeyword]))
262+
{
263+
StatusOverride statusOverride = StatusOverride.None;
264+
265+
string rawStatusOverride = section[ConfigurationFields.VariantDefinitionStatusOverride];
266+
267+
if (!string.IsNullOrEmpty(rawStatusOverride))
268+
{
269+
statusOverride = ParseEnum<StatusOverride>(configurationSection.Key, rawStatusOverride, ConfigurationFields.VariantDefinitionStatusOverride);
270+
}
271+
272+
VariantDefinition variant = new VariantDefinition()
273+
{
274+
Name = section[ConfigurationFields.NameKeyword],
275+
ConfigurationValue = section.GetSection(ConfigurationFields.VariantDefinitionConfigurationValue),
276+
ConfigurationReference = section[ConfigurationFields.VariantDefinitionConfigurationReference],
277+
StatusOverride = statusOverride
278+
};
279+
280+
variants.Add(variant);
281+
}
282+
}
190283
}
191284

192285
return new FeatureDefinition()
193286
{
194287
Name = configurationSection.Key,
195288
EnabledFor = enabledFor,
196-
RequirementType = requirementType
289+
RequirementType = requirementType,
290+
Status = featureStatus,
291+
Allocation = allocation,
292+
Variants = variants
197293
};
198294
}
199295

200296
private IEnumerable<IConfigurationSection> GetFeatureDefinitionSections()
201297
{
202-
const string FeatureManagementSectionName = "FeatureManagement";
203-
204-
if (_configuration.GetChildren().Any(s => s.Key.Equals(FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)))
298+
if (_configuration.GetChildren().Any(s => s.Key.Equals(ConfigurationFields.FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)))
205299
{
206300
//
207301
// Look for feature definitions under the "FeatureManagement" section
208-
return _configuration.GetSection(FeatureManagementSectionName).GetChildren();
302+
return _configuration.GetSection(ConfigurationFields.FeatureManagementSectionName).GetChildren();
209303
}
210304
else
211305
{
212306
return _configuration.GetChildren();
213307
}
214308
}
309+
310+
private T ParseEnum<T>(string feature, string rawValue, string fieldKeyword)
311+
where T: struct, Enum
312+
{
313+
Debug.Assert(!string.IsNullOrEmpty(rawValue));
314+
315+
if (!Enum.TryParse(rawValue, ignoreCase: true, out T value))
316+
{
317+
throw new FeatureManagementException(
318+
FeatureManagementError.InvalidConfigurationSetting,
319+
string.Format(ParseValueErrorString, fieldKeyword, rawValue, feature));
320+
}
321+
322+
return value;
323+
}
324+
325+
private double ParseDouble(string feature, string rawValue, string fieldKeyword)
326+
{
327+
Debug.Assert(!string.IsNullOrEmpty(rawValue));
328+
329+
if (!double.TryParse(rawValue, out double value))
330+
{
331+
throw new FeatureManagementException(
332+
FeatureManagementError.InvalidConfigurationSetting,
333+
string.Format(ParseValueErrorString, fieldKeyword, rawValue, feature));
334+
}
335+
336+
return value;
337+
}
215338
}
216339
}

0 commit comments

Comments
 (0)