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 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
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 `InMemoryFeatureDefinitionProvider`.

```
services.AddSingleton<IFeatureDefinitionProvider, InMemoryFeatureDefinitionProvider>()
.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 feature definitions 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> _definitions;
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>();
_definitions = 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 @@ -49,52 +49,52 @@ public Task<FeatureSettings> GetFeatureSettingsAsync(string featureName)

if (Interlocked.Exchange(ref _stale, 0) != 0)
{
_settings.Clear();
_definitions.Clear();
}

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

return Task.FromResult(settings);
return Task.FromResult(definition);
}

//
// 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)
{
_settings.Clear();
_definitions.Clear();
}

//
// Iterate over all features registered in the system at initial invocation time
foreach (IConfigurationSection featureSection in GetFeatureConfigurationSections())
foreach (IConfigurationSection featureSection in GetFeatureDefinitionSections())
{
//
// Underlying IConfigurationSection data is dynamic so latest feature settings are returned
yield return _settings.GetOrAdd(featureSection.Key, (_) => ReadFeatureSettings(featureSection));
// Underlying IConfigurationSection data is dynamic so latest feature definitions are returned
yield return _definitions.GetOrAdd(featureSection.Key, (_) => ReadFeatureDefinition(featureSection));
}
}

private FeatureSettings ReadFeatureSettings(string featureName)
private FeatureDefinition ReadFeatureDefinition(string featureName)
{
IConfigurationSection configuration = GetFeatureConfigurationSections()
IConfigurationSection configuration = GetFeatureDefinitionSections()
.FirstOrDefault(section => section.Key.Equals(featureName, StringComparison.OrdinalIgnoreCase));

if (configuration == null)
{
return null;
}

return ReadFeatureSettings(configuration);
return ReadFeatureDefinition(configuration);
}

private FeatureSettings ReadFeatureSettings(IConfigurationSection configurationSection)
private FeatureDefinition ReadFeatureDefinition(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,32 +156,32 @@ 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
};
}

private IEnumerable<IConfigurationSection> GetFeatureConfigurationSections()
private IEnumerable<IConfigurationSection> GetFeatureDefinitionSections()
{
const string FeatureManagementSectionName = "FeatureManagement";

if (_configuration.GetChildren().Any(s => s.Key.Equals(FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)))
{
//
// Look for settings under the "FeatureManagement" section
// Look for feature definitions under the "FeatureManagement" section
return _configuration.GetSection(FeatureManagementSectionName).GetChildren();
}
else
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
26 changes: 13 additions & 13 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 _featureDefinitionProvider;
private readonly IEnumerable<IFeatureFilterMetadata> _featureFilters;
private readonly IEnumerable<ISessionManager> _sessionManagers;
private readonly ILogger _logger;
Expand All @@ -25,13 +25,13 @@ class FeatureManager : IFeatureManager
private readonly FeatureManagementOptions _options;

public FeatureManager(
IFeatureSettingsProvider settingsProvider,
IFeatureDefinitionProvider featureDefinitionProvider,
IEnumerable<IFeatureFilterMetadata> featureFilters,
IEnumerable<ISessionManager> sessionManagers,
ILoggerFactory loggerFactory,
IOptions<FeatureManagementOptions> options)
{
_settingsProvider = settingsProvider;
_featureDefinitionProvider = featureDefinitionProvider;
_featureFilters = featureFilters ?? throw new ArgumentNullException(nameof(featureFilters));
_sessionManagers = sessionManagers ?? throw new ArgumentNullException(nameof(sessionManagers));
_logger = loggerFactory.CreateLogger<FeatureManager>();
Expand All @@ -52,9 +52,9 @@ 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 featureDefintion in _featureDefinitionProvider.GetAllFeatureDefinitionsAsync().ConfigureAwait(false))
{
yield return featureSettings.Name;
yield return featureDefintion.Name;
}
}

Expand All @@ -72,15 +72,15 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo

bool enabled = false;

FeatureSettings settings = await _settingsProvider.GetFeatureSettingsAsync(feature).ConfigureAwait(false);
FeatureDefinition featureDefinition = await _featureDefinitionProvider.GetFeatureDefinitionAsync(feature).ConfigureAwait(false);

if (settings != null)
if (featureDefinition != null)
{
//
// Check if feature is always on
// If it is, result is true, goto: cache

if (settings.EnabledFor.Any(featureFilter => string.Equals(featureFilter.Name, "AlwaysOn", StringComparison.OrdinalIgnoreCase)))
if (featureDefinition.EnabledFor.Any(featureFilter => string.Equals(featureFilter.Name, "AlwaysOn", StringComparison.OrdinalIgnoreCase)))
{
enabled = true;
}
Expand All @@ -90,13 +90,13 @@ 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 featureFilterConfiguration in featureDefinition.EnabledFor)
{
IFeatureFilterMetadata filter = GetFeatureFilterMetadata(featureFilterSettings.Name);
IFeatureFilterMetadata filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name);

if (filter == null)
{
string errorMessage = $"The feature filter '{featureFilterSettings.Name}' specified for feature '{feature}' was not found.";
string errorMessage = $"The feature filter '{featureFilterConfiguration.Name}' specified for feature '{feature}' was not found.";

if (!_options.IgnoreMissingFeatureFilters)
{
Expand All @@ -113,14 +113,14 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo
var context = new FeatureFilterEvaluationContext()
{
FeatureName = feature,
Parameters = featureFilterSettings.Parameters
Parameters = featureFilterConfiguration.Parameters
};

//
// IContextualFeatureFilter
if (useAppContext)
{
ContextualFeatureFilterEvaluator contextualFilter = GetContextualFeatureFilter(featureFilterSettings.Name, typeof(TContext));
ContextualFeatureFilterEvaluator contextualFilter = GetContextualFeatureFilter(featureFilterConfiguration.Name, typeof(TContext));

if (contextualFilter != null && await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false))
{
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
Loading