Skip to content

Commit 24e7926

Browse files
Merge pull request #536 from microsoft/zhiyuanliang/merge-ff-source
Merge feature flags from different configuration source
1 parent 8c89d08 commit 24e7926

File tree

9 files changed

+202
-25
lines changed

9 files changed

+202
-25
lines changed

src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ public sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionP
2222
//
2323
// IFeatureDefinitionProviderCacheable interface is only used to mark this provider as cacheable. This allows our test suite's
2424
// provider to be marked for caching as well.
25-
2625
private readonly IConfiguration _configuration;
26+
private IEnumerable<IConfigurationSection> _dotnetFeatureDefinitionSections;
27+
private IEnumerable<IConfigurationSection> _microsoftFeatureDefinitionSections;
2728
private readonly ConcurrentDictionary<string, Task<FeatureDefinition>> _definitions;
2829
private IDisposable _changeSubscription;
2930
private int _stale = 0;
@@ -48,6 +49,10 @@ public ConfigurationFeatureDefinitionProvider(IConfiguration configuration)
4849
{
4950
return Task.FromResult(GetMicrosoftSchemaFeatureDefinition(featureName) ?? GetDotnetSchemaFeatureDefinition(featureName));
5051
};
52+
53+
_dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();
54+
55+
_microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
5156
}
5257

5358
/// <summary>
@@ -90,6 +95,10 @@ public Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName)
9095
if (Interlocked.Exchange(ref _stale, 0) != 0)
9196
{
9297
_definitions.Clear();
98+
99+
_dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();
100+
101+
_microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
93102
}
94103

95104
return _definitions.GetOrAdd(featureName, _getFeatureDefinitionFunc);
@@ -109,11 +118,13 @@ public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
109118
if (Interlocked.Exchange(ref _stale, 0) != 0)
110119
{
111120
_definitions.Clear();
112-
}
113121

114-
IEnumerable<IConfigurationSection> microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
122+
_dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();
123+
124+
_microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
125+
}
115126

116-
foreach (IConfigurationSection featureSection in microsoftFeatureDefinitionSections)
127+
foreach (IConfigurationSection featureSection in _microsoftFeatureDefinitionSections)
117128
{
118129
string featureName = featureSection[MicrosoftFeatureManagementFields.Id];
119130

@@ -132,9 +143,7 @@ public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
132143
}
133144
}
134145

135-
IEnumerable<IConfigurationSection> dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();
136-
137-
foreach (IConfigurationSection featureSection in dotnetFeatureDefinitionSections)
146+
foreach (IConfigurationSection featureSection in _dotnetFeatureDefinitionSections)
138147
{
139148
string featureName = featureSection.Key;
140149

@@ -156,9 +165,7 @@ public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
156165

157166
private FeatureDefinition GetDotnetSchemaFeatureDefinition(string featureName)
158167
{
159-
IEnumerable<IConfigurationSection> dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();
160-
161-
IConfigurationSection dotnetFeatureDefinitionConfiguration = dotnetFeatureDefinitionSections
168+
IConfigurationSection dotnetFeatureDefinitionConfiguration = _dotnetFeatureDefinitionSections
162169
.FirstOrDefault(section =>
163170
string.Equals(section.Key, featureName, StringComparison.OrdinalIgnoreCase));
164171

@@ -172,9 +179,7 @@ private FeatureDefinition GetDotnetSchemaFeatureDefinition(string featureName)
172179

173180
private FeatureDefinition GetMicrosoftSchemaFeatureDefinition(string featureName)
174181
{
175-
IEnumerable<IConfigurationSection> microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
176-
177-
IConfigurationSection microsoftFeatureDefinitionConfiguration = microsoftFeatureDefinitionSections
182+
IConfigurationSection microsoftFeatureDefinitionConfiguration = _microsoftFeatureDefinitionSections
178183
.LastOrDefault(section =>
179184
string.Equals(section[MicrosoftFeatureManagementFields.Id], featureName, StringComparison.OrdinalIgnoreCase));
180185

@@ -211,9 +216,55 @@ private IEnumerable<IConfigurationSection> GetDotnetFeatureDefinitionSections()
211216

212217
private IEnumerable<IConfigurationSection> GetMicrosoftFeatureDefinitionSections()
213218
{
214-
return _configuration.GetSection(MicrosoftFeatureManagementFields.FeatureManagementSectionName)
215-
.GetSection(MicrosoftFeatureManagementFields.FeatureFlagsSectionName)
216-
.GetChildren();
219+
var featureDefinitionSections = new List<IConfigurationSection>();
220+
221+
FindFeatureFlags(_configuration, featureDefinitionSections);
222+
223+
return featureDefinitionSections;
224+
}
225+
226+
private void FindFeatureFlags(IConfiguration configuration, List<IConfigurationSection> featureDefinitionSections)
227+
{
228+
if (!(configuration is IConfigurationRoot configurationRoot) ||
229+
configurationRoot.Providers.Any(provider =>
230+
!(provider is ConfigurationProvider) && !(provider is ChainedConfigurationProvider)))
231+
{
232+
IConfigurationSection featureFlagsSection = configuration
233+
.GetSection(MicrosoftFeatureManagementFields.FeatureManagementSectionName)
234+
.GetSection(MicrosoftFeatureManagementFields.FeatureFlagsSectionName);
235+
236+
if (featureFlagsSection.Exists())
237+
{
238+
featureDefinitionSections.AddRange(featureFlagsSection.GetChildren());
239+
}
240+
241+
return;
242+
}
243+
244+
foreach (IConfigurationProvider provider in configurationRoot.Providers)
245+
{
246+
if (provider is ConfigurationProvider configurationProvider)
247+
{
248+
//
249+
// Cannot use the original provider directly as its reload token is subscribed
250+
var onDemandConfigurationProvider = new OnDemandConfigurationProvider(configurationProvider);
251+
252+
var onDemandConfigurationRoot = new ConfigurationRoot(new[] { onDemandConfigurationProvider });
253+
254+
IConfigurationSection featureFlagsSection = onDemandConfigurationRoot
255+
.GetSection(MicrosoftFeatureManagementFields.FeatureManagementSectionName)
256+
.GetSection(MicrosoftFeatureManagementFields.FeatureFlagsSectionName);
257+
258+
if (featureFlagsSection.Exists())
259+
{
260+
featureDefinitionSections.AddRange(featureFlagsSection.GetChildren());
261+
}
262+
}
263+
else if (provider is ChainedConfigurationProvider chainedProvider)
264+
{
265+
FindFeatureFlags(chainedProvider.Configuration, featureDefinitionSections);
266+
}
267+
}
217268
}
218269

219270
private FeatureDefinition ParseDotnetSchemaFeatureDefinition(IConfigurationSection configurationSection)

src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@
4141

4242
<ItemGroup>
4343
<PackageReference Include="Microsoft.Bcl.TimeProvider" Version="8.0.1" />
44-
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.1.23" />
45-
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.1.10" />
46-
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
44+
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
45+
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
46+
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
47+
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
4748
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
4849
</ItemGroup>
4950

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Microsoft.Extensions.Configuration;
2+
using System.Collections.Generic;
3+
using System.Reflection;
4+
5+
namespace Microsoft.FeatureManagement
6+
{
7+
internal class OnDemandConfigurationProvider : ConfigurationProvider
8+
{
9+
private static readonly PropertyInfo _DataProperty = typeof(ConfigurationProvider).GetProperty(nameof(Data), BindingFlags.NonPublic | BindingFlags.Instance);
10+
11+
public OnDemandConfigurationProvider(ConfigurationProvider configurationProvider)
12+
{
13+
var data = _DataProperty.GetValue(configurationProvider) as IDictionary<string, string>;
14+
15+
Data = data;
16+
}
17+
}
18+
}

tests/Tests.FeatureManagement.AspNetCore/Tests.FeatureManagement.AspNetCore.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@
2424
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="8.0.1" />
2525
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
2626
<PackageReference Include="System.Text.Json" Version="8.0.5" />
27-
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
27+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
2828
</ItemGroup>
2929

3030
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
3131
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="8.0.1" />
3232
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
3333
<PackageReference Include="System.Text.Json" Version="8.0.5" />
34-
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
34+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
3535
</ItemGroup>
3636

3737
<ItemGroup>

tests/Tests.FeatureManagement/FeatureManagementTest.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,50 @@ public async Task LastFeatureFlagWins()
394394

395395
Assert.True(await featureManager.IsEnabledAsync(Features.DuplicateFlag));
396396
}
397+
398+
[Fact]
399+
public async Task MergesFeatureFlagsFromDifferentConfigurationSources()
400+
{
401+
/*
402+
* appsettings1.json
403+
* Feature1: true
404+
* Feature2: true
405+
* FeatureA: true
406+
*
407+
* appsettings2.json
408+
* Feature1: true
409+
* Feature2: false
410+
* FeatureB: true
411+
*
412+
* appsettings3.json
413+
* Feature1: false
414+
* Feature2: false
415+
* FeatureC: true
416+
*/
417+
418+
IConfiguration configuration1 = new ConfigurationBuilder()
419+
.AddJsonFile("appsettings1.json")
420+
.AddJsonFile("appsettings2.json")
421+
.Build();
422+
423+
IConfiguration configuration2 = new ConfigurationBuilder()
424+
.AddConfiguration(configuration1) // chained configuration
425+
.AddJsonFile("appsettings3.json")
426+
.Build();
427+
428+
var featureManager1 = new FeatureManager(new ConfigurationFeatureDefinitionProvider(configuration1));
429+
Assert.True(await featureManager1.IsEnabledAsync("FeatureA"));
430+
Assert.True(await featureManager1.IsEnabledAsync("FeatureB"));
431+
Assert.True(await featureManager1.IsEnabledAsync("Feature1"));
432+
Assert.False(await featureManager1.IsEnabledAsync("Feature2")); // appsettings2 should override appsettings1
433+
434+
var featureManager2 = new FeatureManager(new ConfigurationFeatureDefinitionProvider(configuration2));
435+
Assert.True(await featureManager2.IsEnabledAsync("FeatureA"));
436+
Assert.True(await featureManager2.IsEnabledAsync("FeatureB"));
437+
Assert.True(await featureManager2.IsEnabledAsync("FeatureC"));
438+
Assert.False(await featureManager2.IsEnabledAsync("Feature1")); // appsettings3 should override previous settings
439+
Assert.False(await featureManager2.IsEnabledAsync("Feature2")); // appsettings3 should override previous settings
440+
}
397441
}
398442

399443
public class FeatureManagementFeatureFilterGeneralTest

tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFrameworks>net48;net8.0;net9.0</TargetFrameworks>
@@ -18,21 +18,21 @@
1818
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
1919
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.32" />
2020
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.32" />
21-
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
21+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
2222
</ItemGroup>
2323

2424
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
2525
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
2626
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
2727
<PackageReference Include="System.Text.Json" Version="8.0.5" />
28-
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
28+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
2929
</ItemGroup>
3030

3131
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
3232
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
3333
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
3434
<PackageReference Include="System.Text.Json" Version="8.0.5" />
35-
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
35+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
3636
</ItemGroup>
3737

3838
<ItemGroup>
@@ -43,6 +43,15 @@
4343
<None Update="appsettings.json">
4444
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
4545
</None>
46+
<None Update="appsettings1.json">
47+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
48+
</None>
49+
<None Update="appsettings2.json">
50+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
51+
</None>
52+
<None Update="appsettings3.json">
53+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
54+
</None>
4655
<None Update="DotnetFeatureManagementSchema.json">
4756
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
4857
</None>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"feature_management": {
3+
"feature_flags": [
4+
{
5+
"id": "Feature1",
6+
"enabled": true
7+
},
8+
{
9+
"id": "Feature2",
10+
"enabled": true
11+
},
12+
{
13+
"id": "FeatureA",
14+
"enabled": true
15+
}
16+
]
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"feature_management": {
3+
"feature_flags": [
4+
{
5+
"id": "Feature1",
6+
"enabled": true
7+
},
8+
{
9+
"id": "Feature2",
10+
"enabled": false
11+
},
12+
{
13+
"id": "FeatureB",
14+
"enabled": true
15+
}
16+
]
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"feature_management": {
3+
"feature_flags": [
4+
{
5+
"id": "Feature1",
6+
"enabled": false
7+
},
8+
{
9+
"id": "Feature2",
10+
"enabled": false
11+
},
12+
{
13+
"id": "FeatureC",
14+
"enabled": true
15+
}
16+
]
17+
}
18+
}

0 commit comments

Comments
 (0)