Skip to content

Commit b5e3a4f

Browse files
Respect .NET and Microsoft feature management schemas in configuration (#470)
* respect all feature management schemas * add testcase * add testcase * correct test case * simplify methods * remove unused package * unify newline usage * remove useless code & rename variables * reuse ParseEnum * rename variable
1 parent c8deb66 commit b5e3a4f

File tree

3 files changed

+123
-234
lines changed

3 files changed

+123
-234
lines changed

src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs

Lines changed: 74 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@ public sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionP
2727
private readonly ConcurrentDictionary<string, FeatureDefinition> _definitions;
2828
private IDisposable _changeSubscription;
2929
private int _stale = 0;
30-
private long _initialized = 0;
31-
private bool _microsoftFeatureManagementSchemaEnabled;
32-
private readonly object _lock = new object();
3330

3431
const string ParseValueErrorString = "Invalid setting '{0}' with value '{1}' for feature '{2}'.";
3532

@@ -84,18 +81,15 @@ public Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName)
8481
throw new ArgumentException($"The value '{ConfigurationPath.KeyDelimiter}' is not allowed in the feature name.", nameof(featureName));
8582
}
8683

87-
EnsureInit();
88-
8984
if (Interlocked.Exchange(ref _stale, 0) != 0)
9085
{
9186
_definitions.Clear();
9287
}
9388

94-
//
95-
// Query by feature name
96-
FeatureDefinition definition = _definitions.GetOrAdd(featureName, (name) => ReadFeatureDefinition(name));
97-
98-
return Task.FromResult(definition);
89+
return Task.FromResult(
90+
_definitions.GetOrAdd(
91+
featureName,
92+
(_) => GetMicrosoftSchemaFeatureDefinition(featureName) ?? GetDotnetSchemaFeatureDefinition(featureName)));
9993
}
10094

10195
/// <summary>
@@ -109,18 +103,16 @@ public Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName)
109103
public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
110104
#pragma warning restore CS1998
111105
{
112-
EnsureInit();
113-
114106
if (Interlocked.Exchange(ref _stale, 0) != 0)
115107
{
116108
_definitions.Clear();
117109
}
118110

119-
//
120-
// Iterate over all features registered in the system at initial invocation time
121-
foreach (IConfigurationSection featureSection in GetFeatureDefinitionSections())
111+
IEnumerable<IConfigurationSection> microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
112+
113+
foreach (IConfigurationSection featureSection in microsoftFeatureDefinitionSections)
122114
{
123-
string featureName = GetFeatureName(featureSection);
115+
string featureName = featureSection[MicrosoftFeatureManagementFields.Id];
124116

125117
if (string.IsNullOrEmpty(featureName))
126118
{
@@ -129,80 +121,99 @@ public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
129121

130122
//
131123
// Underlying IConfigurationSection data is dynamic so latest feature definitions are returned
132-
FeatureDefinition definition = _definitions.GetOrAdd(featureName, (_) => ReadFeatureDefinition(featureSection));
124+
FeatureDefinition definition = _definitions.GetOrAdd(featureName, (_) => ParseMicrosoftSchemaFeatureDefinition(featureSection));
133125

134-
//
135-
// Null cache entry possible if someone accesses non-existent flag directly (IsEnabled)
136126
if (definition != null)
137127
{
138128
yield return definition;
139129
}
140130
}
141-
}
142131

143-
private void EnsureInit()
144-
{
145-
if (_initialized == 0)
146-
{
147-
IConfiguration MicrosoftFeatureManagementConfigurationSection = _configuration
148-
.GetChildren()
149-
.FirstOrDefault(section =>
150-
string.Equals(
151-
section.Key,
152-
MicrosoftFeatureManagementFields.FeatureManagementSectionName,
153-
StringComparison.OrdinalIgnoreCase));
132+
IEnumerable<IConfigurationSection> dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();
154133

155-
bool hasMicrosoftFeatureManagementSchema = MicrosoftFeatureManagementConfigurationSection != null;
134+
foreach (IConfigurationSection featureSection in dotnetFeatureDefinitionSections)
135+
{
136+
string featureName = featureSection.Key;
156137

157-
if (MicrosoftFeatureManagementConfigurationSection == null & RootConfigurationFallbackEnabled)
138+
if (string.IsNullOrEmpty(featureName))
158139
{
159-
IConfiguration featureFlagsSection = _configuration
160-
.GetChildren()
161-
.FirstOrDefault(section =>
162-
string.Equals(
163-
section.Key,
164-
MicrosoftFeatureManagementFields.FeatureFlagsSectionName,
165-
StringComparison.OrdinalIgnoreCase));
166-
167-
hasMicrosoftFeatureManagementSchema = featureFlagsSection != null;
140+
continue;
168141
}
169142

170-
lock (_lock)
171-
{
172-
if (Interlocked.Read(ref _initialized) == 0)
173-
{
174-
_microsoftFeatureManagementSchemaEnabled = hasMicrosoftFeatureManagementSchema;
143+
//
144+
// Underlying IConfigurationSection data is dynamic so latest feature definitions are returned
145+
FeatureDefinition definition = _definitions.GetOrAdd(featureName, (_) => ParseDotnetSchemaFeatureDefinition(featureSection));
175146

176-
Interlocked.Exchange(ref _initialized, 1);
177-
}
147+
if (definition != null)
148+
{
149+
yield return definition;
178150
}
179151
}
180152
}
181153

182-
private FeatureDefinition ReadFeatureDefinition(string featureName)
154+
private FeatureDefinition GetDotnetSchemaFeatureDefinition(string featureName)
183155
{
184-
IConfigurationSection configuration = GetFeatureDefinitionSections()
185-
.FirstOrDefault(section => string.Equals(GetFeatureName(section), featureName, StringComparison.OrdinalIgnoreCase));
156+
IEnumerable<IConfigurationSection> dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();
157+
158+
IConfigurationSection configuration = dotnetFeatureDefinitionSections
159+
.FirstOrDefault(section =>
160+
string.Equals(section.Key, featureName, StringComparison.OrdinalIgnoreCase));
186161

187162
if (configuration == null)
188163
{
189164
return null;
190165
}
191166

192-
return ReadFeatureDefinition(configuration);
167+
return ParseDotnetSchemaFeatureDefinition(configuration);
193168
}
194169

195-
private FeatureDefinition ReadFeatureDefinition(IConfigurationSection configurationSection)
170+
private FeatureDefinition GetMicrosoftSchemaFeatureDefinition(string featureName)
196171
{
197-
if (_microsoftFeatureManagementSchemaEnabled)
172+
IEnumerable<IConfigurationSection> microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
173+
174+
IConfigurationSection configuration = microsoftFeatureDefinitionSections
175+
.FirstOrDefault(section =>
176+
string.Equals(section[MicrosoftFeatureManagementFields.Id], featureName, StringComparison.OrdinalIgnoreCase));
177+
178+
if (configuration == null)
179+
{
180+
return null;
181+
}
182+
183+
return ParseMicrosoftSchemaFeatureDefinition(configuration);
184+
}
185+
186+
private IEnumerable<IConfigurationSection> GetDotnetFeatureDefinitionSections()
187+
{
188+
IConfigurationSection featureManagementConfigurationSection = _configuration.GetSection(DotnetFeatureManagementFields.FeatureManagementSectionName);
189+
190+
if (featureManagementConfigurationSection.Exists())
191+
{
192+
return featureManagementConfigurationSection.GetChildren();
193+
}
194+
195+
//
196+
// Root configuration fallback only applies to .NET schema.
197+
// If Microsoft schema can be found, root configuration fallback will not be effective.
198+
if (RootConfigurationFallbackEnabled &&
199+
!_configuration.GetChildren()
200+
.Any(section =>
201+
string.Equals(section.Key, MicrosoftFeatureManagementFields.FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)))
198202
{
199-
return ParseMicrosoftFeatureDefinition(configurationSection);
203+
return _configuration.GetChildren();
200204
}
201205

202-
return ParseDotnetFeatureDefinition(configurationSection);
206+
return Enumerable.Empty<IConfigurationSection>();
203207
}
204208

205-
private FeatureDefinition ParseDotnetFeatureDefinition(IConfigurationSection configurationSection)
209+
private IEnumerable<IConfigurationSection> GetMicrosoftFeatureDefinitionSections()
210+
{
211+
return _configuration.GetSection(MicrosoftFeatureManagementFields.FeatureManagementSectionName)
212+
.GetSection(MicrosoftFeatureManagementFields.FeatureFlagsSectionName)
213+
.GetChildren();
214+
}
215+
216+
private FeatureDefinition ParseDotnetSchemaFeatureDefinition(IConfigurationSection configurationSection)
206217
{
207218
/*
208219
@@ -229,7 +240,7 @@ We support
229240
230241
*/
231242

232-
string featureName = GetFeatureName(configurationSection);
243+
string featureName = configurationSection.Key;
233244

234245
var enabledFor = new List<FeatureFilterConfiguration>();
235246

@@ -293,7 +304,7 @@ We support
293304
};
294305
}
295306

296-
private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection configurationSection)
307+
private FeatureDefinition ParseMicrosoftSchemaFeatureDefinition(IConfigurationSection configurationSection)
297308
{
298309
/*
299310
@@ -323,7 +334,7 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection
323334
324335
*/
325336

326-
string featureName = GetFeatureName(configurationSection);
337+
string featureName = configurationSection[MicrosoftFeatureManagementFields.Id];
327338

328339
var enabledFor = new List<FeatureFilterConfiguration>();
329340

@@ -354,13 +365,9 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection
354365
{
355366
string rawRequirementType = conditionsSection[MicrosoftFeatureManagementFields.RequirementType];
356367

357-
//
358-
// If requirement type is specified, parse it and set the requirementType variable
359-
if (!string.IsNullOrEmpty(rawRequirementType) && !Enum.TryParse(rawRequirementType, ignoreCase: true, out requirementType))
368+
if (!string.IsNullOrEmpty(rawRequirementType))
360369
{
361-
throw new FeatureManagementException(
362-
FeatureManagementError.InvalidConfigurationSetting,
363-
$"Invalid value '{rawRequirementType}' for field '{MicrosoftFeatureManagementFields.RequirementType}' of feature '{featureName}'.");
370+
requirementType = ParseEnum<RequirementType>(featureName, rawRequirementType, MicrosoftFeatureManagementFields.RequirementType);
364371
}
365372

366373
featureStatus = FeatureStatus.Conditional;
@@ -512,59 +519,6 @@ private FeatureDefinition ParseMicrosoftFeatureDefinition(IConfigurationSection
512519
};
513520
}
514521

515-
private string GetFeatureName(IConfigurationSection section)
516-
{
517-
if (_microsoftFeatureManagementSchemaEnabled)
518-
{
519-
return section[MicrosoftFeatureManagementFields.Id];
520-
}
521-
522-
return section.Key;
523-
}
524-
525-
private IEnumerable<IConfigurationSection> GetFeatureDefinitionSections()
526-
{
527-
if (!_configuration.GetChildren().Any())
528-
{
529-
Logger?.LogDebug($"Configuration is empty.");
530-
531-
return Enumerable.Empty<IConfigurationSection>();
532-
}
533-
534-
IConfiguration featureManagementConfigurationSection = _configuration
535-
.GetChildren()
536-
.FirstOrDefault(section =>
537-
string.Equals(
538-
section.Key,
539-
_microsoftFeatureManagementSchemaEnabled ?
540-
MicrosoftFeatureManagementFields.FeatureManagementSectionName :
541-
DotnetFeatureManagementFields.FeatureManagementSectionName,
542-
StringComparison.OrdinalIgnoreCase));
543-
544-
if (featureManagementConfigurationSection == null)
545-
{
546-
if (RootConfigurationFallbackEnabled)
547-
{
548-
featureManagementConfigurationSection = _configuration;
549-
}
550-
else
551-
{
552-
Logger?.LogDebug($"No feature management configuration section was found.");
553-
554-
return Enumerable.Empty<IConfigurationSection>();
555-
}
556-
}
557-
558-
if (_microsoftFeatureManagementSchemaEnabled)
559-
{
560-
IConfigurationSection featureFlagsSection = featureManagementConfigurationSection.GetSection(MicrosoftFeatureManagementFields.FeatureFlagsSectionName);
561-
562-
return featureFlagsSection.GetChildren();
563-
}
564-
565-
return featureManagementConfigurationSection.GetChildren();
566-
}
567-
568522
private T ParseEnum<T>(string feature, string rawValue, string fieldKeyword)
569523
where T : struct, Enum
570524
{

tests/Tests.FeatureManagement/DotnetFeatureManagementSchema.json

Lines changed: 0 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -11,80 +11,6 @@
1111
}
1212
}
1313
]
14-
},
15-
"ContextualFeature": {
16-
"EnabledFor": [
17-
{
18-
"Name": "ContextualTest",
19-
"Parameters": {
20-
"AllowedAccounts": [
21-
"abc"
22-
]
23-
}
24-
}
25-
]
26-
},
27-
"AnyFilterFeature": {
28-
"RequirementType": "Any",
29-
"EnabledFor": [
30-
{
31-
"Name": "Test",
32-
"Parameters": {
33-
"Id": "1"
34-
}
35-
},
36-
{
37-
"Name": "Test",
38-
"Parameters": {
39-
"Id": "2"
40-
}
41-
}
42-
]
43-
},
44-
"AllFilterFeature": {
45-
"RequirementType": "all",
46-
"EnabledFor": [
47-
{
48-
"Name": "Test",
49-
"Parameters": {
50-
"Id": "1"
51-
}
52-
},
53-
{
54-
"Name": "Test",
55-
"Parameters": {
56-
"Id": "2"
57-
}
58-
}
59-
]
60-
},
61-
"FeatureUsesFiltersWithDuplicatedAlias": {
62-
"RequirementType": "all",
63-
"EnabledFor": [
64-
{
65-
"Name": "DuplicatedFilterName"
66-
},
67-
{
68-
"Name": "Percentage",
69-
"Parameters": {
70-
"Value": 100
71-
}
72-
}
73-
]
74-
},
75-
"CustomFilterFeature": {
76-
"EnabledFor": [
77-
{
78-
"Name": "CustomTargetingFilter",
79-
"Parameters": {
80-
"Audience": {
81-
"Users": [
82-
"Jeff"
83-
]
84-
}
85-
}
86-
}
87-
]
8814
}
8915
}
9016
}

0 commit comments

Comments
 (0)