Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ public sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionP
//
// 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 readonly IConfiguration _configuration;
private IEnumerable<IConfigurationSection> _dotnetFeatureDefinitionSections;
private IEnumerable<IConfigurationSection> _microsoftFeatureDefinitionSections;
private readonly ConcurrentDictionary<string, Task<FeatureDefinition>> _definitions;
private IDisposable _changeSubscription;
private int _stale = 0;
Expand All @@ -48,6 +49,10 @@ public ConfigurationFeatureDefinitionProvider(IConfiguration configuration)
{
return Task.FromResult(GetMicrosoftSchemaFeatureDefinition(featureName) ?? GetDotnetSchemaFeatureDefinition(featureName));
};

_dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();

_microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
}

/// <summary>
Expand Down Expand Up @@ -90,6 +95,10 @@ public Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName)
if (Interlocked.Exchange(ref _stale, 0) != 0)
{
_definitions.Clear();

_dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();

_microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
}

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

IEnumerable<IConfigurationSection> microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
_dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();

_microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
}

foreach (IConfigurationSection featureSection in microsoftFeatureDefinitionSections)
foreach (IConfigurationSection featureSection in _microsoftFeatureDefinitionSections)
{
string featureName = featureSection[MicrosoftFeatureManagementFields.Id];

Expand All @@ -132,9 +143,7 @@ public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
}
}

IEnumerable<IConfigurationSection> dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();

foreach (IConfigurationSection featureSection in dotnetFeatureDefinitionSections)
foreach (IConfigurationSection featureSection in _dotnetFeatureDefinitionSections)
{
string featureName = featureSection.Key;

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

private FeatureDefinition GetDotnetSchemaFeatureDefinition(string featureName)
{
IEnumerable<IConfigurationSection> dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();

IConfigurationSection dotnetFeatureDefinitionConfiguration = dotnetFeatureDefinitionSections
IConfigurationSection dotnetFeatureDefinitionConfiguration = _dotnetFeatureDefinitionSections
.FirstOrDefault(section =>
string.Equals(section.Key, featureName, StringComparison.OrdinalIgnoreCase));

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

private FeatureDefinition GetMicrosoftSchemaFeatureDefinition(string featureName)
{
IEnumerable<IConfigurationSection> microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();

IConfigurationSection microsoftFeatureDefinitionConfiguration = microsoftFeatureDefinitionSections
IConfigurationSection microsoftFeatureDefinitionConfiguration = _microsoftFeatureDefinitionSections
.LastOrDefault(section =>
string.Equals(section[MicrosoftFeatureManagementFields.Id], featureName, StringComparison.OrdinalIgnoreCase));

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

private IEnumerable<IConfigurationSection> GetMicrosoftFeatureDefinitionSections()
{
return _configuration.GetSection(MicrosoftFeatureManagementFields.FeatureManagementSectionName)
.GetSection(MicrosoftFeatureManagementFields.FeatureFlagsSectionName)
.GetChildren();
var featureDefinitionSections = new List<IConfigurationSection>();

FindFeatureFlags(_configuration, featureDefinitionSections);

return featureDefinitionSections;
}

private void FindFeatureFlags(IConfiguration configuration, List<IConfigurationSection> featureDefinitionSections)
{
if (!(configuration is IConfigurationRoot configurationRoot) ||
configurationRoot.Providers.Any(provider =>
!(provider is ConfigurationProvider) && !(provider is ChainedConfigurationProvider)))
{
IConfigurationSection featureFlagsSection = configuration
.GetSection(MicrosoftFeatureManagementFields.FeatureManagementSectionName)
.GetSection(MicrosoftFeatureManagementFields.FeatureFlagsSectionName);

if (featureFlagsSection.Exists())
{
featureDefinitionSections.AddRange(featureFlagsSection.GetChildren());
}

return;
}

foreach (IConfigurationProvider provider in configurationRoot.Providers)
{
if (provider is ConfigurationProvider configurationProvider)
{
//
// Cannot use the original provider directly as its reload token is subscribed
var onDemandConfigurationProvider = new OnDemandConfigurationProvider(configurationProvider);

var onDemandConfigurationRoot = new ConfigurationRoot(new[] { onDemandConfigurationProvider });

IConfigurationSection featureFlagsSection = onDemandConfigurationRoot
.GetSection(MicrosoftFeatureManagementFields.FeatureManagementSectionName)
.GetSection(MicrosoftFeatureManagementFields.FeatureFlagsSectionName);

if (featureFlagsSection.Exists())
{
featureDefinitionSections.AddRange(featureFlagsSection.GetChildren());
}
}
else if (provider is ChainedConfigurationProvider chainedProvider)
{
FindFeatureFlags(chainedProvider.Configuration, featureDefinitionSections);
}
}
}

private FeatureDefinition ParseDotnetSchemaFeatureDefinition(IConfigurationSection configurationSection)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@

<ItemGroup>
<PackageReference Include="Microsoft.Bcl.TimeProvider" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.1.23" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.1.10" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
</ItemGroup>

Expand Down
18 changes: 18 additions & 0 deletions src/Microsoft.FeatureManagement/OnDemandConfigurationProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.Extensions.Configuration;
using System.Collections.Generic;
using System.Reflection;

namespace Microsoft.FeatureManagement
{
internal class OnDemandConfigurationProvider : ConfigurationProvider
{
private static readonly PropertyInfo _DataProperty = typeof(ConfigurationProvider).GetProperty(nameof(Data), BindingFlags.NonPublic | BindingFlags.Instance);

public OnDemandConfigurationProvider(ConfigurationProvider configurationProvider)
{
var data = _DataProperty.GetValue(configurationProvider) as IDictionary<string, string>;

Data = data;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
</ItemGroup>

<ItemGroup>
Expand Down
44 changes: 44 additions & 0 deletions tests/Tests.FeatureManagement/FeatureManagementTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,50 @@ public async Task LastFeatureFlagWins()

Assert.True(await featureManager.IsEnabledAsync(Features.DuplicateFlag));
}

[Fact]
public async Task MergesFeatureFlagsFromDifferentConfigurationSources()
{
/*
* appsettings1.json
* Feature1: true
* Feature2: true
* FeatureA: true
*
* appsettings2.json
* Feature1: true
* Feature2: false
* FeatureB: true
*
* appsettings3.json
* Feature1: false
* Feature2: false
* FeatureC: true
*/

IConfiguration configuration1 = new ConfigurationBuilder()
.AddJsonFile("appsettings1.json")
.AddJsonFile("appsettings2.json")
.Build();

IConfiguration configuration2 = new ConfigurationBuilder()
.AddConfiguration(configuration1) // chained configuration
.AddJsonFile("appsettings3.json")
.Build();

var featureManager1 = new FeatureManager(new ConfigurationFeatureDefinitionProvider(configuration1));
Assert.True(await featureManager1.IsEnabledAsync("FeatureA"));
Assert.True(await featureManager1.IsEnabledAsync("FeatureB"));
Assert.True(await featureManager1.IsEnabledAsync("Feature1"));
Assert.False(await featureManager1.IsEnabledAsync("Feature2")); // appsettings2 should override appsettings1

var featureManager2 = new FeatureManager(new ConfigurationFeatureDefinitionProvider(configuration2));
Assert.True(await featureManager2.IsEnabledAsync("FeatureA"));
Assert.True(await featureManager2.IsEnabledAsync("FeatureB"));
Assert.True(await featureManager2.IsEnabledAsync("FeatureC"));
Assert.False(await featureManager2.IsEnabledAsync("Feature1")); // appsettings3 should override previous settings
Assert.False(await featureManager2.IsEnabledAsync("Feature2")); // appsettings3 should override previous settings
}
}

public class FeatureManagementFeatureFilterGeneralTest
Expand Down
17 changes: 13 additions & 4 deletions tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
</ItemGroup>

<ItemGroup>
Expand All @@ -43,6 +43,15 @@
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="appsettings1.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="appsettings2.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="appsettings3.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="DotnetFeatureManagementSchema.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
Expand Down
18 changes: 18 additions & 0 deletions tests/Tests.FeatureManagement/appsettings1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"feature_management": {
"feature_flags": [
{
"id": "Feature1",
"enabled": true
},
{
"id": "Feature2",
"enabled": true
},
{
"id": "FeatureA",
"enabled": true
}
]
}
}
18 changes: 18 additions & 0 deletions tests/Tests.FeatureManagement/appsettings2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"feature_management": {
"feature_flags": [
{
"id": "Feature1",
"enabled": true
},
{
"id": "Feature2",
"enabled": false
},
{
"id": "FeatureB",
"enabled": true
}
]
}
}
18 changes: 18 additions & 0 deletions tests/Tests.FeatureManagement/appsettings3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"feature_management": {
"feature_flags": [
{
"id": "Feature1",
"enabled": false
},
{
"id": "Feature2",
"enabled": false
},
{
"id": "FeatureC",
"enabled": true
}
]
}
}