From c19e45741e175d9238084fd09f9ac5c62cdb298a Mon Sep 17 00:00:00 2001 From: Jimmy Campbell Date: Tue, 14 Dec 2021 11:00:37 -0800 Subject: [PATCH 1/4] Update to dynamic features. * Update types to respect dynamic feature name. E.g. IFeatureManager -> IDynamicFeatureManager. * Updated property/method summaries to distinguish dynamic features and feature flags. * Added IDynamicFeatureManagerSnapshot * Separate feature flags and dynamic features in configuration schema * Backward compatability support for old configuration schema. * Breaking: Update custom feature definition provider to return feature flags and dynamic features. * Added DynamicFeatureDefinition class * renamed FeatureDefinition to FeatureFlagDefinition. * Breaking: IFeatureManager.GetFeatureNamesAsync renamed to IFeatureManager.GetFeatureFlagNamesAsync * Corrected namespace for built-in feature variant assigners. * Microsoft.FeatureManagement -> Microsoft.FeatureManagement.Assigners * Added test for custom feature definition provider. * Added test for backcompat support for v1 configuration schema. --- .../CustomAssignmentConsoleApp/Program.cs | 2 +- .../RecurringAssigner.cs | 2 +- examples/FeatureFlagDemo/Startup.cs | 1 + .../Views/Shared/_Layout.cshtml | 2 +- examples/FeatureFlagDemo/appsettings.json | 170 ++++---- examples/TargetingConsoleApp/Program.cs | 3 +- .../ConfigurationFeatureDefinitionProvider.cs | 172 +++++--- ...figurationFeatureVariantOptionsResolver.cs | 2 +- ...inition.cs => DynamicFeatureDefinition.cs} | 13 +- .../FeatureFlagDefinition.cs | 24 ++ .../FeatureManager.cs | 21 +- .../FeatureManagerSnapshot.cs | 83 +++- .../FeatureVariantAssignmentContext.cs | 4 +- .../IContextualFeatureVariantAssigner.cs | 10 +- .../IDynamicFeatureManager.cs | 42 ++ .../IDynamicFeatureManagerSnapshot.cs | 12 + .../IFeatureDefinitionProvider.cs | 29 +- .../IFeatureManager.cs | 22 +- .../IFeatureVariantAssigner.cs | 8 +- .../IFeatureVariantAssignerMetadata.cs | 2 +- .../IFeatureVariantManager.cs | 34 -- .../IFeatureVariantOptionsResolver.cs | 10 +- .../ServiceCollectionExtensions.cs | 2 +- ...ntextualTargetingFeatureVariantAssigner.cs | 4 +- .../TargetingFeatureVariantAssigner.cs | 2 +- .../FeatureManagement.cs | 166 +++++++- .../InMemoryFeatureDefinitionProvider.cs | 33 +- .../Tests.FeatureManagement.csproj | 5 + .../Tests.FeatureManagement/appsettings.json | 371 +++++++++--------- .../appsettings.v1.json | 16 + 30 files changed, 828 insertions(+), 439 deletions(-) rename src/Microsoft.FeatureManagement/{FeatureDefinition.cs => DynamicFeatureDefinition.cs} (61%) create mode 100644 src/Microsoft.FeatureManagement/FeatureFlagDefinition.cs create mode 100644 src/Microsoft.FeatureManagement/IDynamicFeatureManager.cs create mode 100644 src/Microsoft.FeatureManagement/IDynamicFeatureManagerSnapshot.cs delete mode 100644 src/Microsoft.FeatureManagement/IFeatureVariantManager.cs create mode 100644 tests/Tests.FeatureManagement/appsettings.v1.json diff --git a/examples/CustomAssignmentConsoleApp/Program.cs b/examples/CustomAssignmentConsoleApp/Program.cs index c1f91629..feacf45b 100644 --- a/examples/CustomAssignmentConsoleApp/Program.cs +++ b/examples/CustomAssignmentConsoleApp/Program.cs @@ -35,7 +35,7 @@ public static async Task Main(string[] args) // Get the feature manager from application services using (ServiceProvider serviceProvider = services.BuildServiceProvider()) { - IFeatureVariantManager variantManager = serviceProvider.GetRequiredService(); + IDynamicFeatureManager variantManager = serviceProvider.GetRequiredService(); DailyDiscountOptions discountOptions = await variantManager .GetVariantAsync("DailyDiscount", CancellationToken.None); diff --git a/examples/CustomAssignmentConsoleApp/RecurringAssigner.cs b/examples/CustomAssignmentConsoleApp/RecurringAssigner.cs index 47ded1fc..3a6845df 100644 --- a/examples/CustomAssignmentConsoleApp/RecurringAssigner.cs +++ b/examples/CustomAssignmentConsoleApp/RecurringAssigner.cs @@ -15,7 +15,7 @@ class RecurringAssigner : IFeatureVariantAssigner { public ValueTask AssignVariantAsync(FeatureVariantAssignmentContext variantAssignmentContext, CancellationToken _) { - FeatureDefinition featureDefinition = variantAssignmentContext.FeatureDefinition; + DynamicFeatureDefinition featureDefinition = variantAssignmentContext.FeatureDefinition; FeatureVariant chosenVariant = null; diff --git a/examples/FeatureFlagDemo/Startup.cs b/examples/FeatureFlagDemo/Startup.cs index 573d1fe3..11adfb93 100644 --- a/examples/FeatureFlagDemo/Startup.cs +++ b/examples/FeatureFlagDemo/Startup.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.FeatureManagement; +using Microsoft.FeatureManagement.Assigners; using Microsoft.FeatureManagement.FeatureFilters; namespace FeatureFlagDemo diff --git a/examples/FeatureFlagDemo/Views/Shared/_Layout.cshtml b/examples/FeatureFlagDemo/Views/Shared/_Layout.cshtml index 516eb3ba..c4f49f22 100644 --- a/examples/FeatureFlagDemo/Views/Shared/_Layout.cshtml +++ b/examples/FeatureFlagDemo/Views/Shared/_Layout.cshtml @@ -1,5 +1,5 @@ @using Microsoft.FeatureManagement -@inject IFeatureVariantManager variantManager; +@inject IDynamicFeatureManager variantManager; @{ DiscountBannerOptions opts = await variantManager.GetVariantAsync("DiscountBanner", Context.RequestAborted); } diff --git a/examples/FeatureFlagDemo/appsettings.json b/examples/FeatureFlagDemo/appsettings.json index 919af4bf..7ae75e61 100644 --- a/examples/FeatureFlagDemo/appsettings.json +++ b/examples/FeatureFlagDemo/appsettings.json @@ -5,100 +5,102 @@ } }, "AllowedHosts": "*", - // Define feature flags in config file "FeatureManagement": { - - "Home": true, - "Beta": { - "EnabledFor": [ - { - "Name": "Targeting", - "Parameters": { // This json object maps to a strongly typed configuration class - "Audience": { - "Users": [ - "Jeff", - "Alicia" - ], - "Groups": [ - { - "Name": "Ring0", - "RolloutPercentage": 80 - }, - { - "Name": "Ring1", - "RolloutPercentage": 50 - } - ], - "DefaultRolloutPercentage": 20 + "FeatureFlags": { + "Home": true, + "Beta": { + "EnabledFor": [ + { + "Name": "Targeting", + "Parameters": { // This json object maps to a strongly typed configuration class + "Audience": { + "Users": [ + "Jeff", + "Alicia" + ], + "Groups": [ + { + "Name": "Ring0", + "RolloutPercentage": 80 + }, + { + "Name": "Ring1", + "RolloutPercentage": 50 + } + ], + "DefaultRolloutPercentage": 20 + } } } - } - ] - }, - "CustomViewData": { - "EnabledFor": [ - { - "Name": "Browser", - "Parameters": { - "AllowedBrowsers": [ "Chrome", "Edge" ] + ] + }, + "CustomViewData": { + "EnabledFor": [ + { + "Name": "Browser", + "Parameters": { + "AllowedBrowsers": [ "Chrome", "Edge" ] + } } - } - ] - }, - "ContentEnhancement": { - "EnabledFor": [ - { - "Name": "TimeWindow", - "Parameters": { - "Start": "Wed, 01 May 2019 13:59:59 GMT", - "End": "Mon, 01 July 2019 00:00:00 GMT" + ] + }, + "ContentEnhancement": { + "EnabledFor": [ + { + "Name": "TimeWindow", + "Parameters": { + "Start": "Wed, 01 May 2019 13:59:59 GMT", + "End": "Mon, 01 July 2019 00:00:00 GMT" + } } - } - ] - }, - "EnhancedPipeline": { - "EnabledFor": [ - { - "Name": "Microsoft.Percentage", - "Parameters": { - "Value": 50 + ] + }, + "EnhancedPipeline": { + "EnabledFor": [ + { + "Name": "Microsoft.Percentage", + "Parameters": { + "Value": 50 + } } - } - ] + ] + } }, - "DiscountBanner": { - "Assigner": "Targeting", - "Variants": [ - { - "Default": true, - "Name": "Big", - "ConfigurationReference": "DiscountBanner:Big" - }, - { - "Name": "Small", - "ConfigurationReference": "DiscountBanner:Small", - "AssignmentParameters": { - "Audience": { - "Users": [ - "Jeff", - "Alicia" - ], - "Groups": [ - { - "Name": "Ring0", - "RolloutPercentage": 80 - }, - { - "Name": "Ring1", - "RolloutPercentage": 50 - } - ], - "DefaultRolloutPercentage": 20 + "DynamicFeatures": { + "DiscountBanner": { + "Assigner": "Targeting", + "Variants": [ + { + "Default": true, + "Name": "Big", + "ConfigurationReference": "DiscountBanner:Big" + }, + { + "Name": "Small", + "ConfigurationReference": "DiscountBanner:Small", + "AssignmentParameters": { + "Audience": { + "Users": [ + "Jeff", + "Alicia" + ], + "Groups": [ + { + "Name": "Ring0", + "RolloutPercentage": 80 + }, + { + "Name": "Ring1", + "RolloutPercentage": 50 + } + ], + "DefaultRolloutPercentage": 20 + } } } - } - ] + ] + } } }, "DiscountBanner": { diff --git a/examples/TargetingConsoleApp/Program.cs b/examples/TargetingConsoleApp/Program.cs index 464cb417..1b25c4dc 100644 --- a/examples/TargetingConsoleApp/Program.cs +++ b/examples/TargetingConsoleApp/Program.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.FeatureManagement; +using Microsoft.FeatureManagement.Assigners; using Microsoft.FeatureManagement.FeatureFilters; using System; using System.Collections.Generic; @@ -40,7 +41,7 @@ public static async Task Main(string[] args) using (ServiceProvider serviceProvider = services.BuildServiceProvider()) { IFeatureManager featureManager = serviceProvider.GetRequiredService(); - IFeatureVariantManager variantManager = serviceProvider.GetRequiredService(); + IDynamicFeatureManager variantManager = serviceProvider.GetRequiredService(); // // We'll simulate a task to run on behalf of each known user diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 2c8fe03d..6fae88de 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; @@ -18,17 +19,22 @@ namespace Microsoft.FeatureManagement /// sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionProvider, IDisposable { + private const string FeatureManagementSectionName = "FeatureManagement"; + private const string FeatureFlagDefinitionsSectionName = "FeatureFlags"; + private const string DynamicFeatureDefinitionsSectionName= "DynamicFeatures"; private const string FeatureFiltersSectionName = "EnabledFor"; private const string FeatureVariantsSectionName = "Variants"; private readonly IConfiguration _configuration; - private readonly ConcurrentDictionary _definitions; + private readonly ConcurrentDictionary _featureFlagDefinitions; + private readonly ConcurrentDictionary _dynamicFeatureDefinitions; private IDisposable _changeSubscription; private int _stale = 0; public ConfigurationFeatureDefinitionProvider(IConfiguration configuration) { _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - _definitions = new ConcurrentDictionary(); + _featureFlagDefinitions = new ConcurrentDictionary(); + _dynamicFeatureDefinitions = new ConcurrentDictionary(); _changeSubscription = ChangeToken.OnChange( () => _configuration.GetReloadToken(), @@ -42,21 +48,18 @@ public void Dispose() _changeSubscription = null; } - public Task GetFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken) + public Task GetFeatureFlagDefinitionAsync(string featureName, CancellationToken cancellationToken) { if (featureName == null) { throw new ArgumentNullException(nameof(featureName)); } - if (Interlocked.Exchange(ref _stale, 0) != 0) - { - _definitions.Clear(); - } + EnsureFresh(); // // Query by feature name - FeatureDefinition definition = _definitions.GetOrAdd(featureName, (name) => ReadFeatureDefinition(name)); + FeatureFlagDefinition definition = _featureFlagDefinitions.GetOrAdd(featureName, (name) => ReadFeatureFlagDefinition(name)); return Task.FromResult(definition); } @@ -65,27 +68,59 @@ public Task GetFeatureDefinitionAsync(string featureName, Can // 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 GetAllFeatureDefinitionsAsync([EnumeratorCancellation] CancellationToken cancellationToken) + public async IAsyncEnumerable GetAllFeatureFlagDefinitionsAsync([EnumeratorCancellation] CancellationToken cancellationToken) #pragma warning restore CS1998 { - if (Interlocked.Exchange(ref _stale, 0) != 0) + EnsureFresh(); + + // + // Iterate over all features registered in the system at initial invocation time + foreach (IConfigurationSection featureSection in GetFeatureFlagDefinitionSections()) { - _definitions.Clear(); + // + // Underlying IConfigurationSection data is dynamic so latest feature definitions are returned + yield return _featureFlagDefinitions.GetOrAdd(featureSection.Key, (_) => ReadFeatureFlagDefinition(featureSection)); } + } + + public Task GetDynamicFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken = default) + { + if (featureName == null) + { + throw new ArgumentNullException(nameof(featureName)); + } + + EnsureFresh(); + + // + // Query by feature name + DynamicFeatureDefinition definition = _dynamicFeatureDefinitions.GetOrAdd(featureName, (name) => ReadDynamicFeatureDefinition(name)); + + 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 GetAllDynamicFeatureDefinitionsAsync([EnumeratorCancellation] CancellationToken cancellationToken) +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + EnsureFresh(); // // Iterate over all features registered in the system at initial invocation time - foreach (IConfigurationSection featureSection in GetFeatureDefinitionSections()) + foreach (IConfigurationSection featureSection in GetDynamicFeatureDefinitionSections()) { // // Underlying IConfigurationSection data is dynamic so latest feature definitions are returned - yield return _definitions.GetOrAdd(featureSection.Key, (_) => ReadFeatureDefinition(featureSection)); + yield return _dynamicFeatureDefinitions.GetOrAdd(featureSection.Key, (_) => ReadDynamicFeatureDefinition(featureSection)); } } - private FeatureDefinition ReadFeatureDefinition(string featureName) + private FeatureFlagDefinition ReadFeatureFlagDefinition(string featureName) { - IConfigurationSection configuration = GetFeatureDefinitionSections() + IConfigurationSection configuration = GetFeatureFlagDefinitionSections() .FirstOrDefault(section => section.Key.Equals(featureName, StringComparison.OrdinalIgnoreCase)); if (configuration == null) @@ -93,10 +128,23 @@ private FeatureDefinition ReadFeatureDefinition(string featureName) return null; } - return ReadFeatureDefinition(configuration); + return ReadFeatureFlagDefinition(configuration); } - private FeatureDefinition ReadFeatureDefinition(IConfigurationSection configurationSection) + private DynamicFeatureDefinition ReadDynamicFeatureDefinition(string featureName) + { + IConfigurationSection configuration = GetDynamicFeatureDefinitionSections() + .FirstOrDefault(section => section.Key.Equals(featureName, StringComparison.OrdinalIgnoreCase)); + + if (configuration == null) + { + return null; + } + + return ReadDynamicFeatureDefinition(configuration); + } + + private FeatureFlagDefinition ReadFeatureFlagDefinition(IConfigurationSection configurationSection) { /* @@ -127,11 +175,9 @@ We support */ - var enabledFor = new List(); + Debug.Assert(configurationSection != null); - var variants = new List(); - - string assigner = null; + var enabledFor = new List(); string val = configurationSection.Value; // configuration[$"{featureName}"]; @@ -162,7 +208,7 @@ 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(FeatureFilterConfiguration.Name)])) + if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[nameof(FeatureFilterConfiguration.Name)])) { enabledFor.Add(new FeatureFilterConfiguration { @@ -171,48 +217,78 @@ We support }); } } + } - IEnumerable variantSections = configurationSection.GetSection(FeatureVariantsSectionName).GetChildren(); + return new FeatureFlagDefinition() + { + Name = configurationSection.Key, + EnabledFor = enabledFor, + }; + } + + private DynamicFeatureDefinition ReadDynamicFeatureDefinition(IConfigurationSection configurationSection) + { + Debug.Assert(configurationSection != null); + + var variants = new List(); - foreach (IConfigurationSection section in variantSections) + foreach (IConfigurationSection section in configurationSection.GetSection(FeatureVariantsSectionName).GetChildren()) + { + if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[nameof(FeatureVariant.Name)])) { - if (int.TryParse(section.Key, out int i) && !string.IsNullOrEmpty(section[nameof(FeatureVariant.Name)])) + variants.Add(new FeatureVariant { - variants.Add(new FeatureVariant - { - Default = section.GetValue(nameof(FeatureVariant.Default)), - Name = section.GetValue(nameof(FeatureVariant.Name)), - ConfigurationReference = section.GetValue(nameof(FeatureVariant.ConfigurationReference)), - AssignmentParameters = section.GetSection(nameof(FeatureVariant.AssignmentParameters)) - }); - } + Default = section.GetValue(nameof(FeatureVariant.Default)), + Name = section.GetValue(nameof(FeatureVariant.Name)), + ConfigurationReference = section.GetValue(nameof(FeatureVariant.ConfigurationReference)), + AssignmentParameters = section.GetSection(nameof(FeatureVariant.AssignmentParameters)) + }); } - - assigner = configurationSection.GetValue(nameof(FeatureDefinition.Assigner)); } - return new FeatureDefinition() + return new DynamicFeatureDefinition() { Name = configurationSection.Key, - EnabledFor = enabledFor, Variants = variants, - Assigner = assigner + Assigner = configurationSection.GetValue(nameof(DynamicFeatureDefinition.Assigner)) }; } - private IEnumerable GetFeatureDefinitionSections() + private IEnumerable GetFeatureFlagDefinitionSections() { - const string FeatureManagementSectionName = "FeatureManagement"; + IEnumerable featureManagementChildren = GetFeatureManagementSection().GetChildren(); - if (_configuration.GetChildren().Any(s => s.Key.Equals(FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase))) - { - // - // Look for feature definitions under the "FeatureManagement" section - return _configuration.GetSection(FeatureManagementSectionName).GetChildren(); - } - else + IConfigurationSection featureFlagsSection = featureManagementChildren.FirstOrDefault(s => s.Key == FeatureFlagDefinitionsSectionName); + + // + // Support backward compatability where feature flag definitions were directly under the feature management section + return featureFlagsSection == null ? + featureManagementChildren : + featureFlagsSection.GetChildren(); + } + + private IEnumerable GetDynamicFeatureDefinitionSections() + { + return GetFeatureManagementSection() + .GetSection(DynamicFeatureDefinitionsSectionName) + .GetChildren(); + } + + private IConfiguration GetFeatureManagementSection() + { + // + // Look for feature definitions under the "FeatureManagement" section + return _configuration.GetChildren().Any(s => s.Key.Equals(FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)) ? + _configuration.GetSection(FeatureManagementSectionName) : + _configuration; + } + + private void EnsureFresh() + { + if (Interlocked.Exchange(ref _stale, 0) != 0) { - return _configuration.GetChildren(); + _featureFlagDefinitions.Clear(); + _dynamicFeatureDefinitions.Clear(); } } } diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs index dcb32a0e..c5868056 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs @@ -20,7 +20,7 @@ public ConfigurationFeatureVariantOptionsResolver(IConfiguration configuration) _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); } - public ValueTask GetOptionsAsync(FeatureDefinition featureDefinition, FeatureVariant variant, CancellationToken cancellationToken) + public ValueTask GetOptionsAsync(DynamicFeatureDefinition featureDefinition, FeatureVariant variant, CancellationToken cancellationToken) { if (variant == null) { diff --git a/src/Microsoft.FeatureManagement/FeatureDefinition.cs b/src/Microsoft.FeatureManagement/DynamicFeatureDefinition.cs similarity index 61% rename from src/Microsoft.FeatureManagement/FeatureDefinition.cs rename to src/Microsoft.FeatureManagement/DynamicFeatureDefinition.cs index 19287df6..8dfb95cc 100644 --- a/src/Microsoft.FeatureManagement/FeatureDefinition.cs +++ b/src/Microsoft.FeatureManagement/DynamicFeatureDefinition.cs @@ -7,27 +7,22 @@ namespace Microsoft.FeatureManagement { /// - /// The definition of a feature. + /// The definition of a dynamic feature. /// - public class FeatureDefinition + public class DynamicFeatureDefinition { /// - /// The name of the feature. + /// The name of the dynamic feature. /// public string Name { get; set; } - /// - /// The feature filters that the feature can be enabled for. - /// - public IEnumerable EnabledFor { get; set; } = Enumerable.Empty(); - /// /// The assigner used to pick the variant that should be used when a variant is requested /// public string Assigner { get; set; } /// - /// The feature variants listed for this feature. + /// The feature variants listed for this dynamic feature. /// public IEnumerable Variants { get; set; } = Enumerable.Empty(); } diff --git a/src/Microsoft.FeatureManagement/FeatureFlagDefinition.cs b/src/Microsoft.FeatureManagement/FeatureFlagDefinition.cs new file mode 100644 index 00000000..4cce2194 --- /dev/null +++ b/src/Microsoft.FeatureManagement/FeatureFlagDefinition.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.FeatureManagement +{ + /// + /// The definition of a feature flag. + /// + public class FeatureFlagDefinition + { + /// + /// The name of the feature flag. + /// + public string Name { get; set; } + + /// + /// The feature filters that the feature flag can be enabled for. + /// + public IEnumerable EnabledFor { get; set; } = Enumerable.Empty(); + } +} diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 60c9855a..5edabf55 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; @@ -17,7 +16,7 @@ namespace Microsoft.FeatureManagement /// /// Used to evaluate whether a feature is enabled or disabled. /// - class FeatureManager : IFeatureManager, IFeatureVariantManager + class FeatureManager : IFeatureManager, IDynamicFeatureManager { private readonly IFeatureDefinitionProvider _featureDefinitionProvider; private readonly IEnumerable _featureFilters; @@ -63,9 +62,17 @@ public Task IsEnabledAsync(string feature, TContext appContext, return IsEnabledAsync(feature, appContext, true, cancellationToken); } - public async IAsyncEnumerable GetFeatureNamesAsync([EnumeratorCancellation] CancellationToken cancellationToken) + public async IAsyncEnumerable GetFeatureFlagNamesAsync([EnumeratorCancellation] CancellationToken cancellationToken) { - await foreach (FeatureDefinition featureDefintion in _featureDefinitionProvider.GetAllFeatureDefinitionsAsync(cancellationToken).ConfigureAwait(false)) + await foreach (FeatureFlagDefinition featureDefintion in _featureDefinitionProvider.GetAllFeatureFlagDefinitionsAsync(cancellationToken).ConfigureAwait(false)) + { + yield return featureDefintion.Name; + } + } + + public async IAsyncEnumerable GetDynamicFeatureNamesAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (DynamicFeatureDefinition featureDefintion in _featureDefinitionProvider.GetAllDynamicFeatureDefinitionsAsync(cancellationToken).ConfigureAwait(false)) { yield return featureDefintion.Name; } @@ -90,8 +97,8 @@ private async ValueTask GetVariantAsync(string feature, TContext FeatureVariant variant = null; - FeatureDefinition featureDefinition = await _featureDefinitionProvider - .GetFeatureDefinitionAsync(feature, cancellationToken) + DynamicFeatureDefinition featureDefinition = await _featureDefinitionProvider + .GetDynamicFeatureDefinitionAsync(feature, cancellationToken) .ConfigureAwait(false); if (featureDefinition == null) @@ -206,7 +213,7 @@ private async Task IsEnabledAsync(string feature, TContext appCo bool enabled = false; - FeatureDefinition featureDefinition = await _featureDefinitionProvider.GetFeatureDefinitionAsync(feature, cancellationToken).ConfigureAwait(false); + FeatureFlagDefinition featureDefinition = await _featureDefinitionProvider.GetFeatureFlagDefinitionAsync(feature, cancellationToken).ConfigureAwait(false); if (featureDefinition != null) { diff --git a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs index 46549f4d..b39b5b44 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs @@ -12,37 +12,99 @@ namespace Microsoft.FeatureManagement /// /// Provides a snapshot of feature state to ensure consistency across a given request. /// - class FeatureManagerSnapshot : IFeatureManagerSnapshot + class FeatureManagerSnapshot : IFeatureManagerSnapshot, IDynamicFeatureManagerSnapshot { private readonly IFeatureManager _featureManager; + private readonly IDynamicFeatureManager _dynamicFeatureManager; private readonly IDictionary _flagCache = new Dictionary(); - private IEnumerable _featureNames; + private readonly IDictionary _variantCache = new Dictionary(); + private IEnumerable _featureFlagNames; + private IEnumerable _dynamicFeatureNames; - public FeatureManagerSnapshot(IFeatureManager featureManager) + public FeatureManagerSnapshot( + IFeatureManager featureManager, + IDynamicFeatureManager dynamicFeatureManager) { _featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager)); + _dynamicFeatureManager = dynamicFeatureManager ?? throw new ArgumentNullException(nameof(dynamicFeatureManager)); } - public async IAsyncEnumerable GetFeatureNamesAsync([EnumeratorCancellation]CancellationToken cancellationToken) + public async IAsyncEnumerable GetFeatureFlagNamesAsync([EnumeratorCancellation]CancellationToken cancellationToken) { - if (_featureNames == null) + if (_featureFlagNames == null) { var featureNames = new List(); - await foreach (string featureName in _featureManager.GetFeatureNamesAsync(cancellationToken).ConfigureAwait(false)) + await foreach (string featureName in _featureManager.GetFeatureFlagNamesAsync(cancellationToken).ConfigureAwait(false)) { featureNames.Add(featureName); } - _featureNames = featureNames; + _featureFlagNames = featureNames; } - foreach (string featureName in _featureNames) + foreach (string featureName in _featureFlagNames) { yield return featureName; } } + public async IAsyncEnumerable GetDynamicFeatureNamesAsync([EnumeratorCancellation]CancellationToken cancellationToken = default) + { + if (_dynamicFeatureNames == null) + { + var dynamicFeatureNames = new List(); + + await foreach (string featureName in _featureManager.GetFeatureFlagNamesAsync(cancellationToken).ConfigureAwait(false)) + { + dynamicFeatureNames.Add(featureName); + } + + _dynamicFeatureNames = dynamicFeatureNames; + } + + foreach (string featureName in _dynamicFeatureNames) + { + yield return featureName; + } + } + + public async ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken) + { + string cacheKey = GetVariantCacheKey(feature); + + // + // First, check local cache + if (_flagCache.ContainsKey(feature)) + { + return (T)_variantCache[cacheKey]; + } + + T variant = await _dynamicFeatureManager.GetVariantAsync(feature, cancellationToken).ConfigureAwait(false); + + _variantCache[cacheKey] = variant; + + return variant; + } + + public async ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken) + { + string cacheKey = GetVariantCacheKey(feature); + + // + // First, check local cache + if (_flagCache.ContainsKey(feature)) + { + return (T)_variantCache[cacheKey]; + } + + T variant = await _dynamicFeatureManager.GetVariantAsync(feature, cancellationToken).ConfigureAwait(false); + + _variantCache[cacheKey] = variant; + + return variant; + } + public async Task IsEnabledAsync(string feature, CancellationToken cancellationToken) { // @@ -74,5 +136,10 @@ public async Task IsEnabledAsync(string feature, TContext contex return enabled; } + + private string GetVariantCacheKey(string feature) + { + return $"{typeof(T).FullName}\n{feature}"; + } } } diff --git a/src/Microsoft.FeatureManagement/FeatureVariantAssignmentContext.cs b/src/Microsoft.FeatureManagement/FeatureVariantAssignmentContext.cs index 265a6ce1..0620288b 100644 --- a/src/Microsoft.FeatureManagement/FeatureVariantAssignmentContext.cs +++ b/src/Microsoft.FeatureManagement/FeatureVariantAssignmentContext.cs @@ -9,8 +9,8 @@ namespace Microsoft.FeatureManagement public class FeatureVariantAssignmentContext { /// - /// The definition of the feature in need of an assigned variant + /// The definition of the dynamic feature in need of an assigned variant /// - public FeatureDefinition FeatureDefinition { get; set; } + public DynamicFeatureDefinition FeatureDefinition { get; set; } } } diff --git a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAssigner.cs b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAssigner.cs index 173def4f..26f625ba 100644 --- a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAssigner.cs +++ b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAssigner.cs @@ -7,18 +7,18 @@ namespace Microsoft.FeatureManagement { /// - /// Provides a method to assign a variant of a feature to be used based off of custom conditions. + /// Provides a method to assign a variant of a dynamic feature to be used based off of custom conditions. /// /// A custom type that the assigner requires to perform assignment public interface IContextualFeatureVariantAssigner : IFeatureVariantAssignerMetadata { /// - /// Assign a variant of a feature to be used based off of customized criteria. + /// Assign a variant of a dynamic feature to be used based off of customized criteria. /// - /// A variant assignment context that contains information needed to assign a variant for a feature. - /// A context defined by the application that is passed in to the feature management system to provide contextual information for assigning a variant of a feature. + /// A variant assignment context that contains information needed to assign a variant for a dynamic feature. + /// A context defined by the application that is passed in to the feature management system to provide contextual information for assigning a variant of a dynamic feature. /// The cancellation token to cancel the operation. - /// The variant that should be assigned for a given feature. + /// The variant that should be assigned for a given dynamic feature. ValueTask AssignVariantAsync(FeatureVariantAssignmentContext variantAssignmentContext, TContext appContext, CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/IDynamicFeatureManager.cs b/src/Microsoft.FeatureManagement/IDynamicFeatureManager.cs new file mode 100644 index 00000000..3bfd6719 --- /dev/null +++ b/src/Microsoft.FeatureManagement/IDynamicFeatureManager.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement +{ + /// + /// Used to access the variants of a dynamic feature. + /// + public interface IDynamicFeatureManager + { + /// + /// Retrieves a list of dynamic feature names registered in the feature manager. + /// + /// The cancellation token to cancel the operation. + /// An enumerator which provides asynchronous iteration over the dynamic feature names registered in the feature manager. + IAsyncEnumerable GetDynamicFeatureNamesAsync(CancellationToken cancellationToken = default); + + /// + /// Retrieves a typed representation of the feature variant that should be used for a given dynamic feature. + /// + /// The type that the feature variant's configuration should be bound to. + /// The name of the dynamic feature. + /// The cancellation token to cancel the operation. + /// A typed representation of the feature variant that should be used for a given dynamic feature. + ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken); + + /// + /// Retrieves a typed representation of the feature variant that should be used for a given dynamic feature. + /// + /// The type that the feature variant's configuration should be bound to. + /// The type of the context being provided to the dynamic feature manager for use during the process of choosing which variant to use. + /// The name of the dynamic feature. + /// A context providing information that can be used to evaluate which variant should be used for the dynamic feature. + /// The cancellation token to cancel the operation. + /// A typed representation of the feature variant's configuration that should be used for a given feature. + ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.FeatureManagement/IDynamicFeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/IDynamicFeatureManagerSnapshot.cs new file mode 100644 index 00000000..1a5b34c1 --- /dev/null +++ b/src/Microsoft.FeatureManagement/IDynamicFeatureManagerSnapshot.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +namespace Microsoft.FeatureManagement +{ + /// + /// Provides a snapshot of feature state to ensure consistency across a given request. + /// + public interface IDynamicFeatureManagerSnapshot : IDynamicFeatureManager + { + } +} diff --git a/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs index c8c37494..69edd73a 100644 --- a/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs @@ -13,18 +13,33 @@ namespace Microsoft.FeatureManagement public interface IFeatureDefinitionProvider { /// - /// Retrieves the definition for a given feature. + /// Retrieves the definition for a given feature flag. /// - /// The name of the feature to retrieve the definition for. + /// The name of the feature flag to retrieve the definition for. /// The cancellation token to cancel the operation. - /// The feature's definition. - Task GetFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken = default); + /// The feature flag's definition. + Task GetFeatureFlagDefinitionAsync(string featureName, CancellationToken cancellationToken = default); /// - /// Retrieves definitions for all features. + /// Retrieves definitions for all feature flags. /// /// The cancellation token to cancel the operation. - /// An enumerator which provides asynchronous iteration over feature definitions. - IAsyncEnumerable GetAllFeatureDefinitionsAsync(CancellationToken cancellationToken = default); + /// An enumerator which provides asynchronous iteration over feature flag definitions. + IAsyncEnumerable GetAllFeatureFlagDefinitionsAsync(CancellationToken cancellationToken = default); + + /// + /// Retrieves the definition for a given dynamic feature. + /// + /// The name of the dynamic feature to retrieve the definition for. + /// The cancellation token to cancel the operation. + /// The dynamic feature's definition. + Task GetDynamicFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken = default); + + /// + /// Retrieves definitions for all dynamic features. + /// + /// The cancellation token to cancel the operation. + /// An enumerator which provides asynchronous iteration over dynamic feature definitions. + IAsyncEnumerable GetAllDynamicFeatureDefinitionsAsync(CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureManager.cs b/src/Microsoft.FeatureManagement/IFeatureManager.cs index 53231f89..d9259c7b 100644 --- a/src/Microsoft.FeatureManagement/IFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IFeatureManager.cs @@ -8,32 +8,32 @@ namespace Microsoft.FeatureManagement { /// - /// Used to evaluate whether a feature is enabled or disabled. + /// Used to evaluate whether a feature flag is enabled or disabled. /// public interface IFeatureManager { /// - /// Retrieves a list of feature names registered in the feature manager. + /// Retrieves a list of feature flag names registered in the feature manager. /// /// The cancellation token to cancel the operation. - /// An enumerator which provides asynchronous iteration over the feature names registered in the feature manager. - IAsyncEnumerable GetFeatureNamesAsync(CancellationToken cancellationToken = default); + /// An enumerator which provides asynchronous iteration over the feature flag names registered in the feature manager. + IAsyncEnumerable GetFeatureFlagNamesAsync(CancellationToken cancellationToken = default); /// - /// Checks whether a given feature is enabled. + /// Checks whether a given feature flag is enabled. /// - /// The name of the feature to check. + /// The name of the feature flag to check. /// The cancellation token to cancel the operation. - /// True if the feature is enabled, otherwise false. + /// True if the feature flag is enabled, otherwise false. Task IsEnabledAsync(string feature, CancellationToken cancellationToken = default); /// - /// Checks whether a given feature is enabled. + /// Checks whether a given feature flag is enabled. /// - /// The name of the feature to check. - /// A context providing information that can be used to evaluate whether a feature should be on or off. + /// The name of the feature flag to check. + /// A context providing information that can be used to evaluate whether a feature flag should be on or off. /// The cancellation token to cancel the operation. - /// True if the feature is enabled, otherwise false. + /// True if the feature flag is enabled, otherwise false. Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantAssigner.cs b/src/Microsoft.FeatureManagement/IFeatureVariantAssigner.cs index ee6e1627..ce6de1fa 100644 --- a/src/Microsoft.FeatureManagement/IFeatureVariantAssigner.cs +++ b/src/Microsoft.FeatureManagement/IFeatureVariantAssigner.cs @@ -7,16 +7,16 @@ namespace Microsoft.FeatureManagement { /// - /// Provides a method to assign a variant of a feature to be used based off of custom conditions. + /// Provides a method to assign a variant of a dynamic feature to be used based off of custom conditions. /// public interface IFeatureVariantAssigner : IFeatureVariantAssignerMetadata { /// - /// Assign a variant of a feature to be used based off of customized criteria. + /// Assign a variant of a dynamic feature to be used based off of customized criteria. /// - /// A variant assignment context that contains information needed to assign a variant for a feature. + /// A variant assignment context that contains information needed to assign a variant for a dynamic feature. /// The cancellation token to cancel the operation. - /// The variant that should be assigned for a given feature. + /// The variant that should be assigned for a given dynamic feature. ValueTask AssignVariantAsync(FeatureVariantAssignmentContext variantAssignmentContext, CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantAssignerMetadata.cs b/src/Microsoft.FeatureManagement/IFeatureVariantAssignerMetadata.cs index 713cf852..2b09c689 100644 --- a/src/Microsoft.FeatureManagement/IFeatureVariantAssignerMetadata.cs +++ b/src/Microsoft.FeatureManagement/IFeatureVariantAssignerMetadata.cs @@ -4,7 +4,7 @@ namespace Microsoft.FeatureManagement { /// - /// Marker interface for feature variant assigners used to assign which variant should be used for a feature. + /// Marker interface for feature variant assigners used to assign which variant should be used for a dynamic feature. /// public interface IFeatureVariantAssignerMetadata { diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantManager.cs b/src/Microsoft.FeatureManagement/IFeatureVariantManager.cs deleted file mode 100644 index cc69ffc8..00000000 --- a/src/Microsoft.FeatureManagement/IFeatureVariantManager.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.FeatureManagement -{ - /// - /// Used to access the variants of a feature. - /// - public interface IFeatureVariantManager - { - /// - /// Retrieves a typed representation of the configuration variant that should be used for a given feature. - /// - /// The type that the variants configuration should be bound to. - /// The name of the feature. - /// The cancellation token to cancel the operation. - /// A typed representation of the configuration variant that should be used for a given feature. - ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken); - - /// - /// Retrieves a typed representation of the configuration variant that should be used for a given feature. - /// - /// The type that the variants configuration should be bound to. - /// The type of the context being provided to the feature variant manager for use during the process of choosing which variant to use. - /// The name of the feature. - /// A context providing information that can be used to evaluate whether a feature should be on or off. - /// The cancellation token to cancel the operation. - /// A typed representation of the configuration variant that should be used for a given feature. - ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken); - } -} diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs b/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs index be0e2cd0..ecf15bdb 100644 --- a/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs +++ b/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs @@ -12,13 +12,13 @@ namespace Microsoft.FeatureManagement public interface IFeatureVariantOptionsResolver { /// - /// Retrieves typed options for a given feature definition and chosen variant. + /// Retrieves typed options for a given dynamic feature definition and chosen variant. /// /// The type of the options to return. - /// The definition of the feature that the resolution is being performed for. - /// The chosen variant of the feature. + /// The definition of the dynamic feature that the resolution is being performed for. + /// The chosen variant of the dynamic feature. /// The cancellation token to cancel the operation. - /// Typed options for a given feature definition and chosen variant. - ValueTask GetOptionsAsync(FeatureDefinition featureDefinition, FeatureVariant variant, CancellationToken cancellationToken); + /// Typed options for a given dynamic feature definition and chosen variant. + ValueTask GetOptionsAsync(DynamicFeatureDefinition featureDefinition, FeatureVariant variant, CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 233413d1..a57dbba4 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -32,7 +32,7 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec services.TryAddSingleton(sp => sp.GetRequiredService()); - services.TryAddSingleton(sp => sp.GetRequiredService()); + services.TryAddSingleton(sp => sp.GetRequiredService()); services.TryAddSingleton(); diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs index 73807e9e..e703da76 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs @@ -10,7 +10,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.FeatureManagement +namespace Microsoft.FeatureManagement.Assigners { /// /// A feature variant assigner that can be used to assign a variant based on targeted audiences. @@ -49,7 +49,7 @@ public ValueTask AssignVariantAsync(FeatureVariantAssignmentCont throw new ArgumentNullException(nameof(targetingContext)); } - FeatureDefinition featureDefinition = variantAssignmentContext.FeatureDefinition; + DynamicFeatureDefinition featureDefinition = variantAssignmentContext.FeatureDefinition; if (featureDefinition == null) { diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAssigner.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAssigner.cs index f041c299..7068f0b3 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAssigner.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAssigner.cs @@ -8,7 +8,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.FeatureManagement +namespace Microsoft.FeatureManagement.Assigners { /// /// A feature variant assigner that can be used to assign a variant based on targeted audiences. diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index e35265d7..0a1f4719 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.FeatureManagement; +using Microsoft.FeatureManagement.Assigners; using Microsoft.FeatureManagement.FeatureFilters; using System; using System.Collections.Generic; @@ -74,7 +75,7 @@ public async Task ReadsConfiguration() Assert.True(called); - IFeatureVariantManager variantManager = serviceProvider.GetRequiredService(); + IDynamicFeatureManager variantManager = serviceProvider.GetRequiredService(); IEnumerable featureVariantAssigners = serviceProvider.GetRequiredService>(); @@ -117,6 +118,51 @@ public async Task ReadsConfiguration() Assert.Equal("def", val); } + [Fact] + public async Task ReadsV1Configuration() + { + IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.v1.json").Build(); + + var services = new ServiceCollection(); + + services + .AddSingleton(config) + .AddFeatureManagement() + .AddFeatureFilter() + .AddFeatureVariantAssigner(); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + IFeatureManager featureManager = serviceProvider.GetRequiredService(); + + Assert.True(await featureManager.IsEnabledAsync(OnFeature, CancellationToken.None)); + + Assert.False(await featureManager.IsEnabledAsync(OffFeature, CancellationToken.None)); + + IEnumerable featureFilters = serviceProvider.GetRequiredService>(); + + // + // 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, CancellationToken.None); + + Assert.True(called); + } + [Fact] public async Task AllowsSuffix() { @@ -417,7 +463,7 @@ public async Task VariantTargeting() ServiceProvider serviceProvider = services.BuildServiceProvider(); - IFeatureVariantManager variantManager = serviceProvider.GetRequiredService(); + IDynamicFeatureManager variantManager = serviceProvider.GetRequiredService(); // // Targeted @@ -460,7 +506,7 @@ public async Task AccumulatesAudience() ServiceProvider serviceProvider = services.BuildServiceProvider(); - IFeatureVariantManager variantManager = serviceProvider.GetRequiredService(); + IDynamicFeatureManager variantManager = serviceProvider.GetRequiredService(); IFeatureDefinitionProvider featureProvider = serviceProvider.GetRequiredService(); @@ -659,7 +705,7 @@ public async Task UsesContextVariants() return ctx.FeatureDefinition.Variants.FirstOrDefault(v => v.Default); }; - IFeatureVariantManager variantManager = provider.GetRequiredService(); + IDynamicFeatureManager variantManager = provider.GetRequiredService(); AppContext context = new AppContext(); @@ -737,16 +783,29 @@ public async Task ListsFeatures() { IFeatureManager featureManager = provider.GetRequiredService(); - bool hasItems = false; + bool hasFeatureFlags = false; - await foreach (string feature in featureManager.GetFeatureNamesAsync(CancellationToken.None)) + await foreach (string feature in featureManager.GetFeatureFlagNamesAsync(CancellationToken.None)) { - hasItems = true; + hasFeatureFlags = true; break; } - Assert.True(hasItems); + Assert.True(hasFeatureFlags); + + IDynamicFeatureManager dynamicFeatureManager = provider.GetRequiredService(); + + bool hasDynamicFeatures = false; + + await foreach (string feature in dynamicFeatureManager.GetDynamicFeatureNamesAsync(CancellationToken.None)) + { + hasDynamicFeatures = true; + + break; + } + + Assert.True(hasDynamicFeatures); } } @@ -800,7 +859,11 @@ public async Task SwallowsExceptionForMissingFeatureFilter() [Fact] public async Task CustomFeatureDefinitionProvider() { - FeatureDefinition testFeature = new FeatureDefinition + const string DynamicFeature = "DynamicFeature"; + + // + // Feature flag + FeatureFlagDefinition testFeature = new FeatureFlagDefinition { Name = ConditionalFeature, EnabledFor = new List() @@ -816,12 +879,56 @@ public async Task CustomFeatureDefinitionProvider() } }; + // + // Dynamic feature + DynamicFeatureDefinition dynamicFeature = new DynamicFeatureDefinition + { + Name = DynamicFeature, + Assigner = "Test", + Variants = new List() + { + new FeatureVariant + { + Name = "V1", + AssignmentParameters = new ConfigurationBuilder().AddInMemoryCollection( + new Dictionary() + { + { "P1", "V1" } + }) + .Build(), + ConfigurationReference = "Ref1", + Default = true + }, + new FeatureVariant + { + Name = "V2", + AssignmentParameters = new ConfigurationBuilder().AddInMemoryCollection( + new Dictionary() + { + { "P2", "V2" } + }) + .Build(), + ConfigurationReference = "Ref2" + } + } + }; + var services = new ServiceCollection(); - services.AddSingleton(new InMemoryFeatureDefinitionProvider(new FeatureDefinition[] { testFeature })) + services.AddSingleton( + new InMemoryFeatureDefinitionProvider( + new FeatureFlagDefinition[] + { + testFeature + }, + new DynamicFeatureDefinition[] + { + dynamicFeature + })) .AddSingleton(new ConfigurationBuilder().Build()) .AddFeatureManagement() - .AddFeatureFilter(); + .AddFeatureFilter() + .AddFeatureVariantAssigner(); ServiceProvider serviceProvider = services.BuildServiceProvider(); @@ -849,6 +956,43 @@ public async Task CustomFeatureDefinitionProvider() await featureManager.IsEnabledAsync(ConditionalFeature, CancellationToken.None); Assert.True(called); + + IDynamicFeatureManager dynamicFeatureManager = serviceProvider.GetRequiredService(); + + IEnumerable featureAssigners = serviceProvider.GetRequiredService>(); + + // + // Sync filter + TestAssigner testFeatureVariantAssigner = (TestAssigner)featureAssigners.First(f => f is TestAssigner); + + called = false; + + testFeatureVariantAssigner.Callback = (assignmentContext) => + { + called = true; + + Assert.True(assignmentContext.FeatureDefinition.Variants.Count() == 2); + + FeatureVariant v1 = assignmentContext.FeatureDefinition.Variants.First(v => v.Name == "V1"); + + Assert.True(v1.Default); + + Assert.Equal("V1", v1.AssignmentParameters["P1"]); + + Assert.Equal("Ref1", v1.ConfigurationReference); + + FeatureVariant v2 = assignmentContext.FeatureDefinition.Variants.First(v => v.Name == "V2"); + + Assert.False(v2.Default); + + Assert.Equal("Ref2", v2.ConfigurationReference); + + return v1; + }; + + await dynamicFeatureManager.GetVariantAsync(DynamicFeature, CancellationToken.None); + + Assert.True(called); } private static void DisableEndpointRouting(MvcOptions options) diff --git a/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs b/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs index 63a1032e..cb03024a 100644 --- a/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs +++ b/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs @@ -13,26 +13,45 @@ namespace Tests.FeatureManagement { class InMemoryFeatureDefinitionProvider : IFeatureDefinitionProvider { - private IEnumerable _definitions; + private IEnumerable _featureFlagDefinitions; + private IEnumerable _dynamicFeatureDefinitions; - public InMemoryFeatureDefinitionProvider(IEnumerable featureDefinitions) + public InMemoryFeatureDefinitionProvider( + IEnumerable featureFlagDefinitions, + IEnumerable dynamicFeatureDefinitions) { - _definitions = featureDefinitions ?? throw new ArgumentNullException(nameof(featureDefinitions)); + _featureFlagDefinitions = featureFlagDefinitions ?? throw new ArgumentNullException(nameof(featureFlagDefinitions)); + _dynamicFeatureDefinitions = dynamicFeatureDefinitions ?? throw new ArgumentNullException(nameof(dynamicFeatureDefinitions)); } #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - public async IAsyncEnumerable GetAllFeatureDefinitionsAsync([EnumeratorCancellation] CancellationToken cancellationToken) + public async IAsyncEnumerable GetAllFeatureFlagDefinitionsAsync([EnumeratorCancellation] CancellationToken cancellationToken) #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { - foreach (FeatureDefinition definition in _definitions) + foreach (FeatureFlagDefinition definition in _featureFlagDefinitions) { yield return definition; } } - public Task GetFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken) + public Task GetFeatureFlagDefinitionAsync(string featureName, CancellationToken cancellationToken) { - return Task.FromResult(_definitions.FirstOrDefault(definitions => definitions.Name.Equals(featureName, StringComparison.OrdinalIgnoreCase))); + return Task.FromResult(_featureFlagDefinitions.FirstOrDefault(definitions => definitions.Name.Equals(featureName, StringComparison.OrdinalIgnoreCase))); + } + + public Task GetDynamicFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken = default) + { + return Task.FromResult(_dynamicFeatureDefinitions.FirstOrDefault(definitions => definitions.Name.Equals(featureName, StringComparison.OrdinalIgnoreCase))); + } + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + public async IAsyncEnumerable GetAllDynamicFeatureDefinitionsAsync(CancellationToken cancellationToken = default) +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + foreach (DynamicFeatureDefinition definition in _dynamicFeatureDefinitions) + { + yield return definition; + } } } } diff --git a/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj b/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj index f036243b..460c52c7 100644 --- a/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj +++ b/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj @@ -8,9 +8,14 @@ + + + Always + PreserveNewest + Always PreserveNewest diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index 0b7b8b04..8ca72556 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -1,211 +1,208 @@ { - "Logging": { - "LogLevel": { - "Default": "Warning" - } - }, - "AllowedHosts": "*", - "FeatureManagement": { - "OnTestFeature": true, - "OffTestFeature": false, - "TargetingTestFeature": { - "EnabledFor": [ - { - "Name": "Targeting", - "Parameters": { - "Audience": { - "Users": [ - "Jeff", - "Alicia" - ], - "Groups": [ - { - "Name": "Ring0", - "RolloutPercentage": 100 - }, - { - "Name": "Ring1", - "RolloutPercentage": 50 - } - ], - "DefaultRolloutPercentage": 20 + "FeatureFlags": { + "OnTestFeature": true, + "OffTestFeature": false, + "ConditionalFeature": { + "EnabledFor": [ + { + "Name": "Test", + "Parameters": { + "P1": "V1" } } - } - ] - }, - "ConditionalFeature": { - "EnabledFor": [ - { - "Name": "Test", - "Parameters": { - "P1": "V1" + ] + }, + "TargetingTestFeature": { + "EnabledFor": [ + { + "Name": "Targeting", + "Parameters": { + "Audience": { + "Users": [ + "Jeff", + "Alicia" + ], + "Groups": [ + { + "Name": "Ring0", + "RolloutPercentage": 100 + }, + { + "Name": "Ring1", + "RolloutPercentage": 50 + } + ], + "DefaultRolloutPercentage": 20 + } + } } - } - ] - }, - "ConditionalFeature2": { - "EnabledFor": [ - { - "Name": "Test" - } - ] - }, - "WithSuffixFeature": { - "EnabledFor": [ - { - "Name": "TestFilter", - "Parameters": { + ] + }, + "ConditionalFeature2": { + "EnabledFor": [ + { + "Name": "Test" } - } - ] - }, - "WithoutSuffixFeature": { - "EnabledFor": [ - { - "Name": "Test", - "Parameters": { + ] + }, + "WithSuffixFeature": { + "EnabledFor": [ + { + "Name": "TestFilter", + "Parameters": { + } } - } - ] - }, - "ContextualFeature": { - "EnabledFor": [ - { - "Name": "ContextualTest", - "Parameters": { - "AllowedAccounts": [ - "abc" - ] + ] + }, + "WithoutSuffixFeature": { + "EnabledFor": [ + { + "Name": "Test", + "Parameters": { + } } - } - ] - }, - "VariantFeature": { - "Assigner": "Test", - "Variants": [ - { - "Default": true, - "Name": "V1", - "ConfigurationReference": "Ref1" - }, - { - "Name": "V2", - "ConfigurationReference": "Ref2", - "AssignmentParameters": { - "P1": "V1" + ] + }, + "ContextualFeature": { + "EnabledFor": [ + { + "Name": "ContextualTest", + "Parameters": { + "AllowedAccounts": [ + "abc" + ] + } } - } - ] + ] + } }, - "ContextualVariantFeature": { - "Assigner": "ContextualTest", - "Variants": [ - { - "Default": true, - "Name": "V1", - "ConfigurationReference": "Ref1" - }, - { - "Name": "V2", - "ConfigurationReference": "Ref2", - "AssignmentParameters": { - "AllowedAccounts": [ - "abc" - ] + "DynamicFeatures": { + "VariantFeature": { + "Assigner": "Test", + "Variants": [ + { + "Default": true, + "Name": "V1", + "ConfigurationReference": "Ref1" + }, + { + "Name": "V2", + "ConfigurationReference": "Ref2", + "AssignmentParameters": { + "P1": "V1" + } } - } - ] - }, - "ContextualVariantTargetingFeature": { - "Assigner": "Targeting", - "Variants": [ - { - "Default": true, - "Name": "V1", - "ConfigurationReference": "Ref1" - }, - { - "Name": "V2", - "ConfigurationReference": "Ref2", - "AssignmentParameters": { - "Audience": { - "Users": [ - "Jeff" + ] + }, + "ContextualVariantFeature": { + "Assigner": "ContextualTest", + "Variants": [ + { + "Default": true, + "Name": "V1", + "ConfigurationReference": "Ref1" + }, + { + "Name": "V2", + "ConfigurationReference": "Ref2", + "AssignmentParameters": { + "AllowedAccounts": [ + "abc" ] } } - } - ] - }, - "AccumulatedTargetingFeature": { - "Assigner": "Targeting", - "Variants": [ - { - "Default": true, - "Name": "V1", - "ConfigurationReference": "Percentage15" - }, - { - "Name": "V2", - "ConfigurationReference": "Percentage35", - "AssignmentParameters": { - "Audience": { - "DefaultRolloutPercentage": 35 + ] + }, + "ContextualVariantTargetingFeature": { + "Assigner": "Targeting", + "Variants": [ + { + "Default": true, + "Name": "V1", + "ConfigurationReference": "Ref1" + }, + { + "Name": "V2", + "ConfigurationReference": "Ref2", + "AssignmentParameters": { + "Audience": { + "Users": [ + "Jeff" + ] + } } } - }, - { - "Name": "V3", - "ConfigurationReference": "Percentage50", - "AssignmentParameters": { - "Audience": { - "DefaultRolloutPercentage": 50 + ] + }, + "AccumulatedTargetingFeature": { + "Assigner": "Targeting", + "Variants": [ + { + "Default": true, + "Name": "V1", + "ConfigurationReference": "Percentage15" + }, + { + "Name": "V2", + "ConfigurationReference": "Percentage35", + "AssignmentParameters": { + "Audience": { + "DefaultRolloutPercentage": 35 + } } - } - } - ] - }, - "AccumulatedGroupsTargetingFeature": { - "Assigner": "Targeting", - "Variants": [ - { - "Default": true, - "Name": "V1", - "ConfigurationReference": "Percentage15" - }, - { - "Name": "V2", - "ConfigurationReference": "Percentage35", - "AssignmentParameters": { - "Audience": { - "Groups": [ - { - "Name": "r", - "RolloutPercentage": 35 - } - ], - "DefaultRolloutPercentage": 0 + }, + { + "Name": "V3", + "ConfigurationReference": "Percentage50", + "AssignmentParameters": { + "Audience": { + "DefaultRolloutPercentage": 50 + } } } - }, - { - "Name": "V3", - "ConfigurationReference": "Percentage50", - "AssignmentParameters": { - "Audience": { - "Groups": [ - { - "Name": "r", - "RolloutPercentage": 50 - } - ], - "DefaultRolloutPercentage": 0 + ] + }, + "AccumulatedGroupsTargetingFeature": { + "Assigner": "Targeting", + "Variants": [ + { + "Default": true, + "Name": "V1", + "ConfigurationReference": "Percentage15" + }, + { + "Name": "V2", + "ConfigurationReference": "Percentage35", + "AssignmentParameters": { + "Audience": { + "Groups": [ + { + "Name": "r", + "RolloutPercentage": 35 + } + ], + "DefaultRolloutPercentage": 0 + } + } + }, + { + "Name": "V3", + "ConfigurationReference": "Percentage50", + "AssignmentParameters": { + "Audience": { + "Groups": [ + { + "Name": "r", + "RolloutPercentage": 50 + } + ], + "DefaultRolloutPercentage": 0 + } } } - } - ] + ] + } } }, "Ref1": "abc", diff --git a/tests/Tests.FeatureManagement/appsettings.v1.json b/tests/Tests.FeatureManagement/appsettings.v1.json new file mode 100644 index 00000000..93818779 --- /dev/null +++ b/tests/Tests.FeatureManagement/appsettings.v1.json @@ -0,0 +1,16 @@ +{ + "FeatureManagement": { + "OnTestFeature": true, + "OffTestFeature": false, + "ConditionalFeature": { + "EnabledFor": [ + { + "Name": "Test", + "Parameters": { + "P1": "V1" + } + } + ] + } + } +} From f9b661b0b2a519fef9217a3d1172feddc1252601 Mon Sep 17 00:00:00 2001 From: Jimmy Campbell Date: Fri, 7 Jan 2022 15:35:05 -0800 Subject: [PATCH 2/4] * Separated feature flag implementation from dynamic feature impelementation in FeatureManager, ConfigurationFeatureDefinitionProvider, and FeatureManagerSnapshot.. * Separated IConfigurationDefinitionProvider into IFeatureFlagDefinitionProvider and IDynamicFeatureDefinitionProvider * Added missing default cancellation tokens. --- .../CustomAssignmentConsoleApp/Program.cs | 4 +- .../Views/Shared/_Layout.cshtml | 4 +- examples/TargetingConsoleApp/Program.cs | 4 +- ...urationDynamicFeatureDefinitionProvider.cs | 144 ++++++++++ ...igurationFeatureFlagDefinitionProvider.cs} | 111 +------- .../DynamicFeatureManager.cs | 253 ++++++++++++++++++ .../DynamicFeatureManagerSnapshot.cs | 87 ++++++ .../FeatureManager.cs | 201 +------------- .../FeatureManagerSnapshot.cs | 71 +---- .../IContextualFeatureVariantAssigner.cs | 2 +- .../IDynamicFeatureDefinitionProvider.cs | 30 +++ .../IDynamicFeatureManager.cs | 8 +- .../IFeatureDefinitionProvider.cs | 45 ---- .../IFeatureFlagDefinitionProvider.cs | 30 +++ .../IFeatureManager.cs | 8 +- .../IFeatureVariantAssigner.cs | 2 +- .../IFeatureVariantOptionsResolver.cs | 2 +- .../ServiceCollectionExtensions.cs | 18 +- .../FeatureManagement.cs | 24 +- .../InMemoryFeatureDefinitionProvider.cs | 2 +- 20 files changed, 600 insertions(+), 450 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/ConfigurationDynamicFeatureDefinitionProvider.cs rename src/Microsoft.FeatureManagement/{ConfigurationFeatureDefinitionProvider.cs => ConfigurationFeatureFlagDefinitionProvider.cs} (61%) create mode 100644 src/Microsoft.FeatureManagement/DynamicFeatureManager.cs create mode 100644 src/Microsoft.FeatureManagement/DynamicFeatureManagerSnapshot.cs create mode 100644 src/Microsoft.FeatureManagement/IDynamicFeatureDefinitionProvider.cs delete mode 100644 src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs create mode 100644 src/Microsoft.FeatureManagement/IFeatureFlagDefinitionProvider.cs diff --git a/examples/CustomAssignmentConsoleApp/Program.cs b/examples/CustomAssignmentConsoleApp/Program.cs index feacf45b..a9ec26d4 100644 --- a/examples/CustomAssignmentConsoleApp/Program.cs +++ b/examples/CustomAssignmentConsoleApp/Program.cs @@ -35,9 +35,9 @@ public static async Task Main(string[] args) // Get the feature manager from application services using (ServiceProvider serviceProvider = services.BuildServiceProvider()) { - IDynamicFeatureManager variantManager = serviceProvider.GetRequiredService(); + IDynamicFeatureManager dynamicFeatureManager = serviceProvider.GetRequiredService(); - DailyDiscountOptions discountOptions = await variantManager + DailyDiscountOptions discountOptions = await dynamicFeatureManager .GetVariantAsync("DailyDiscount", CancellationToken.None); // diff --git a/examples/FeatureFlagDemo/Views/Shared/_Layout.cshtml b/examples/FeatureFlagDemo/Views/Shared/_Layout.cshtml index c4f49f22..f8a5d76b 100644 --- a/examples/FeatureFlagDemo/Views/Shared/_Layout.cshtml +++ b/examples/FeatureFlagDemo/Views/Shared/_Layout.cshtml @@ -1,7 +1,7 @@ @using Microsoft.FeatureManagement -@inject IDynamicFeatureManager variantManager; +@inject IDynamicFeatureManager dynamicFeatureManager; @{ - DiscountBannerOptions opts = await variantManager.GetVariantAsync("DiscountBanner", Context.RequestAborted); + DiscountBannerOptions opts = await dynamicFeatureManager.GetVariantAsync("DiscountBanner", Context.RequestAborted); } diff --git a/examples/TargetingConsoleApp/Program.cs b/examples/TargetingConsoleApp/Program.cs index 1b25c4dc..ca4efd46 100644 --- a/examples/TargetingConsoleApp/Program.cs +++ b/examples/TargetingConsoleApp/Program.cs @@ -41,7 +41,7 @@ public static async Task Main(string[] args) using (ServiceProvider serviceProvider = services.BuildServiceProvider()) { IFeatureManager featureManager = serviceProvider.GetRequiredService(); - IDynamicFeatureManager variantManager = serviceProvider.GetRequiredService(); + IDynamicFeatureManager dynamicFeatureManager = serviceProvider.GetRequiredService(); // // We'll simulate a task to run on behalf of each known user @@ -77,7 +77,7 @@ public static async Task Main(string[] args) // // Retrieve feature variant using targeting - CartOptions cartOptions = await variantManager + CartOptions cartOptions = await dynamicFeatureManager .GetVariantAsync( DynamicFeatureName, targetingContext, diff --git a/src/Microsoft.FeatureManagement/ConfigurationDynamicFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationDynamicFeatureDefinitionProvider.cs new file mode 100644 index 00000000..82794ef6 --- /dev/null +++ b/src/Microsoft.FeatureManagement/ConfigurationDynamicFeatureDefinitionProvider.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Primitives; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement +{ + /// + /// A feature definition provider that pulls dynamic feature definitions from the .NET Core system. + /// + sealed class ConfigurationDynamicFeatureDefinitionProvider : IDynamicFeatureDefinitionProvider, IDisposable + { + private const string FeatureManagementSectionName = "FeatureManagement"; + private const string DynamicFeatureDefinitionsSectionName= "DynamicFeatures"; + private const string FeatureVariantsSectionName = "Variants"; + private readonly IConfiguration _configuration; + private readonly ConcurrentDictionary _dynamicFeatureDefinitions; + private IDisposable _changeSubscription; + private int _stale = 0; + + public ConfigurationDynamicFeatureDefinitionProvider(IConfiguration configuration) + { + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _dynamicFeatureDefinitions = new ConcurrentDictionary(); + + _changeSubscription = ChangeToken.OnChange( + () => _configuration.GetReloadToken(), + () => _stale = 1); + } + + public void Dispose() + { + _changeSubscription?.Dispose(); + + _changeSubscription = null; + } + + public Task GetDynamicFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken = default) + { + if (featureName == null) + { + throw new ArgumentNullException(nameof(featureName)); + } + + EnsureFresh(); + + // + // Query by feature name + DynamicFeatureDefinition definition = _dynamicFeatureDefinitions.GetOrAdd(featureName, (name) => ReadDynamicFeatureDefinition(name)); + + 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 GetAllDynamicFeatureDefinitionsAsync([EnumeratorCancellation] CancellationToken cancellationToken) +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + EnsureFresh(); + + // + // Iterate over all features registered in the system at initial invocation time + foreach (IConfigurationSection featureSection in GetDynamicFeatureDefinitionSections()) + { + // + // Underlying IConfigurationSection data is dynamic so latest feature definitions are returned + yield return _dynamicFeatureDefinitions.GetOrAdd(featureSection.Key, (_) => ReadDynamicFeatureDefinition(featureSection)); + } + } + + private DynamicFeatureDefinition ReadDynamicFeatureDefinition(string featureName) + { + IConfigurationSection configuration = GetDynamicFeatureDefinitionSections() + .FirstOrDefault(section => section.Key.Equals(featureName, StringComparison.OrdinalIgnoreCase)); + + if (configuration == null) + { + return null; + } + + return ReadDynamicFeatureDefinition(configuration); + } + + private DynamicFeatureDefinition ReadDynamicFeatureDefinition(IConfigurationSection configurationSection) + { + Debug.Assert(configurationSection != null); + + var variants = new List(); + + foreach (IConfigurationSection section in configurationSection.GetSection(FeatureVariantsSectionName).GetChildren()) + { + if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[nameof(FeatureVariant.Name)])) + { + variants.Add(new FeatureVariant + { + Default = section.GetValue(nameof(FeatureVariant.Default)), + Name = section.GetValue(nameof(FeatureVariant.Name)), + ConfigurationReference = section.GetValue(nameof(FeatureVariant.ConfigurationReference)), + AssignmentParameters = section.GetSection(nameof(FeatureVariant.AssignmentParameters)) + }); + } + } + + return new DynamicFeatureDefinition() + { + Name = configurationSection.Key, + Variants = variants, + Assigner = configurationSection.GetValue(nameof(DynamicFeatureDefinition.Assigner)) + }; + } + + private IEnumerable GetDynamicFeatureDefinitionSections() + { + // + // Look for feature definitions under the "FeatureManagement" section + IConfiguration featureManagementSection = _configuration.GetChildren().Any(s => s.Key.Equals(FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)) ? + _configuration.GetSection(FeatureManagementSectionName) : + _configuration; + + return featureManagementSection + .GetSection(DynamicFeatureDefinitionsSectionName) + .GetChildren(); + } + + private void EnsureFresh() + { + if (Interlocked.Exchange(ref _stale, 0) != 0) + { + _dynamicFeatureDefinitions.Clear(); + } + } + } +} diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureFlagDefinitionProvider.cs similarity index 61% rename from src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs rename to src/Microsoft.FeatureManagement/ConfigurationFeatureFlagDefinitionProvider.cs index 6fae88de..d302cff2 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureFlagDefinitionProvider.cs @@ -15,26 +15,22 @@ namespace Microsoft.FeatureManagement { /// - /// A feature definition provider that pulls feature definitions from the .NET Core system. + /// A feature definition provider that pulls feature flag definitions from the .NET Core system. /// - sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionProvider, IDisposable + sealed class ConfigurationFeatureFlagDefinitionProvider : IFeatureFlagDefinitionProvider, IDisposable { private const string FeatureManagementSectionName = "FeatureManagement"; private const string FeatureFlagDefinitionsSectionName = "FeatureFlags"; - private const string DynamicFeatureDefinitionsSectionName= "DynamicFeatures"; private const string FeatureFiltersSectionName = "EnabledFor"; - private const string FeatureVariantsSectionName = "Variants"; private readonly IConfiguration _configuration; private readonly ConcurrentDictionary _featureFlagDefinitions; - private readonly ConcurrentDictionary _dynamicFeatureDefinitions; private IDisposable _changeSubscription; private int _stale = 0; - public ConfigurationFeatureDefinitionProvider(IConfiguration configuration) + public ConfigurationFeatureFlagDefinitionProvider(IConfiguration configuration) { _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _featureFlagDefinitions = new ConcurrentDictionary(); - _dynamicFeatureDefinitions = new ConcurrentDictionary(); _changeSubscription = ChangeToken.OnChange( () => _configuration.GetReloadToken(), @@ -83,41 +79,6 @@ public async IAsyncEnumerable GetAllFeatureFlagDefinition } } - public Task GetDynamicFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken = default) - { - if (featureName == null) - { - throw new ArgumentNullException(nameof(featureName)); - } - - EnsureFresh(); - - // - // Query by feature name - DynamicFeatureDefinition definition = _dynamicFeatureDefinitions.GetOrAdd(featureName, (name) => ReadDynamicFeatureDefinition(name)); - - 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 GetAllDynamicFeatureDefinitionsAsync([EnumeratorCancellation] CancellationToken cancellationToken) -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - { - EnsureFresh(); - - // - // Iterate over all features registered in the system at initial invocation time - foreach (IConfigurationSection featureSection in GetDynamicFeatureDefinitionSections()) - { - // - // Underlying IConfigurationSection data is dynamic so latest feature definitions are returned - yield return _dynamicFeatureDefinitions.GetOrAdd(featureSection.Key, (_) => ReadDynamicFeatureDefinition(featureSection)); - } - } - private FeatureFlagDefinition ReadFeatureFlagDefinition(string featureName) { IConfigurationSection configuration = GetFeatureFlagDefinitionSections() @@ -131,19 +92,6 @@ private FeatureFlagDefinition ReadFeatureFlagDefinition(string featureName) return ReadFeatureFlagDefinition(configuration); } - private DynamicFeatureDefinition ReadDynamicFeatureDefinition(string featureName) - { - IConfigurationSection configuration = GetDynamicFeatureDefinitionSections() - .FirstOrDefault(section => section.Key.Equals(featureName, StringComparison.OrdinalIgnoreCase)); - - if (configuration == null) - { - return null; - } - - return ReadDynamicFeatureDefinition(configuration); - } - private FeatureFlagDefinition ReadFeatureFlagDefinition(IConfigurationSection configurationSection) { /* @@ -226,37 +174,15 @@ We support }; } - private DynamicFeatureDefinition ReadDynamicFeatureDefinition(IConfigurationSection configurationSection) - { - Debug.Assert(configurationSection != null); - - var variants = new List(); - - foreach (IConfigurationSection section in configurationSection.GetSection(FeatureVariantsSectionName).GetChildren()) - { - if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[nameof(FeatureVariant.Name)])) - { - variants.Add(new FeatureVariant - { - Default = section.GetValue(nameof(FeatureVariant.Default)), - Name = section.GetValue(nameof(FeatureVariant.Name)), - ConfigurationReference = section.GetValue(nameof(FeatureVariant.ConfigurationReference)), - AssignmentParameters = section.GetSection(nameof(FeatureVariant.AssignmentParameters)) - }); - } - } - - return new DynamicFeatureDefinition() - { - Name = configurationSection.Key, - Variants = variants, - Assigner = configurationSection.GetValue(nameof(DynamicFeatureDefinition.Assigner)) - }; - } - private IEnumerable GetFeatureFlagDefinitionSections() { - IEnumerable featureManagementChildren = GetFeatureManagementSection().GetChildren(); + // + // Look for feature definitions under the "FeatureManagement" section + IConfiguration featureManagementSection = _configuration.GetChildren().Any(s => s.Key.Equals(FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)) ? + _configuration.GetSection(FeatureManagementSectionName) : + _configuration; + + IEnumerable featureManagementChildren = featureManagementSection.GetChildren(); IConfigurationSection featureFlagsSection = featureManagementChildren.FirstOrDefault(s => s.Key == FeatureFlagDefinitionsSectionName); @@ -266,29 +192,12 @@ private IEnumerable GetFeatureFlagDefinitionSections() featureManagementChildren : featureFlagsSection.GetChildren(); } - - private IEnumerable GetDynamicFeatureDefinitionSections() - { - return GetFeatureManagementSection() - .GetSection(DynamicFeatureDefinitionsSectionName) - .GetChildren(); - } - - private IConfiguration GetFeatureManagementSection() - { - // - // Look for feature definitions under the "FeatureManagement" section - return _configuration.GetChildren().Any(s => s.Key.Equals(FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)) ? - _configuration.GetSection(FeatureManagementSectionName) : - _configuration; - } private void EnsureFresh() { if (Interlocked.Exchange(ref _stale, 0) != 0) { _featureFlagDefinitions.Clear(); - _dynamicFeatureDefinitions.Clear(); } } } diff --git a/src/Microsoft.FeatureManagement/DynamicFeatureManager.cs b/src/Microsoft.FeatureManagement/DynamicFeatureManager.cs new file mode 100644 index 00000000..e9d9252a --- /dev/null +++ b/src/Microsoft.FeatureManagement/DynamicFeatureManager.cs @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement +{ + /// + /// Used to retrieve variants for dynamic features. + /// + class DynamicFeatureManager : IDynamicFeatureManager + { + private readonly IDynamicFeatureDefinitionProvider _featureDefinitionProvider; + private readonly IEnumerable _variantAssigners; + private readonly IFeatureVariantOptionsResolver _variantOptionsResolver; + private readonly ConcurrentDictionary _assignerMetadataCache; + private readonly ConcurrentDictionary _contextualFeatureVariantAssignerCache; + + public DynamicFeatureManager( + IDynamicFeatureDefinitionProvider featureDefinitionProvider, + IEnumerable variantAssigner, + IFeatureVariantOptionsResolver variantOptionsResolver) + { + _variantAssigners = variantAssigner ?? throw new ArgumentNullException(nameof(variantAssigner)); + _variantOptionsResolver = variantOptionsResolver ?? throw new ArgumentNullException(nameof(variantOptionsResolver)); + _featureDefinitionProvider = featureDefinitionProvider ?? throw new ArgumentNullException(nameof(featureDefinitionProvider)); + _assignerMetadataCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + _contextualFeatureVariantAssignerCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + } + + public async IAsyncEnumerable GetDynamicFeatureNamesAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (DynamicFeatureDefinition featureDefintion in _featureDefinitionProvider.GetAllDynamicFeatureDefinitionsAsync(cancellationToken).ConfigureAwait(false)) + { + yield return featureDefintion.Name; + } + } + + public ValueTask GetVariantAsync(string feature, TContext appContext, CancellationToken cancellationToken) + { + return GetVariantAsync(feature, appContext, true, cancellationToken); + } + + public ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken) + { + return GetVariantAsync(feature, null, false, cancellationToken); + } + + private async ValueTask GetVariantAsync(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken) + { + if (feature == null) + { + throw new ArgumentNullException(nameof(feature)); + } + + FeatureVariant variant = null; + + DynamicFeatureDefinition featureDefinition = await _featureDefinitionProvider + .GetDynamicFeatureDefinitionAsync(feature, cancellationToken) + .ConfigureAwait(false); + + if (featureDefinition == null) + { + throw new FeatureManagementException( + FeatureManagementError.MissingFeature, + $"The feature declaration for the dynamic feature '{feature}' was not found."); + } + + if (string.IsNullOrEmpty(featureDefinition.Assigner)) + { + throw new FeatureManagementException( + FeatureManagementError.MissingFeatureVariantAssigner, + $"Missing feature variant assigner name for the feature {feature}"); + } + + if (featureDefinition.Variants == null) + { + throw new FeatureManagementException( + FeatureManagementError.MissingFeatureVariant, + $"No variants are registered for the feature {feature}"); + } + + FeatureVariant defaultVariant = null; + + foreach (FeatureVariant v in featureDefinition.Variants) + { + if (v.Default) + { + if (defaultVariant != null) + { + throw new FeatureManagementException( + FeatureManagementError.AmbiguousDefaultFeatureVariant, + $"Multiple default variants are registered for the feature '{feature}'."); + } + + defaultVariant = v; + } + + if (v.ConfigurationReference == null) + { + throw new FeatureManagementException( + FeatureManagementError.MissingConfigurationReference, + $"The variant '{variant.Name}' for the feature '{feature}' does not have a configuration reference."); + } + } + + if (defaultVariant == null) + { + throw new FeatureManagementException( + FeatureManagementError.MissingDefaultFeatureVariant, + $"A default variant cannot be found for the feature '{feature}'."); + } + + IFeatureVariantAssignerMetadata assigner = GetFeatureVariantAssignerMetadata(featureDefinition.Assigner); + + if (assigner == null) + { + throw new FeatureManagementException( + FeatureManagementError.MissingFeatureVariantAssigner, + $"The feature variant assigner '{featureDefinition.Assigner}' specified for feature '{feature}' was not found."); + } + + var context = new FeatureVariantAssignmentContext() + { + FeatureDefinition = featureDefinition + }; + + // + // IFeatureVariantAssigner + if (assigner is IFeatureVariantAssigner featureVariantAssigner) + { + variant = await featureVariantAssigner.AssignVariantAsync(context, cancellationToken).ConfigureAwait(false); + } + // + // IContextualFeatureVariantAssigner + else if (useAppContext && + TryGetContextualFeatureVariantAssigner(featureDefinition.Assigner, typeof(TContext), out ContextualFeatureVariantAssignerEvaluator contextualAssigner)) + { + variant = await contextualAssigner.AssignVariantAsync(context, appContext, cancellationToken).ConfigureAwait(false); + } + // + // The assigner doesn't implement a feature variant assigner interface capable of performing the evaluation + else + { + throw new FeatureManagementException( + FeatureManagementError.InvalidFeatureVariantAssigner, + useAppContext ? + $"The feature variant assigner '{featureDefinition.Assigner}' specified for the feature '{feature}' is not capable of evaluating the requested feature with the provided context." : + $"The feature variant assigner '{featureDefinition.Assigner}' specified for the feature '{feature}' is not capable of evaluating the requested feature."); + } + + if (variant == null) + { + variant = defaultVariant; + } + + return await _variantOptionsResolver.GetOptionsAsync(featureDefinition, variant, cancellationToken).ConfigureAwait(false); + } + + private IFeatureVariantAssignerMetadata GetFeatureVariantAssignerMetadata(string assignerName) + { + const string assignerSuffix = "assigner"; + + IFeatureVariantAssignerMetadata assigner = _assignerMetadataCache.GetOrAdd( + assignerName, + (_) => { + + IEnumerable matchingAssigners = _variantAssigners.Where(a => + { + Type assignerType = a.GetType(); + + string name = ((AssignerAliasAttribute)Attribute.GetCustomAttribute(assignerType, typeof(AssignerAliasAttribute)))?.Alias; + + if (name == null) + { + name = assignerType.Name; + } + + return IsMatchingMetadataName(name, assignerName, assignerSuffix); + }); + + if (matchingAssigners.Count() > 1) + { + throw new FeatureManagementException(FeatureManagementError.AmbiguousFeatureVariantAssigner, $"Multiple feature variant assigners match the configured assigner named '{assignerName}'."); + } + + return matchingAssigners.FirstOrDefault(); + } + ); + + return assigner; + } + + private static bool IsMatchingMetadataName(string metadataName, string desiredName, string suffix) + { + // + // Feature filters can be referenced with or without the 'filter' suffix + // E.g. A feature can reference a filter named 'CustomFilter' as 'Custom' or 'CustomFilter' + if (!desiredName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase) && + metadataName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) + { + metadataName = metadataName.Substring(0, metadataName.Length - suffix.Length); + } + + // + // Feature filters can have namespaces in their alias + // If a feature is configured to use a filter without a namespace such as 'MyFilter', then it can match 'MyOrg.MyProduct.MyFilter' or simply 'MyFilter' + // If a feature is configured to use a filter with a namespace such as 'MyOrg.MyProduct.MyFilter' then it can only match 'MyOrg.MyProduct.MyFilter' + if (desiredName.Contains('.')) + { + // + // The configured filter name is namespaced. It must be an exact match. + return string.Equals(metadataName, desiredName, StringComparison.OrdinalIgnoreCase); + } + else + { + // + // We take the simple name of a filter, E.g. 'MyFilter' for 'MyOrg.MyProduct.MyFilter' + string simpleName = metadataName.Contains('.') ? metadataName.Split('.').Last() : metadataName; + + return string.Equals(simpleName, desiredName, StringComparison.OrdinalIgnoreCase); + } + } + + private bool TryGetContextualFeatureVariantAssigner(string assignerName, Type appContextType, out ContextualFeatureVariantAssignerEvaluator assigner) + { + if (appContextType == null) + { + throw new ArgumentNullException(nameof(appContextType)); + } + + assigner = _contextualFeatureVariantAssignerCache.GetOrAdd( + $"{assignerName}{Environment.NewLine}{appContextType.FullName}", + (_) => { + + IFeatureVariantAssignerMetadata metadata = GetFeatureVariantAssignerMetadata(assignerName); + + return ContextualFeatureVariantAssignerEvaluator.IsContextualVariantAssigner(metadata, appContextType) ? + new ContextualFeatureVariantAssignerEvaluator(metadata, appContextType) : + null; + } + ); + + return assigner != null; + } + } +} diff --git a/src/Microsoft.FeatureManagement/DynamicFeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/DynamicFeatureManagerSnapshot.cs new file mode 100644 index 00000000..517c496f --- /dev/null +++ b/src/Microsoft.FeatureManagement/DynamicFeatureManagerSnapshot.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement +{ + /// + /// Provides a snapshot of feature state to ensure consistency across a given request. + /// + class DynamicFeatureManagerSnapshot : IDynamicFeatureManagerSnapshot + { + private readonly IDynamicFeatureManager _dynamicFeatureManager; + private readonly IDictionary _variantCache = new Dictionary(); + private IEnumerable _dynamicFeatureNames; + + public DynamicFeatureManagerSnapshot(IDynamicFeatureManager dynamicFeatureManager) + { + _dynamicFeatureManager = dynamicFeatureManager ?? throw new ArgumentNullException(nameof(dynamicFeatureManager)); + } + + public async IAsyncEnumerable GetDynamicFeatureNamesAsync([EnumeratorCancellation]CancellationToken cancellationToken = default) + { + if (_dynamicFeatureNames == null) + { + var dynamicFeatureNames = new List(); + + await foreach (string featureName in _dynamicFeatureManager.GetDynamicFeatureNamesAsync(cancellationToken).ConfigureAwait(false)) + { + dynamicFeatureNames.Add(featureName); + } + + _dynamicFeatureNames = dynamicFeatureNames; + } + + foreach (string featureName in _dynamicFeatureNames) + { + yield return featureName; + } + } + + public async ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken) + { + string cacheKey = GetVariantCacheKey(feature); + + // + // First, check local cache + if (_variantCache.ContainsKey(feature)) + { + return (T)_variantCache[cacheKey]; + } + + T variant = await _dynamicFeatureManager.GetVariantAsync(feature, cancellationToken).ConfigureAwait(false); + + _variantCache[cacheKey] = variant; + + return variant; + } + + public async ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken) + { + string cacheKey = GetVariantCacheKey(feature); + + // + // First, check local cache + if (_variantCache.ContainsKey(feature)) + { + return (T)_variantCache[cacheKey]; + } + + T variant = await _dynamicFeatureManager.GetVariantAsync(feature, context, cancellationToken).ConfigureAwait(false); + + _variantCache[cacheKey] = variant; + + return variant; + } + + private string GetVariantCacheKey(string feature) + { + return $"{typeof(T).FullName}\n{feature}"; + } + } +} diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 5edabf55..82f68ac8 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -16,39 +16,29 @@ namespace Microsoft.FeatureManagement /// /// Used to evaluate whether a feature is enabled or disabled. /// - class FeatureManager : IFeatureManager, IDynamicFeatureManager + class FeatureManager : IFeatureManager { - private readonly IFeatureDefinitionProvider _featureDefinitionProvider; + private readonly IFeatureFlagDefinitionProvider _featureDefinitionProvider; private readonly IEnumerable _featureFilters; - private readonly IEnumerable _variantAssigners; - private readonly IFeatureVariantOptionsResolver _variantOptionsResolver; private readonly IEnumerable _sessionManagers; private readonly ILogger _logger; private readonly ConcurrentDictionary _filterMetadataCache; - private readonly ConcurrentDictionary _assignerMetadataCache; private readonly ConcurrentDictionary _contextualFeatureFilterCache; - private readonly ConcurrentDictionary _contextualFeatureVariantAssignerCache; private readonly FeatureManagementOptions _options; public FeatureManager( - IFeatureDefinitionProvider featureDefinitionProvider, + IFeatureFlagDefinitionProvider featureDefinitionProvider, IEnumerable featureFilters, - IEnumerable variantAssigner, - IFeatureVariantOptionsResolver variantOptionsResolver, IEnumerable sessionManagers, ILoggerFactory loggerFactory, IOptions options) { _featureFilters = featureFilters ?? throw new ArgumentNullException(nameof(featureFilters)); - _variantAssigners = variantAssigner ?? throw new ArgumentNullException(nameof(variantAssigner)); - _variantOptionsResolver = variantOptionsResolver ?? throw new ArgumentNullException(nameof(variantOptionsResolver)); _featureDefinitionProvider = featureDefinitionProvider ?? throw new ArgumentNullException(nameof(featureDefinitionProvider)); _sessionManagers = sessionManagers ?? throw new ArgumentNullException(nameof(sessionManagers)); _logger = loggerFactory.CreateLogger(); _filterMetadataCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); _contextualFeatureFilterCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - _assignerMetadataCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - _contextualFeatureVariantAssignerCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); } @@ -70,135 +60,6 @@ public async IAsyncEnumerable GetFeatureFlagNamesAsync([EnumeratorCancel } } - public async IAsyncEnumerable GetDynamicFeatureNamesAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (DynamicFeatureDefinition featureDefintion in _featureDefinitionProvider.GetAllDynamicFeatureDefinitionsAsync(cancellationToken).ConfigureAwait(false)) - { - yield return featureDefintion.Name; - } - } - - public ValueTask GetVariantAsync(string feature, TContext appContext, CancellationToken cancellationToken) - { - return GetVariantAsync(feature, appContext, true, cancellationToken); - } - - public ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken) - { - return GetVariantAsync(feature, null, false, cancellationToken); - } - - private async ValueTask GetVariantAsync(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken) - { - if (feature == null) - { - throw new ArgumentNullException(nameof(feature)); - } - - FeatureVariant variant = null; - - DynamicFeatureDefinition featureDefinition = await _featureDefinitionProvider - .GetDynamicFeatureDefinitionAsync(feature, cancellationToken) - .ConfigureAwait(false); - - if (featureDefinition == null) - { - throw new FeatureManagementException( - FeatureManagementError.MissingFeature, - $"The feature declaration for the dynamic feature '{feature}' was not found."); - } - - if (string.IsNullOrEmpty(featureDefinition.Assigner)) - { - throw new FeatureManagementException( - FeatureManagementError.MissingFeatureVariantAssigner, - $"Missing feature variant assigner name for the feature {feature}"); - } - - if (featureDefinition.Variants == null) - { - throw new FeatureManagementException( - FeatureManagementError.MissingFeatureVariant, - $"No variants are registered for the feature {feature}"); - } - - FeatureVariant defaultVariant = null; - - foreach (FeatureVariant v in featureDefinition.Variants) - { - if (v.Default) - { - if (defaultVariant != null) - { - throw new FeatureManagementException( - FeatureManagementError.AmbiguousDefaultFeatureVariant, - $"Multiple default variants are registered for the feature '{feature}'."); - } - - defaultVariant = v; - } - - if (v.ConfigurationReference == null) - { - throw new FeatureManagementException( - FeatureManagementError.MissingConfigurationReference, - $"The variant '{variant.Name}' for the feature '{feature}' does not have a configuration reference."); - } - } - - if (defaultVariant == null) - { - throw new FeatureManagementException( - FeatureManagementError.MissingDefaultFeatureVariant, - $"A default variant cannot be found for the feature '{feature}'."); - } - - IFeatureVariantAssignerMetadata assigner = GetFeatureVariantAssignerMetadata(featureDefinition.Assigner); - - if (assigner == null) - { - throw new FeatureManagementException( - FeatureManagementError.MissingFeatureVariantAssigner, - $"The feature variant assigner '{featureDefinition.Assigner}' specified for feature '{feature}' was not found."); - } - - var context = new FeatureVariantAssignmentContext() - { - FeatureDefinition = featureDefinition - }; - - // - // IFeatureVariantAssigner - if (assigner is IFeatureVariantAssigner featureVariantAssigner) - { - variant = await featureVariantAssigner.AssignVariantAsync(context, cancellationToken).ConfigureAwait(false); - } - // - // IContextualFeatureVariantAssigner - else if (useAppContext && - TryGetContextualFeatureVariantAssigner(featureDefinition.Assigner, typeof(TContext), out ContextualFeatureVariantAssignerEvaluator contextualAssigner)) - { - variant = await contextualAssigner.AssignVariantAsync(context, appContext, cancellationToken).ConfigureAwait(false); - } - // - // The assigner doesn't implement a feature variant assigner interface capable of performing the evaluation - else - { - throw new FeatureManagementException( - FeatureManagementError.InvalidFeatureVariantAssigner, - useAppContext ? - $"The feature variant assigner '{featureDefinition.Assigner}' specified for the feature '{feature}' is not capable of evaluating the requested feature with the provided context." : - $"The feature variant assigner '{featureDefinition.Assigner}' specified for the feature '{feature}' is not capable of evaluating the requested feature."); - } - - if (variant == null) - { - variant = defaultVariant; - } - - return await _variantOptionsResolver.GetOptionsAsync(featureDefinition, variant, cancellationToken).ConfigureAwait(false); - } - private async Task IsEnabledAsync(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken) { foreach (ISessionManager sessionManager in _sessionManagers) @@ -343,40 +204,6 @@ private IFeatureFilterMetadata GetFeatureFilterMetadata(string filterName) return filter; } - private IFeatureVariantAssignerMetadata GetFeatureVariantAssignerMetadata(string assignerName) - { - const string assignerSuffix = "assigner"; - - IFeatureVariantAssignerMetadata assigner = _assignerMetadataCache.GetOrAdd( - assignerName, - (_) => { - - IEnumerable matchingAssigners = _variantAssigners.Where(a => - { - Type assignerType = a.GetType(); - - string name = ((AssignerAliasAttribute)Attribute.GetCustomAttribute(assignerType, typeof(AssignerAliasAttribute)))?.Alias; - - if (name == null) - { - name = assignerType.Name; - } - - return IsMatchingMetadataName(name, assignerName, assignerSuffix); - }); - - if (matchingAssigners.Count() > 1) - { - throw new FeatureManagementException(FeatureManagementError.AmbiguousFeatureVariantAssigner, $"Multiple feature variant assigners match the configured assigner named '{assignerName}'."); - } - - return matchingAssigners.FirstOrDefault(); - } - ); - - return assigner; - } - private static bool IsMatchingMetadataName(string metadataName, string desiredName, string suffix) { // @@ -429,27 +256,5 @@ private bool TryGetContextualFeatureFilter(string filterName, Type appContextTyp return filter != null; } - - private bool TryGetContextualFeatureVariantAssigner(string assignerName, Type appContextType, out ContextualFeatureVariantAssignerEvaluator assigner) - { - if (appContextType == null) - { - throw new ArgumentNullException(nameof(appContextType)); - } - - assigner = _contextualFeatureVariantAssignerCache.GetOrAdd( - $"{assignerName}{Environment.NewLine}{appContextType.FullName}", - (_) => { - - IFeatureVariantAssignerMetadata metadata = GetFeatureVariantAssignerMetadata(assignerName); - - return ContextualFeatureVariantAssignerEvaluator.IsContextualVariantAssigner(metadata, appContextType) ? - new ContextualFeatureVariantAssignerEvaluator(metadata, appContextType) : - null; - } - ); - - return assigner != null; - } } } diff --git a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs index b39b5b44..2abea404 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs @@ -12,21 +12,15 @@ namespace Microsoft.FeatureManagement /// /// Provides a snapshot of feature state to ensure consistency across a given request. /// - class FeatureManagerSnapshot : IFeatureManagerSnapshot, IDynamicFeatureManagerSnapshot + class FeatureManagerSnapshot : IFeatureManagerSnapshot { private readonly IFeatureManager _featureManager; - private readonly IDynamicFeatureManager _dynamicFeatureManager; private readonly IDictionary _flagCache = new Dictionary(); - private readonly IDictionary _variantCache = new Dictionary(); private IEnumerable _featureFlagNames; - private IEnumerable _dynamicFeatureNames; - public FeatureManagerSnapshot( - IFeatureManager featureManager, - IDynamicFeatureManager dynamicFeatureManager) + public FeatureManagerSnapshot(IFeatureManager featureManager) { _featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager)); - _dynamicFeatureManager = dynamicFeatureManager ?? throw new ArgumentNullException(nameof(dynamicFeatureManager)); } public async IAsyncEnumerable GetFeatureFlagNamesAsync([EnumeratorCancellation]CancellationToken cancellationToken) @@ -49,62 +43,6 @@ public async IAsyncEnumerable GetFeatureFlagNamesAsync([EnumeratorCancel } } - public async IAsyncEnumerable GetDynamicFeatureNamesAsync([EnumeratorCancellation]CancellationToken cancellationToken = default) - { - if (_dynamicFeatureNames == null) - { - var dynamicFeatureNames = new List(); - - await foreach (string featureName in _featureManager.GetFeatureFlagNamesAsync(cancellationToken).ConfigureAwait(false)) - { - dynamicFeatureNames.Add(featureName); - } - - _dynamicFeatureNames = dynamicFeatureNames; - } - - foreach (string featureName in _dynamicFeatureNames) - { - yield return featureName; - } - } - - public async ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken) - { - string cacheKey = GetVariantCacheKey(feature); - - // - // First, check local cache - if (_flagCache.ContainsKey(feature)) - { - return (T)_variantCache[cacheKey]; - } - - T variant = await _dynamicFeatureManager.GetVariantAsync(feature, cancellationToken).ConfigureAwait(false); - - _variantCache[cacheKey] = variant; - - return variant; - } - - public async ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken) - { - string cacheKey = GetVariantCacheKey(feature); - - // - // First, check local cache - if (_flagCache.ContainsKey(feature)) - { - return (T)_variantCache[cacheKey]; - } - - T variant = await _dynamicFeatureManager.GetVariantAsync(feature, cancellationToken).ConfigureAwait(false); - - _variantCache[cacheKey] = variant; - - return variant; - } - public async Task IsEnabledAsync(string feature, CancellationToken cancellationToken) { // @@ -136,10 +74,5 @@ public async Task IsEnabledAsync(string feature, TContext contex return enabled; } - - private string GetVariantCacheKey(string feature) - { - return $"{typeof(T).FullName}\n{feature}"; - } } } diff --git a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAssigner.cs b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAssigner.cs index 26f625ba..9dab47de 100644 --- a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAssigner.cs +++ b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAssigner.cs @@ -19,6 +19,6 @@ public interface IContextualFeatureVariantAssigner : IFeatureVariantAs /// A context defined by the application that is passed in to the feature management system to provide contextual information for assigning a variant of a dynamic feature. /// The cancellation token to cancel the operation. /// The variant that should be assigned for a given dynamic feature. - ValueTask AssignVariantAsync(FeatureVariantAssignmentContext variantAssignmentContext, TContext appContext, CancellationToken cancellationToken); + ValueTask AssignVariantAsync(FeatureVariantAssignmentContext variantAssignmentContext, TContext appContext, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/IDynamicFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/IDynamicFeatureDefinitionProvider.cs new file mode 100644 index 00000000..9f805ce1 --- /dev/null +++ b/src/Microsoft.FeatureManagement/IDynamicFeatureDefinitionProvider.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement +{ + /// + /// A provider of dynamic feature definitions. + /// + public interface IDynamicFeatureDefinitionProvider + { + /// + /// Retrieves the definition for a given dynamic feature. + /// + /// The name of the dynamic feature to retrieve the definition for. + /// The cancellation token to cancel the operation. + /// The dynamic feature's definition. + Task GetDynamicFeatureDefinitionAsync(string dynamicFeatureName, CancellationToken cancellationToken = default); + + /// + /// Retrieves definitions for all dynamic features. + /// + /// The cancellation token to cancel the operation. + /// An enumerator which provides asynchronous iteration over dynamic feature definitions. + IAsyncEnumerable GetAllDynamicFeatureDefinitionsAsync(CancellationToken cancellationToken = default); + } +} diff --git a/src/Microsoft.FeatureManagement/IDynamicFeatureManager.cs b/src/Microsoft.FeatureManagement/IDynamicFeatureManager.cs index 3bfd6719..e1785c08 100644 --- a/src/Microsoft.FeatureManagement/IDynamicFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IDynamicFeatureManager.cs @@ -23,20 +23,20 @@ public interface IDynamicFeatureManager /// Retrieves a typed representation of the feature variant that should be used for a given dynamic feature. /// /// The type that the feature variant's configuration should be bound to. - /// The name of the dynamic feature. + /// The name of the dynamic feature. /// The cancellation token to cancel the operation. /// A typed representation of the feature variant that should be used for a given dynamic feature. - ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken); + ValueTask GetVariantAsync(string dynamicFeature, CancellationToken cancellationToken = default); /// /// Retrieves a typed representation of the feature variant that should be used for a given dynamic feature. /// /// The type that the feature variant's configuration should be bound to. /// The type of the context being provided to the dynamic feature manager for use during the process of choosing which variant to use. - /// The name of the dynamic feature. + /// The name of the dynamic feature. /// A context providing information that can be used to evaluate which variant should be used for the dynamic feature. /// The cancellation token to cancel the operation. /// A typed representation of the feature variant's configuration that should be used for a given feature. - ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken); + ValueTask GetVariantAsync(string dynamicFeature, TContext context, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs deleted file mode 100644 index 69edd73a..00000000 --- a/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.FeatureManagement -{ - /// - /// A provider of feature definitions. - /// - public interface IFeatureDefinitionProvider - { - /// - /// Retrieves the definition for a given feature flag. - /// - /// The name of the feature flag to retrieve the definition for. - /// The cancellation token to cancel the operation. - /// The feature flag's definition. - Task GetFeatureFlagDefinitionAsync(string featureName, CancellationToken cancellationToken = default); - - /// - /// Retrieves definitions for all feature flags. - /// - /// The cancellation token to cancel the operation. - /// An enumerator which provides asynchronous iteration over feature flag definitions. - IAsyncEnumerable GetAllFeatureFlagDefinitionsAsync(CancellationToken cancellationToken = default); - - /// - /// Retrieves the definition for a given dynamic feature. - /// - /// The name of the dynamic feature to retrieve the definition for. - /// The cancellation token to cancel the operation. - /// The dynamic feature's definition. - Task GetDynamicFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken = default); - - /// - /// Retrieves definitions for all dynamic features. - /// - /// The cancellation token to cancel the operation. - /// An enumerator which provides asynchronous iteration over dynamic feature definitions. - IAsyncEnumerable GetAllDynamicFeatureDefinitionsAsync(CancellationToken cancellationToken = default); - } -} diff --git a/src/Microsoft.FeatureManagement/IFeatureFlagDefinitionProvider.cs b/src/Microsoft.FeatureManagement/IFeatureFlagDefinitionProvider.cs new file mode 100644 index 00000000..024e28ad --- /dev/null +++ b/src/Microsoft.FeatureManagement/IFeatureFlagDefinitionProvider.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement +{ + /// + /// A provider of feature flag definitions. + /// + public interface IFeatureFlagDefinitionProvider + { + /// + /// Retrieves the definition for a given feature flag. + /// + /// The name of the feature flag to retrieve the definition for. + /// The cancellation token to cancel the operation. + /// The feature flag's definition. + Task GetFeatureFlagDefinitionAsync(string featureFlagName, CancellationToken cancellationToken = default); + + /// + /// Retrieves definitions for all feature flags. + /// + /// The cancellation token to cancel the operation. + /// An enumerator which provides asynchronous iteration over feature flag definitions. + IAsyncEnumerable GetAllFeatureFlagDefinitionsAsync(CancellationToken cancellationToken = default); + } +} diff --git a/src/Microsoft.FeatureManagement/IFeatureManager.cs b/src/Microsoft.FeatureManagement/IFeatureManager.cs index d9259c7b..ff69fb4e 100644 --- a/src/Microsoft.FeatureManagement/IFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IFeatureManager.cs @@ -22,18 +22,18 @@ public interface IFeatureManager /// /// Checks whether a given feature flag is enabled. /// - /// The name of the feature flag to check. + /// The name of the feature flag to check. /// The cancellation token to cancel the operation. /// True if the feature flag is enabled, otherwise false. - Task IsEnabledAsync(string feature, CancellationToken cancellationToken = default); + Task IsEnabledAsync(string featureFlag, CancellationToken cancellationToken = default); /// /// Checks whether a given feature flag is enabled. /// - /// The name of the feature flag to check. + /// The name of the feature flag to check. /// A context providing information that can be used to evaluate whether a feature flag should be on or off. /// The cancellation token to cancel the operation. /// True if the feature flag is enabled, otherwise false. - Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken = default); + Task IsEnabledAsync(string featureFlag, TContext context, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantAssigner.cs b/src/Microsoft.FeatureManagement/IFeatureVariantAssigner.cs index ce6de1fa..cae12197 100644 --- a/src/Microsoft.FeatureManagement/IFeatureVariantAssigner.cs +++ b/src/Microsoft.FeatureManagement/IFeatureVariantAssigner.cs @@ -17,6 +17,6 @@ public interface IFeatureVariantAssigner : IFeatureVariantAssignerMetadata /// A variant assignment context that contains information needed to assign a variant for a dynamic feature. /// The cancellation token to cancel the operation. /// The variant that should be assigned for a given dynamic feature. - ValueTask AssignVariantAsync(FeatureVariantAssignmentContext variantAssignmentContext, CancellationToken cancellationToken); + ValueTask AssignVariantAsync(FeatureVariantAssignmentContext variantAssignmentContext, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs b/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs index ecf15bdb..760c9358 100644 --- a/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs +++ b/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs @@ -19,6 +19,6 @@ public interface IFeatureVariantOptionsResolver /// The chosen variant of the dynamic feature. /// The cancellation token to cancel the operation. /// Typed options for a given dynamic feature definition and chosen variant. - ValueTask GetOptionsAsync(DynamicFeatureDefinition featureDefinition, FeatureVariant variant, CancellationToken cancellationToken); + ValueTask GetOptionsAsync(DynamicFeatureDefinition featureDefinition, FeatureVariant variant, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index a57dbba4..fdb94aaa 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -24,21 +24,21 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec // // Add required services - services.TryAddSingleton(); + services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(); - services.AddSingleton(); + services.TryAddSingleton(); - services.TryAddSingleton(sp => sp.GetRequiredService()); + services.TryAddSingleton(); - services.TryAddSingleton(sp => sp.GetRequiredService()); + services.TryAddSingleton(); services.TryAddSingleton(); - services.AddScoped(); + services.TryAddScoped(); - services.TryAddScoped(sp => sp.GetRequiredService()); + services.TryAddScoped(); return new FeatureManagementBuilder(services); } @@ -56,7 +56,9 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec throw new ArgumentNullException(nameof(configuration)); } - services.AddSingleton(new ConfigurationFeatureDefinitionProvider(configuration)); + services.AddSingleton(new ConfigurationFeatureFlagDefinitionProvider(configuration)); + + services.AddSingleton(new ConfigurationDynamicFeatureDefinitionProvider(configuration)); return services.AddFeatureManagement(); } diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index 0a1f4719..4f14944f 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -508,7 +508,7 @@ public async Task AccumulatesAudience() IDynamicFeatureManager variantManager = serviceProvider.GetRequiredService(); - IFeatureDefinitionProvider featureProvider = serviceProvider.GetRequiredService(); + IFeatureFlagDefinitionProvider featureProvider = serviceProvider.GetRequiredService(); var occurences = new Dictionary(); @@ -915,16 +915,18 @@ public async Task CustomFeatureDefinitionProvider() var services = new ServiceCollection(); - services.AddSingleton( - new InMemoryFeatureDefinitionProvider( - new FeatureFlagDefinition[] - { - testFeature - }, - new DynamicFeatureDefinition[] - { - dynamicFeature - })) + var definitionProvider = new InMemoryFeatureDefinitionProvider( + new FeatureFlagDefinition[] + { + testFeature + }, + new DynamicFeatureDefinition[] + { + dynamicFeature + }); + + services.AddSingleton(definitionProvider) + .AddSingleton(definitionProvider) .AddSingleton(new ConfigurationBuilder().Build()) .AddFeatureManagement() .AddFeatureFilter() diff --git a/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs b/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs index cb03024a..01e347ab 100644 --- a/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs +++ b/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs @@ -11,7 +11,7 @@ namespace Tests.FeatureManagement { - class InMemoryFeatureDefinitionProvider : IFeatureDefinitionProvider + class InMemoryFeatureDefinitionProvider : IFeatureFlagDefinitionProvider, IDynamicFeatureDefinitionProvider { private IEnumerable _featureFlagDefinitions; private IEnumerable _dynamicFeatureDefinitions; From ba664ee550b4d1d1068eba7713a159bad3516879 Mon Sep 17 00:00:00 2001 From: Jimmy Campbell Date: Mon, 10 Jan 2022 09:38:51 -0800 Subject: [PATCH 3/4] Use shared helper for filter/assigner reference matching. --- .../DynamicFeatureManager.cs | 36 ++---------- .../FeatureManager.cs | 36 ++---------- src/Microsoft.FeatureManagement/NameHelper.cs | 56 +++++++++++++++++++ 3 files changed, 64 insertions(+), 64 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/NameHelper.cs diff --git a/src/Microsoft.FeatureManagement/DynamicFeatureManager.cs b/src/Microsoft.FeatureManagement/DynamicFeatureManager.cs index e9d9252a..4ac7dcd1 100644 --- a/src/Microsoft.FeatureManagement/DynamicFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/DynamicFeatureManager.cs @@ -182,7 +182,10 @@ private IFeatureVariantAssignerMetadata GetFeatureVariantAssignerMetadata(string name = assignerType.Name; } - return IsMatchingMetadataName(name, assignerName, assignerSuffix); + return NameHelper.IsMatchingReference( + reference: assignerName, + metadataName: name, + suffix: assignerSuffix); }); if (matchingAssigners.Count() > 1) @@ -197,37 +200,6 @@ private IFeatureVariantAssignerMetadata GetFeatureVariantAssignerMetadata(string return assigner; } - private static bool IsMatchingMetadataName(string metadataName, string desiredName, string suffix) - { - // - // Feature filters can be referenced with or without the 'filter' suffix - // E.g. A feature can reference a filter named 'CustomFilter' as 'Custom' or 'CustomFilter' - if (!desiredName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase) && - metadataName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) - { - metadataName = metadataName.Substring(0, metadataName.Length - suffix.Length); - } - - // - // Feature filters can have namespaces in their alias - // If a feature is configured to use a filter without a namespace such as 'MyFilter', then it can match 'MyOrg.MyProduct.MyFilter' or simply 'MyFilter' - // If a feature is configured to use a filter with a namespace such as 'MyOrg.MyProduct.MyFilter' then it can only match 'MyOrg.MyProduct.MyFilter' - if (desiredName.Contains('.')) - { - // - // The configured filter name is namespaced. It must be an exact match. - return string.Equals(metadataName, desiredName, StringComparison.OrdinalIgnoreCase); - } - else - { - // - // We take the simple name of a filter, E.g. 'MyFilter' for 'MyOrg.MyProduct.MyFilter' - string simpleName = metadataName.Contains('.') ? metadataName.Split('.').Last() : metadataName; - - return string.Equals(simpleName, desiredName, StringComparison.OrdinalIgnoreCase); - } - } - private bool TryGetContextualFeatureVariantAssigner(string assignerName, Type appContextType, out ContextualFeatureVariantAssignerEvaluator assigner) { if (appContextType == null) diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 82f68ac8..b119d4f8 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -189,7 +189,10 @@ private IFeatureFilterMetadata GetFeatureFilterMetadata(string filterName) name = filterType.Name; } - return IsMatchingMetadataName(name, filterName, filterSuffix); + return NameHelper.IsMatchingReference( + reference: filterName, + metadataName: name, + suffix: filterSuffix); }); if (matchingFilters.Count() > 1) @@ -204,37 +207,6 @@ private IFeatureFilterMetadata GetFeatureFilterMetadata(string filterName) return filter; } - private static bool IsMatchingMetadataName(string metadataName, string desiredName, string suffix) - { - // - // Feature filters can be referenced with or without the 'filter' suffix - // E.g. A feature can reference a filter named 'CustomFilter' as 'Custom' or 'CustomFilter' - if (!desiredName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase) && - metadataName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) - { - metadataName = metadataName.Substring(0, metadataName.Length - suffix.Length); - } - - // - // Feature filters can have namespaces in their alias - // If a feature is configured to use a filter without a namespace such as 'MyFilter', then it can match 'MyOrg.MyProduct.MyFilter' or simply 'MyFilter' - // If a feature is configured to use a filter with a namespace such as 'MyOrg.MyProduct.MyFilter' then it can only match 'MyOrg.MyProduct.MyFilter' - if (desiredName.Contains('.')) - { - // - // The configured filter name is namespaced. It must be an exact match. - return string.Equals(metadataName, desiredName, StringComparison.OrdinalIgnoreCase); - } - else - { - // - // We take the simple name of a filter, E.g. 'MyFilter' for 'MyOrg.MyProduct.MyFilter' - string simpleName = metadataName.Contains('.') ? metadataName.Split('.').Last() : metadataName; - - return string.Equals(simpleName, desiredName, StringComparison.OrdinalIgnoreCase); - } - } - private bool TryGetContextualFeatureFilter(string filterName, Type appContextType, out ContextualFeatureFilterEvaluator filter) { if (appContextType == null) diff --git a/src/Microsoft.FeatureManagement/NameHelper.cs b/src/Microsoft.FeatureManagement/NameHelper.cs new file mode 100644 index 00000000..72b2a748 --- /dev/null +++ b/src/Microsoft.FeatureManagement/NameHelper.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; + +namespace Microsoft.FeatureManagement +{ + static class NameHelper + { + /// + /// Evaluates whether a feature filter or feature variant assigner reference matches a given feature filter/assigner name. + /// + /// A reference to some feature metadata that should be checked for a match with the provided metadata name + /// The name used by the feature filter/feature variant assigner + /// An optional suffix that may be included when referencing the metadata type. E.g. "filter" or "assigner". + /// True if the reference is a match for the metadata name. False otherwise. + public static bool IsMatchingReference(string reference, string metadataName, string suffix) + { + if (string.IsNullOrEmpty(reference)) + { + throw new ArgumentNullException(nameof(reference)); + } + + if (string.IsNullOrEmpty(metadataName)) + { + throw new ArgumentNullException(nameof(metadataName)); + } + + // + // Feature filters/assigner can be referenced with or without their associated suffix ('filter' or 'assigner') + // E.g. A feature can reference a filter named 'CustomFilter' as 'Custom' or 'CustomFilter' + if (!reference.EndsWith(suffix, StringComparison.OrdinalIgnoreCase) && + metadataName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) + { + metadataName = metadataName.Substring(0, metadataName.Length - suffix.Length); + } + + // + // Feature filters/assigners can have namespaces in their alias + // If a feature is configured to use a filter without a namespace such as 'MyFilter', then it can match 'MyOrg.MyProduct.MyFilter' or simply 'MyFilter' + // If a feature is configured to use a filter with a namespace such as 'MyOrg.MyProduct.MyFilter' then it can only match 'MyOrg.MyProduct.MyFilter' + if (reference.Contains('.')) + { + // + // The configured metadata name is namespaced. It must be an exact match. + return string.Equals(metadataName, reference, StringComparison.OrdinalIgnoreCase); + } + else + { + // + // We take the simple name of the metadata, E.g. 'MyFilter' for a feature filter named 'MyOrg.MyProduct.MyFilter' + string simpleName = metadataName.Contains('.') ? metadataName.Split('.').Last() : metadataName; + + return string.Equals(simpleName, reference, StringComparison.OrdinalIgnoreCase); + } + } + } +} From 6cac5335e35611e3b42778eefac43de04342742e Mon Sep 17 00:00:00 2001 From: Jimmy Campbell Date: Mon, 10 Jan 2022 11:00:00 -0800 Subject: [PATCH 4/4] Add copyright header. --- src/Microsoft.FeatureManagement/NameHelper.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement/NameHelper.cs b/src/Microsoft.FeatureManagement/NameHelper.cs index 72b2a748..135deabe 100644 --- a/src/Microsoft.FeatureManagement/NameHelper.cs +++ b/src/Microsoft.FeatureManagement/NameHelper.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System; using System.Linq; namespace Microsoft.FeatureManagement