Skip to content

Implement a default/global feature definition #175

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
@@ -0,0 +1,24 @@
using System.Collections.Generic;

namespace Microsoft.FeatureManagement.Comparers
{
/// <summary>
/// Compares two FeatureFilterConfiguration using only the name
/// </summary>
public class CompareByNameFeatureFilterConfigurationComparer : IEqualityComparer<FeatureFilterConfiguration>
{
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
using System;
Expand All @@ -9,6 +10,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.FeatureManagement.Comparers;

namespace Microsoft.FeatureManagement
{
Expand All @@ -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<string, FeatureDefinition> _definitions;
private IDisposable _changeSubscription;
Expand Down Expand Up @@ -56,6 +59,11 @@ public Task<FeatureDefinition> 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);
}

Expand All @@ -77,14 +85,14 @@ public async IAsyncEnumerable<FeatureDefinition> 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)
{
Expand Down Expand Up @@ -149,14 +157,16 @@ We support
}
else
{
IEnumerable<IConfigurationSection> filterSections = configurationSection.GetSection(FeatureFiltersSectionName).GetChildren();
IEnumerable<IConfigurationSection> 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()
{
Expand All @@ -178,7 +188,8 @@ private IEnumerable<IConfigurationSection> 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
Expand All @@ -189,5 +200,26 @@ private IEnumerable<IConfigurationSection> 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())
};
}
}
}
}
21 changes: 21 additions & 0 deletions tests/Tests.FeatureManagement/FeatureManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestFilter>();

ServiceProvider serviceProvider = services.BuildServiceProvider();

var featureDefinitionProvider = serviceProvider.GetRequiredService<IFeatureDefinitionProvider>();

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
Expand Down
19 changes: 19 additions & 0 deletions tests/Tests.FeatureManagement/appsettings-with-default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",

"FeatureManagement": {
"Default": {
"EnabledFor": [
{
"name": "Test"
}
]
},
"OffTestFeature": false
}
}