diff --git a/src/Microsoft.FeatureManagement/Comparers/CompareByNameFeatureFilterConfigurationComparer.cs b/src/Microsoft.FeatureManagement/Comparers/CompareByNameFeatureFilterConfigurationComparer.cs
new file mode 100644
index 00000000..ca394748
--- /dev/null
+++ b/src/Microsoft.FeatureManagement/Comparers/CompareByNameFeatureFilterConfigurationComparer.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+
+namespace Microsoft.FeatureManagement.Comparers
+{
+ ///
+ /// Compares two FeatureFilterConfiguration using only the name
+ ///
+ public class CompareByNameFeatureFilterConfigurationComparer : IEqualityComparer
+ {
+ public bool Equals(FeatureFilterConfiguration x, FeatureFilterConfiguration y)
+ {
+ if (ReferenceEquals(x, y)) return true;
+ if (ReferenceEquals(x, null)) return false;
+ if (ReferenceEquals(y, null)) return false;
+ if (x.GetType() != y.GetType()) return false;
+ return x.Name == y.Name;
+ }
+
+ public int GetHashCode(FeatureFilterConfiguration obj)
+ {
+ return (obj.Name != null ? obj.Name.GetHashCode() : 0);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
index 8763b850..7ff118c8 100644
--- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
+++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
+
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
using System;
@@ -9,6 +10,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.FeatureManagement.Comparers;
namespace Microsoft.FeatureManagement
{
@@ -18,6 +20,7 @@ namespace Microsoft.FeatureManagement
sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionProvider, IDisposable
{
private const string FeatureFiltersSectionName = "EnabledFor";
+ private const string DefaultDefinitionSectionName = "Default";
private readonly IConfiguration _configuration;
private readonly ConcurrentDictionary _definitions;
private IDisposable _changeSubscription;
@@ -56,6 +59,11 @@ public Task GetFeatureDefinitionAsync(string featureName)
// Query by feature name
FeatureDefinition definition = _definitions.GetOrAdd(featureName, (name) => ReadFeatureDefinition(name));
+ FeatureDefinition defaultDefinition =
+ _definitions.GetOrAdd(DefaultDefinitionSectionName, (name) => ReadFeatureDefinition(name));
+
+ definition = MergeDefinitions(definition, defaultDefinition);
+
return Task.FromResult(definition);
}
@@ -77,14 +85,14 @@ public async IAsyncEnumerable GetAllFeatureDefinitionsAsync()
{
//
// Underlying IConfigurationSection data is dynamic so latest feature definitions are returned
- yield return _definitions.GetOrAdd(featureSection.Key, (_) => ReadFeatureDefinition(featureSection));
+ yield return _definitions.GetOrAdd(featureSection.Key, (_) => ReadFeatureDefinition(featureSection));
}
}
private FeatureDefinition ReadFeatureDefinition(string featureName)
{
IConfigurationSection configuration = GetFeatureDefinitionSections()
- .FirstOrDefault(section => section.Key.Equals(featureName, StringComparison.OrdinalIgnoreCase));
+ .FirstOrDefault(section => section.Key.Equals(featureName, StringComparison.OrdinalIgnoreCase));
if (configuration == null)
{
@@ -149,14 +157,16 @@ We support
}
else
{
- IEnumerable filterSections = configurationSection.GetSection(FeatureFiltersSectionName).GetChildren();
+ IEnumerable filterSections =
+ configurationSection.GetSection(FeatureFiltersSectionName).GetChildren();
foreach (IConfigurationSection section in filterSections)
{
//
// Arrays in json such as "myKey": [ "some", "values" ]
// Are accessed through the configuration system by using the array index as the property name, e.g. "myKey": { "0": "some", "1": "values" }
- if (int.TryParse(section.Key, out int i) && !string.IsNullOrEmpty(section[nameof(FeatureFilterConfiguration.Name)]))
+ if (int.TryParse(section.Key, out int i) &&
+ !string.IsNullOrEmpty(section[nameof(FeatureFilterConfiguration.Name)]))
{
enabledFor.Add(new FeatureFilterConfiguration()
{
@@ -178,7 +188,8 @@ private IEnumerable GetFeatureDefinitionSections()
{
const string FeatureManagementSectionName = "FeatureManagement";
- if (_configuration.GetChildren().Any(s => s.Key.Equals(FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)))
+ if (_configuration.GetChildren().Any(s =>
+ s.Key.Equals(FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)))
{
//
// Look for feature definitions under the "FeatureManagement" section
@@ -189,5 +200,26 @@ private IEnumerable GetFeatureDefinitionSections()
return _configuration.GetChildren();
}
}
+
+ private FeatureDefinition MergeDefinitions(FeatureDefinition targetDefinition,
+ FeatureDefinition otherDefinition)
+ {
+ if (targetDefinition is null)
+ {
+ return null;
+ }
+
+ if (otherDefinition is null)
+ {
+ return targetDefinition;
+ }
+
+ return new FeatureDefinition
+ {
+ Name = targetDefinition.Name,
+ EnabledFor = targetDefinition.EnabledFor.Union(otherDefinition.EnabledFor,
+ new CompareByNameFeatureFilterConfigurationComparer())
+ };
+ }
}
-}
+}
\ No newline at end of file
diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs
index af67fe8e..076571fe 100644
--- a/tests/Tests.FeatureManagement/FeatureManagement.cs
+++ b/tests/Tests.FeatureManagement/FeatureManagement.cs
@@ -669,6 +669,27 @@ public async Task ThreadsafeSnapshot()
}
}
+ [Fact]
+ public async Task DefaultFeatureDefinition()
+ {
+ IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings-with-default.json").Build();
+
+ var services = new ServiceCollection();
+
+ services
+ .AddSingleton(config)
+ .AddFeatureManagement()
+ .AddFeatureFilter();
+
+ ServiceProvider serviceProvider = services.BuildServiceProvider();
+
+ var featureDefinitionProvider = serviceProvider.GetRequiredService();
+
+ var definition = await featureDefinitionProvider.GetFeatureDefinitionAsync(nameof(Features.OffTestFeature));
+
+ Assert.Collection(definition.EnabledFor, configuration => configuration.Name.Equals("Test"));
+ }
+
private static void DisableEndpointRouting(MvcOptions options)
{
#if NET5_0 || NETCOREAPP3_1
diff --git a/tests/Tests.FeatureManagement/appsettings-with-default.json b/tests/Tests.FeatureManagement/appsettings-with-default.json
new file mode 100644
index 00000000..d17e5830
--- /dev/null
+++ b/tests/Tests.FeatureManagement/appsettings-with-default.json
@@ -0,0 +1,19 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+
+ "FeatureManagement": {
+ "Default": {
+ "EnabledFor": [
+ {
+ "name": "Test"
+ }
+ ]
+ },
+ "OffTestFeature": false
+ }
+}