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 @@ -21,8 +21,7 @@ public sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionP
{
private readonly IConfiguration _configuration;
private readonly ConfigurationFeatureDefinitionProviderOptions _options;
private IEnumerable<IConfigurationSection> _dotnetFeatureDefinitionSections;
private IEnumerable<IConfigurationSection> _microsoftFeatureDefinitionSections;
private IEnumerable<FeatureDefinitionSectionsBySource> _featureDefinitionSources;
private readonly ConcurrentDictionary<string, Task<FeatureDefinition>> _definitions;
private IDisposable _changeSubscription;
private int _stale = 0;
Expand All @@ -31,6 +30,21 @@ public sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionP

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

private sealed class FeatureDefinitionSectionsBySource
{
public FeatureDefinitionSectionsBySource(
IEnumerable<IConfigurationSection> dotnetSections,
IEnumerable<IConfigurationSection> microsoftSections)
{
DotnetSections = dotnetSections;
MicrosoftSections = microsoftSections;
}

public IEnumerable<IConfigurationSection> DotnetSections { get; }

public IEnumerable<IConfigurationSection> MicrosoftSections { get; }
}

/// <summary>
/// Creates a configuration feature definition provider.
/// </summary>
Expand Down Expand Up @@ -58,7 +72,7 @@ public ConfigurationFeatureDefinitionProvider(

_getFeatureDefinitionFunc = (featureName) =>
{
return Task.FromResult(GetMicrosoftSchemaFeatureDefinition(featureName) ?? GetDotnetSchemaFeatureDefinition(featureName));
return Task.FromResult(GetFeatureDefinition(featureName));
};
}

Expand Down Expand Up @@ -103,9 +117,7 @@ public Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName)

if (Interlocked.Exchange(ref _stale, 0) != 0)
{
_dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();

_microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
LoadFeatureDefinitionSections();

_definitions.Clear();
}
Expand All @@ -128,67 +140,90 @@ public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()

if (Interlocked.Exchange(ref _stale, 0) != 0)
{
_dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();

_microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
LoadFeatureDefinitionSections();

_definitions.Clear();
}

foreach (IConfigurationSection featureSection in _microsoftFeatureDefinitionSections)
HashSet<string> processedFeatureNames = _options.CustomConfigurationMergingEnabled
? new HashSet<string>(StringComparer.OrdinalIgnoreCase)
: null;

foreach (FeatureDefinitionSectionsBySource source in _featureDefinitionSources)
{
string featureName = featureSection[MicrosoftFeatureManagementFields.Id];
IEnumerable<string> featureNames = source.MicrosoftSections
.Select(section => section[MicrosoftFeatureManagementFields.Id])
.Concat(source.DotnetSections.Select(section => section.Key));

if (string.IsNullOrEmpty(featureName))
foreach (string featureName in featureNames)
{
continue;
}
if (string.IsNullOrEmpty(featureName) ||
(processedFeatureNames != null && !processedFeatureNames.Add(featureName)))
{
continue;
}

//
// Underlying IConfigurationSection data is dynamic so latest feature definitions are returned
FeatureDefinition definition = _definitions.GetOrAdd(featureName, _getFeatureDefinitionFunc).Result;
FeatureDefinition definition = _definitions.GetOrAdd(featureName, _getFeatureDefinitionFunc).Result;

if (definition != null)
{
yield return definition;
if (definition != null)
{
yield return definition;
}
}
}
}

foreach (IConfigurationSection featureSection in _dotnetFeatureDefinitionSections)
private void EnsureInit()
{
if (_initialized == 0)
{
string featureName = featureSection.Key;
LoadFeatureDefinitionSections();

if (string.IsNullOrEmpty(featureName))
{
continue;
}
_initialized = 1;
}
}

//
// Underlying IConfigurationSection data is dynamic so latest feature definitions are returned
FeatureDefinition definition = _definitions.GetOrAdd(featureName, _getFeatureDefinitionFunc).Result;
private void LoadFeatureDefinitionSections()
{
//
// Determine root fallback from the full configuration, not from individual providers.
bool useRootConfiguration = RootConfigurationFallbackEnabled &&
!_configuration.GetSection(DotnetFeatureManagementFields.FeatureManagementSectionName).Exists() &&
!_configuration.GetChildren().Any(section =>
string.Equals(section.Key, MicrosoftFeatureManagementFields.FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase));

if (definition != null)
{
yield return definition;
}
if (!_options.CustomConfigurationMergingEnabled)
{
_featureDefinitionSources = new[] { GetFeatureDefinitionSections(_configuration, useRootConfiguration) };
return;
}

var featureDefinitionSources = new List<FeatureDefinitionSectionsBySource>();

FindFeatureDefinitionSources(_configuration, useRootConfiguration, featureDefinitionSources);

_featureDefinitionSources = featureDefinitionSources;
}

private void EnsureInit()
private FeatureDefinition GetFeatureDefinition(string featureName)
{
if (_initialized == 0)
foreach (FeatureDefinitionSectionsBySource source in _featureDefinitionSources)
{
_dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();

_microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
FeatureDefinition definition = GetMicrosoftSchemaFeatureDefinition(featureName, source.MicrosoftSections) ??
GetDotnetSchemaFeatureDefinition(featureName, source.DotnetSections);

_initialized = 1;
if (definition != null)
{
return definition;
}
}

return null;
}

private FeatureDefinition GetDotnetSchemaFeatureDefinition(string featureName)
private FeatureDefinition GetDotnetSchemaFeatureDefinition(string featureName, IEnumerable<IConfigurationSection> sections)
{
IConfigurationSection dotnetFeatureDefinitionConfiguration = _dotnetFeatureDefinitionSections
IConfigurationSection dotnetFeatureDefinitionConfiguration = sections
.FirstOrDefault(section =>
string.Equals(section.Key, featureName, StringComparison.OrdinalIgnoreCase));

Expand All @@ -200,9 +235,9 @@ private FeatureDefinition GetDotnetSchemaFeatureDefinition(string featureName)
return ParseDotnetSchemaFeatureDefinition(dotnetFeatureDefinitionConfiguration);
}

private FeatureDefinition GetMicrosoftSchemaFeatureDefinition(string featureName)
private FeatureDefinition GetMicrosoftSchemaFeatureDefinition(string featureName, IEnumerable<IConfigurationSection> sections)
{
IConfigurationSection microsoftFeatureDefinitionConfiguration = _microsoftFeatureDefinitionSections
IConfigurationSection microsoftFeatureDefinitionConfiguration = sections
.LastOrDefault(section =>
string.Equals(section[MicrosoftFeatureManagementFields.Id], featureName, StringComparison.OrdinalIgnoreCase));

Expand All @@ -214,64 +249,33 @@ private FeatureDefinition GetMicrosoftSchemaFeatureDefinition(string featureName
return ParseMicrosoftSchemaFeatureDefinition(microsoftFeatureDefinitionConfiguration);
}

private IEnumerable<IConfigurationSection> GetDotnetFeatureDefinitionSections()
{
IConfigurationSection featureManagementConfigurationSection = _configuration.GetSection(DotnetFeatureManagementFields.FeatureManagementSectionName);

if (featureManagementConfigurationSection.Exists())
{
return featureManagementConfigurationSection.GetChildren();
}

//
// Root configuration fallback only applies to .NET schema.
// If Microsoft schema can be found, root configuration fallback will not be effective.
if (RootConfigurationFallbackEnabled &&
!_configuration.GetChildren()
.Any(section =>
string.Equals(section.Key, MicrosoftFeatureManagementFields.FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)))
{
return _configuration.GetChildren();
}

return Enumerable.Empty<IConfigurationSection>();
}

private IEnumerable<IConfigurationSection> GetMicrosoftFeatureDefinitionSections()
private FeatureDefinitionSectionsBySource GetFeatureDefinitionSections(IConfiguration configuration, bool useRootConfiguration)
{
if (!_options.CustomConfigurationMergingEnabled)
{
return _configuration.GetSection(MicrosoftFeatureManagementFields.FeatureManagementSectionName)
return new FeatureDefinitionSectionsBySource(
useRootConfiguration
? configuration.GetChildren()
: configuration.GetSection(DotnetFeatureManagementFields.FeatureManagementSectionName).GetChildren(),
configuration.GetSection(MicrosoftFeatureManagementFields.FeatureManagementSectionName)
.GetSection(MicrosoftFeatureManagementFields.FeatureFlagsSectionName)
.GetChildren();
}

var featureDefinitionSections = new List<IConfigurationSection>();

FindFeatureFlags(_configuration, featureDefinitionSections);

return featureDefinitionSections;
.GetChildren());
}

private void FindFeatureFlags(IConfiguration configuration, List<IConfigurationSection> featureDefinitionSections)
private void FindFeatureDefinitionSources(
IConfiguration configuration,
bool useRootConfiguration,
List<FeatureDefinitionSectionsBySource> featureDefinitionSources)
{
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());
}

featureDefinitionSources.Add(GetFeatureDefinitionSections(configuration, useRootConfiguration));
return;
}

foreach (IConfigurationProvider provider in configurationRoot.Providers)
//
// Keep sources in highest-to-lowest precedence order, including chained providers.
foreach (IConfigurationProvider provider in configurationRoot.Providers.Reverse())
{
if (provider is ConfigurationProvider configurationProvider)
{
Expand All @@ -281,18 +285,11 @@ private void FindFeatureFlags(IConfiguration configuration, List<IConfigurationS

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

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

if (featureFlagsSection.Exists())
{
featureDefinitionSections.AddRange(featureFlagsSection.GetChildren());
}
featureDefinitionSources.Add(GetFeatureDefinitionSections(onDemandConfigurationRoot, useRootConfiguration));
}
else if (provider is ChainedConfigurationProvider chainedProvider)
{
FindFeatureFlags(chainedProvider.Configuration, featureDefinitionSections);
FindFeatureDefinitionSources(chainedProvider.Configuration, useRootConfiguration, featureDefinitionSources);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@ namespace Microsoft.FeatureManagement
public class ConfigurationFeatureDefinitionProviderOptions
{
/// <summary>
/// Controls whether to enable the custom configuration merging logic for Microsoft schema feature flags or fall back to .NET's native configuration merging behavior.
/// Controls whether to enable custom configuration merging for feature flags from multiple configuration sources.
/// </summary>
/// <remarks>
/// This option only affects Microsoft schema feature flags (e.g. feature_management:feature_flags arrays). .NET schema feature flags are not affected by this setting.
///
/// The <see cref="ConfigurationFeatureDefinitionProvider"/> uses custom configuration merging logic for Microsoft schema feature flags to ensure that
/// feature flags with the same ID from different configuration sources are merged correctly based on their logical identity rather than array position.
/// By default, the provider bypasses .NET's native array merging behavior which merges arrays by index position and can lead to unexpected results when feature flags are defined across multiple configuration sources.
/// The <see cref="ConfigurationFeatureDefinitionProvider"/> uses custom configuration merging logic to ensure that feature flags with the same ID from
/// different configuration sources are merged correctly based on their logical identity rather than array position. The last configuration source that
/// defines a feature flag wins, even when earlier and later sources use different feature management schemas. If the same configuration source defines a
/// feature flag in both the .NET schema and the Microsoft schema, the Microsoft schema definition takes precedence.
///
/// When custom merging is enabled, both .NET and Microsoft schema feature flag definitions are selected as a whole from the winning source.
/// Properties, feature filters, and filter parameters are not merged across sources. Each overriding source must supply the complete intended definition.
/// This bypasses .NET's native array merging behavior, which merges arrays by index position and can lead to unexpected results when feature flags are defined across multiple configuration sources.
/// For configuration that cannot be inspected provider by provider, the combined configuration is treated as a single source.
/// When custom merging is disabled (the default), .NET's native configuration merging behavior is used, and Microsoft schema definitions take precedence
/// over .NET schema definitions regardless of configuration source order.
///
/// Consider the following configuration sources:
/// Configuration Source 1:
Expand Down
Loading
Loading