Skip to content
Open
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 @@ -23,6 +23,7 @@ public sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionP
private readonly ConfigurationFeatureDefinitionProviderOptions _options;
private IEnumerable<IConfigurationSection> _dotnetFeatureDefinitionSections;
private IEnumerable<IConfigurationSection> _microsoftFeatureDefinitionSections;
private IDictionary<string, FeatureDefinitionSchema> _featureDefinitionSchemas;
private readonly ConcurrentDictionary<string, Task<FeatureDefinition>> _definitions;
private IDisposable _changeSubscription;
private int _stale = 0;
Expand All @@ -31,6 +32,12 @@ public sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionP

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

private enum FeatureDefinitionSchema
{
Dotnet,
Microsoft
}

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

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

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

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

_microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
LoadFeatureDefinitionSections();

_definitions.Clear();
}
Expand All @@ -128,18 +133,21 @@ public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()

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

_microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
LoadFeatureDefinitionSections();

_definitions.Clear();
}

HashSet<string> processedFeatureNames = _options.CustomConfigurationMergingEnabled
? new HashSet<string>(StringComparer.OrdinalIgnoreCase)
: null;

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

if (string.IsNullOrEmpty(featureName))
if (string.IsNullOrEmpty(featureName) ||
(processedFeatureNames != null && !processedFeatureNames.Add(featureName)))
{
continue;
}
Expand All @@ -158,7 +166,8 @@ public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
{
string featureName = featureSection.Key;

if (string.IsNullOrEmpty(featureName))
if (string.IsNullOrEmpty(featureName) ||
(processedFeatureNames != null && !processedFeatureNames.Add(featureName)))
{
continue;
}
Expand All @@ -178,12 +187,54 @@ private void EnsureInit()
{
if (_initialized == 0)
{
_dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();
LoadFeatureDefinitionSections();

_initialized = 1;
}
}

private void LoadFeatureDefinitionSections()
{
_dotnetFeatureDefinitionSections = GetDotnetFeatureDefinitionSections();

if (!_options.CustomConfigurationMergingEnabled)
{
_microsoftFeatureDefinitionSections = GetMicrosoftFeatureDefinitionSections();
_featureDefinitionSchemas = null;
return;
}

_initialized = 1;
var microsoftFeatureDefinitionSections = new List<IConfigurationSection>();
var featureDefinitionSchemas = new Dictionary<string, FeatureDefinitionSchema>(StringComparer.OrdinalIgnoreCase);

FindFeatureDefinitions(_configuration, microsoftFeatureDefinitionSections, featureDefinitionSchemas);

//
// Root configuration fallback definitions cannot conflict with Microsoft schema definitions.
foreach (IConfigurationSection featureSection in _dotnetFeatureDefinitionSections.Where(section => !featureDefinitionSchemas.ContainsKey(section.Key)))
{
featureDefinitionSchemas[featureSection.Key] = FeatureDefinitionSchema.Dotnet;
}

_microsoftFeatureDefinitionSections = microsoftFeatureDefinitionSections;
_featureDefinitionSchemas = featureDefinitionSchemas;
}

private FeatureDefinition GetFeatureDefinition(string featureName)
{
if (!_options.CustomConfigurationMergingEnabled)
{
return GetMicrosoftSchemaFeatureDefinition(featureName) ?? GetDotnetSchemaFeatureDefinition(featureName);
}

if (!_featureDefinitionSchemas.TryGetValue(featureName, out FeatureDefinitionSchema schema))
{
return null;
}

return schema == FeatureDefinitionSchema.Microsoft
? GetMicrosoftSchemaFeatureDefinition(featureName)
: GetDotnetSchemaFeatureDefinition(featureName);
}

private FeatureDefinition GetDotnetSchemaFeatureDefinition(string featureName)
Expand Down Expand Up @@ -239,35 +290,21 @@ private IEnumerable<IConfigurationSection> GetDotnetFeatureDefinitionSections()

private IEnumerable<IConfigurationSection> GetMicrosoftFeatureDefinitionSections()
{
if (!_options.CustomConfigurationMergingEnabled)
{
return _configuration.GetSection(MicrosoftFeatureManagementFields.FeatureManagementSectionName)
.GetSection(MicrosoftFeatureManagementFields.FeatureFlagsSectionName)
.GetChildren();
}

var featureDefinitionSections = new List<IConfigurationSection>();

FindFeatureFlags(_configuration, featureDefinitionSections);

return featureDefinitionSections;
return _configuration.GetSection(MicrosoftFeatureManagementFields.FeatureManagementSectionName)
.GetSection(MicrosoftFeatureManagementFields.FeatureFlagsSectionName)
.GetChildren();
}

private void FindFeatureFlags(IConfiguration configuration, List<IConfigurationSection> featureDefinitionSections)
private void FindFeatureDefinitions(
IConfiguration configuration,
List<IConfigurationSection> microsoftFeatureDefinitionSections,
IDictionary<string, FeatureDefinitionSchema> featureDefinitionSchemas)
{
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());
}

AddFeatureDefinitions(configuration, microsoftFeatureDefinitionSections, featureDefinitionSchemas);
return;
}

Expand All @@ -281,18 +318,41 @@ 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());
}
AddFeatureDefinitions(onDemandConfigurationRoot, microsoftFeatureDefinitionSections, featureDefinitionSchemas);
}
else if (provider is ChainedConfigurationProvider chainedProvider)
{
FindFeatureFlags(chainedProvider.Configuration, featureDefinitionSections);
FindFeatureDefinitions(chainedProvider.Configuration, microsoftFeatureDefinitionSections, featureDefinitionSchemas);
}
}
}

private void AddFeatureDefinitions(
IConfiguration configuration,
List<IConfigurationSection> microsoftFeatureDefinitionSections,
IDictionary<string, FeatureDefinitionSchema> featureDefinitionSchemas)
{
IConfigurationSection dotnetFeatureManagementSection = configuration
.GetSection(DotnetFeatureManagementFields.FeatureManagementSectionName);

foreach (IConfigurationSection featureSection in dotnetFeatureManagementSection.GetChildren())
{
featureDefinitionSchemas[featureSection.Key] = FeatureDefinitionSchema.Dotnet;
}

IConfigurationSection microsoftFeatureFlagsSection = configuration
.GetSection(MicrosoftFeatureManagementFields.FeatureManagementSectionName)
.GetSection(MicrosoftFeatureManagementFields.FeatureFlagsSectionName);

foreach (IConfigurationSection featureSection in microsoftFeatureFlagsSection.GetChildren())
{
microsoftFeatureDefinitionSections.Add(featureSection);

string featureName = featureSection[MicrosoftFeatureManagementFields.Id];

if (!string.IsNullOrEmpty(featureName))
{
featureDefinitionSchemas[featureName] = FeatureDefinitionSchema.Microsoft;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ 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.
///
/// .NET schema feature flags continue to use .NET's native configuration merging behavior within that schema.
/// When custom merging is enabled, 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.
/// When custom merging is disabled, 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