Skip to content

Commit

Permalink
Added support for custom feature providers. (#79)
Browse files Browse the repository at this point in the history
* Added support for custom feature providers.

New public API
public IFeatureDefinitionProvider;
public FeatureDefinition;
public FeatureFilterConfiguration;

* Fix tests broken from master rebase.

* Updated type names to be consistent with the newly renamed IFeatureDefinitionProvider.

* Updated names for consistency.
  • Loading branch information
jimmyca15 authored Jul 9, 2020
1 parent 49f6588 commit fddc98b
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 79 deletions.
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()
```

# 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
{
/// <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

0 comments on commit fddc98b

Please sign in to comment.