Skip to content
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

Added support for custom feature providers. #79

Merged
Merged
Show file tree
Hide file tree
Changes from 2 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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,28 @@ Feature state is provided by the IConfiguration system. Any caching and dynamic
### Snapshot
There are scenarios which require the state of a feature to remain consistent during the lifetime of a request. The values returned from the standard `IFeatureManager` may change if the `IConfiguration` source which it is pulling from is updated during the request. This can be prevented by using `IFeatureManagerSnapshot`. `IFeatureManagerSnapshot` can be retrieved in the same manner as `IFeatureManager`. `IFeatureManagerSnapshot` implements the interface of `IFeatureManager`, but it caches the first evaluated state of a feature during a request and will return the same state of a feature during its lifetime.

## Custom Feature Providers

The built-in mechanism for defining feature flags relies on .NET Core's configuration system. This allows for features to be defined in an [appsettings.json](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#jcp) file or in configuration providers like [Azure App Configuration](https://docs.microsoft.com/en-us/azure/azure-app-configuration/quickstart-feature-flag-aspnet-core?tabs=core2x). It is possible to substitute this behavior and take complete control of where feature definitions are read from. This enables applications to pull feature flags from custom sources such as a database or a feature management service.

To customize the loading of feature definitions, one must implement the `IFeatureDefinitionProvider` interface.

```
public interface IFeatureDefinitionProvider
{
Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName);

IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync();
}
```

To use an implementation of `IFeatureDefinitionProvider` it must be added into the service collection before adding feature management. The following example adds an implementation of `IFeatureDefinitionProvider` named `InMemoryFeatureSettingsProvider`.
jimmyca15 marked this conversation as resolved.
Show resolved Hide resolved

```
services.AddSingleton<IFeatureDefinitionProvider, InMemoryFeatureSettingsProvider>()
.AddFeatureManagement()
```

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you like the idea that we add these two APIs so we won't always add ConfigurationFeatureDefinitionProvider to DI and make it easier for users? Or is this too much and we can always add in the future?

AddFeatureManagement<IFeatureDefinitionProvider>()
AddFeatureManagement(IFeatureDefinitionProvider fd)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd go with add in the future for that one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Especially if in the future we get a request to provide the capability to pull feature settings from an external source. Then it will point out that maybe the API that you proposed could help with the discoverability that we already offer that option.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Makes sense.

# Contributing

This project welcomes contributions and suggestions. Most contributions require you to agree to a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@
namespace Microsoft.FeatureManagement
{
/// <summary>
/// A feature settings provider that pulls settings from the .NET Core <see cref="IConfiguration"/> system.
/// A feature definition provider that pulls settings from the .NET Core <see cref="IConfiguration"/> system.
/// </summary>
sealed class ConfigurationFeatureSettingsProvider : IFeatureSettingsProvider, IDisposable
sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionProvider, IDisposable
{
private const string FeatureFiltersSectionName = "EnabledFor";
private readonly IConfiguration _configuration;
private readonly ConcurrentDictionary<string, FeatureSettings> _settings;
private readonly ConcurrentDictionary<string, FeatureDefinition> _settings;
private IDisposable _changeSubscription;
private int _stale = 0;

public ConfigurationFeatureSettingsProvider(IConfiguration configuration)
public ConfigurationFeatureDefinitionProvider(IConfiguration configuration)
{
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
_settings = new ConcurrentDictionary<string, FeatureSettings>();
_settings = new ConcurrentDictionary<string, FeatureDefinition>();

_changeSubscription = ChangeToken.OnChange(
() => _configuration.GetReloadToken(),
Expand All @@ -40,7 +40,7 @@ public void Dispose()
_changeSubscription = null;
}

public Task<FeatureSettings> GetFeatureSettingsAsync(string featureName)
public Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName)
{
if (featureName == null)
{
Expand All @@ -54,7 +54,7 @@ public Task<FeatureSettings> GetFeatureSettingsAsync(string featureName)

//
// Query by feature name
FeatureSettings settings = _settings.GetOrAdd(featureName, (name) => ReadFeatureSettings(name));
FeatureDefinition settings = _settings.GetOrAdd(featureName, (name) => ReadFeatureSettings(name));

return Task.FromResult(settings);
}
Expand All @@ -63,7 +63,7 @@ public Task<FeatureSettings> GetFeatureSettingsAsync(string featureName)
// The async key word is necessary for creating IAsyncEnumerable.
// The need to disable this warning occurs when implementaing async stream synchronously.
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
public async IAsyncEnumerable<FeatureSettings> GetAllFeatureSettingsAsync()
public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
#pragma warning restore CS1998
{
if (Interlocked.Exchange(ref _stale, 0) != 0)
Expand All @@ -81,7 +81,7 @@ public async IAsyncEnumerable<FeatureSettings> GetAllFeatureSettingsAsync()
}
}

private FeatureSettings ReadFeatureSettings(string featureName)
private FeatureDefinition ReadFeatureSettings(string featureName)
jimmyca15 marked this conversation as resolved.
Show resolved Hide resolved
{
IConfigurationSection configuration = GetFeatureConfigurationSections()
.FirstOrDefault(section => section.Key.Equals(featureName, StringComparison.OrdinalIgnoreCase));
Expand All @@ -94,7 +94,7 @@ private FeatureSettings ReadFeatureSettings(string featureName)
return ReadFeatureSettings(configuration);
}

private FeatureSettings ReadFeatureSettings(IConfigurationSection configurationSection)
private FeatureDefinition ReadFeatureSettings(IConfigurationSection configurationSection)
{
/*

Expand Down Expand Up @@ -125,7 +125,7 @@ We support

*/

var enabledFor = new List<FeatureFilterSettings>();
var enabledFor = new List<FeatureFilterConfiguration>();

string val = configurationSection.Value; // configuration[$"{featureName}"];

Expand All @@ -142,7 +142,7 @@ We support
//myAlwaysEnabledFeature: {
// enabledFor: true
//}
enabledFor.Add(new FeatureFilterSettings
enabledFor.Add(new FeatureFilterConfiguration
{
Name = "AlwaysOn"
});
Expand All @@ -156,18 +156,18 @@ We support
//
// 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(FeatureFilterSettings.Name)]))
if (int.TryParse(section.Key, out int i) && !string.IsNullOrEmpty(section[nameof(FeatureFilterConfiguration.Name)]))
{
enabledFor.Add(new FeatureFilterSettings()
enabledFor.Add(new FeatureFilterConfiguration()
{
Name = section[nameof(FeatureFilterSettings.Name)],
Parameters = section.GetSection(nameof(FeatureFilterSettings.Parameters))
Name = section[nameof(FeatureFilterConfiguration.Name)],
Parameters = section.GetSection(nameof(FeatureFilterConfiguration.Parameters))
});
}
}
}

return new FeatureSettings()
return new FeatureDefinition()
{
Name = configurationSection.Key,
EnabledFor = enabledFor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@
namespace Microsoft.FeatureManagement
{
/// <summary>
/// The settings for a feature.
/// The definition of a feature.
/// </summary>
class FeatureSettings
public class FeatureDefinition
{
/// <summary>
/// The name of the feature.
/// </summary>
public string Name { get; set; }

/// <summary>
/// The criteria that the feature can be enabled for.
/// The feature filters that the feature can be enabled for.
/// </summary>
public IEnumerable<FeatureFilterSettings> EnabledFor { get; set; } = new List<FeatureFilterSettings>();
public IEnumerable<FeatureFilterConfiguration> EnabledFor { get; set; } = new List<FeatureFilterConfiguration>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
namespace Microsoft.FeatureManagement
{
/// <summary>
/// The settings that define a feature filter.
/// The configuration of a feature filter.
/// </summary>
class FeatureFilterSettings
public class FeatureFilterConfiguration
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if it will be better if we call this FeatureFilterDefinition.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I left it as Configuration rather than definition is that there is only one definition for a feature. However the same feature filter might appear in multiple features. They would just be configured differently. So to me, this was the configuration of a feature filter rather than a definition of one.

{
/// <summary>
/// The name of the feature filer.
/// The name of the feature filter.
/// </summary>
public string Name { get; set; }

Expand Down
10 changes: 5 additions & 5 deletions src/Microsoft.FeatureManagement/FeatureManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace Microsoft.FeatureManagement
/// </summary>
class FeatureManager : IFeatureManager
{
private readonly IFeatureSettingsProvider _settingsProvider;
private readonly IFeatureDefinitionProvider _settingsProvider;
zhenlan marked this conversation as resolved.
Show resolved Hide resolved
private readonly IEnumerable<IFeatureFilterMetadata> _featureFilters;
private readonly IEnumerable<ISessionManager> _sessionManagers;
private readonly ILogger _logger;
Expand All @@ -25,7 +25,7 @@ class FeatureManager : IFeatureManager
private readonly FeatureManagementOptions _options;

public FeatureManager(
IFeatureSettingsProvider settingsProvider,
IFeatureDefinitionProvider settingsProvider,
zhenlan marked this conversation as resolved.
Show resolved Hide resolved
IEnumerable<IFeatureFilterMetadata> featureFilters,
IEnumerable<ISessionManager> sessionManagers,
ILoggerFactory loggerFactory,
Expand All @@ -52,7 +52,7 @@ public Task<bool> IsEnabledAsync<TContext>(string feature, TContext appContext)

public async IAsyncEnumerable<string> GetFeatureNamesAsync()
{
await foreach (FeatureSettings featureSettings in _settingsProvider.GetAllFeatureSettingsAsync().ConfigureAwait(false))
await foreach (FeatureDefinition featureSettings in _settingsProvider.GetAllFeatureDefinitionsAsync().ConfigureAwait(false))
{
yield return featureSettings.Name;
}
Expand All @@ -72,7 +72,7 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo

bool enabled = false;

FeatureSettings settings = await _settingsProvider.GetFeatureSettingsAsync(feature).ConfigureAwait(false);
FeatureDefinition settings = await _settingsProvider.GetFeatureDefinitionAsync(feature).ConfigureAwait(false);
zhenlan marked this conversation as resolved.
Show resolved Hide resolved

if (settings != null)
{
Expand All @@ -90,7 +90,7 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo
// For all enabling filters listed in the feature's state calculate if they return true
// If any executed filters return true, return true

foreach (FeatureFilterSettings featureFilterSettings in settings.EnabledFor)
foreach (FeatureFilterConfiguration featureFilterSettings in settings.EnabledFor)
zhenlan marked this conversation as resolved.
Show resolved Hide resolved
{
IFeatureFilterMetadata filter = GetFeatureFilterMetadata(featureFilterSettings.Name);

Expand Down
27 changes: 27 additions & 0 deletions src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Microsoft.FeatureManagement
{
/// <summary>
/// A provider of feature definitions.
/// </summary>
public interface IFeatureDefinitionProvider
{
/// <summary>
/// Retrieves the definition for a given feature.
/// </summary>
/// <param name="featureName">The name of the feature to retrieve the definition for.</param>
/// <returns>The feature's definition.</returns>
Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName);

/// <summary>
/// Retrieves definitions for all features.
/// </summary>
/// <returns>An enumerator which provides asynchronous iteration over feature definitions.</returns>
IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync();
}
}
27 changes: 0 additions & 27 deletions src/Microsoft.FeatureManagement/IFeatureSettingsProvider.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.FeatureManagement.FeatureFilters;
using System;

namespace Microsoft.FeatureManagement
Expand All @@ -25,7 +24,7 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec

//
// Add required services
services.TryAddSingleton<IFeatureSettingsProvider, ConfigurationFeatureSettingsProvider>();
services.TryAddSingleton<IFeatureDefinitionProvider, ConfigurationFeatureDefinitionProvider>();

services.AddSingleton<IFeatureManager, FeatureManager>();

Expand All @@ -49,7 +48,7 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec
throw new ArgumentNullException(nameof(configuration));
}

services.AddSingleton<IFeatureSettingsProvider>(new ConfigurationFeatureSettingsProvider(configuration));
services.AddSingleton<IFeatureDefinitionProvider>(new ConfigurationFeatureDefinitionProvider(configuration));

return services.AddFeatureManagement();
}
Expand Down
63 changes: 61 additions & 2 deletions tests/Tests.FeatureManagement/FeatureManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public async Task Integrates()

services.AddMvcCore(o =>
{

o.EnableEndpointRouting = false;
o.Filters.AddForFeature<MvcFilter>(ConditionalFeature);
});
})
Expand Down Expand Up @@ -132,7 +132,7 @@ public async Task GatesFeatures()
.AddFeatureManagement()
.AddFeatureFilter<TestFilter>();

services.AddMvcCore();
services.AddMvcCore(o => o.EnableEndpointRouting = false);
})
.Configure(app => app.UseMvc()));

Expand Down Expand Up @@ -483,5 +483,64 @@ public async Task SwallowsExceptionForMissingFeatureFilter()

Assert.False(isEnabled);
}

[Fact]
public async Task CustomSettingsProvider()
jimmyca15 marked this conversation as resolved.
Show resolved Hide resolved
{
FeatureDefinition testFeature = new FeatureDefinition
{
Name = ConditionalFeature,
EnabledFor = new List<FeatureFilterConfiguration>()
{
new FeatureFilterConfiguration
{
Name = "Test",
Parameters = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string>()
{
{ "P1", "V1" },
}).Build()
}
}
};

var services = new ServiceCollection();

services
.Configure<FeatureManagementOptions>(options =>
{
options.IgnoreMissingFeatureFilters = true;
jimmyca15 marked this conversation as resolved.
Show resolved Hide resolved
});

services.AddSingleton<IFeatureDefinitionProvider>(new InMemoryFeatureSettingsProvider(new FeatureDefinition[] { testFeature }))
.AddFeatureManagement()
.AddFeatureFilter<TestFilter>();

ServiceProvider serviceProvider = services.BuildServiceProvider();

IFeatureManager featureManager = serviceProvider.GetRequiredService<IFeatureManager>();

IEnumerable<IFeatureFilterMetadata> featureFilters = serviceProvider.GetRequiredService<IEnumerable<IFeatureFilterMetadata>>();

//
// Sync filter
TestFilter testFeatureFilter = (TestFilter)featureFilters.First(f => f is TestFilter);

bool called = false;

testFeatureFilter.Callback = (evaluationContext) =>
{
called = true;

Assert.Equal("V1", evaluationContext.Parameters["P1"]);

Assert.Equal(ConditionalFeature, evaluationContext.FeatureName);

return true;
};

await featureManager.IsEnabledAsync(ConditionalFeature);

Assert.True(called);
}
}
}
Loading