diff --git a/examples/CustomAssignmentConsoleApp/Program.cs b/examples/CustomAssignmentConsoleApp/Program.cs index c1f91629..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()) { - IFeatureVariantManager variantManager = serviceProvider.GetRequiredService(); + IDynamicFeatureManager dynamicFeatureManager = serviceProvider.GetRequiredService(); - DailyDiscountOptions discountOptions = await variantManager + DailyDiscountOptions discountOptions = await dynamicFeatureManager .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..f8a5d76b 100644 --- a/examples/FeatureFlagDemo/Views/Shared/_Layout.cshtml +++ b/examples/FeatureFlagDemo/Views/Shared/_Layout.cshtml @@ -1,7 +1,7 @@ @using Microsoft.FeatureManagement -@inject IFeatureVariantManager variantManager; +@inject IDynamicFeatureManager dynamicFeatureManager; @{ - DiscountBannerOptions opts = await variantManager.GetVariantAsync("DiscountBanner", Context.RequestAborted); + DiscountBannerOptions opts = await dynamicFeatureManager.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..ca4efd46 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 dynamicFeatureManager = serviceProvider.GetRequiredService(); // // We'll simulate a task to run on behalf of each known user @@ -76,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 59% rename from src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs rename to src/Microsoft.FeatureManagement/ConfigurationFeatureFlagDefinitionProvider.cs index 2c8fe03d..d302cff2 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureFlagDefinitionProvider.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; @@ -14,21 +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 FeatureFiltersSectionName = "EnabledFor"; - private const string FeatureVariantsSectionName = "Variants"; private readonly IConfiguration _configuration; - private readonly ConcurrentDictionary _definitions; + private readonly ConcurrentDictionary _featureFlagDefinitions; private IDisposable _changeSubscription; private int _stale = 0; - public ConfigurationFeatureDefinitionProvider(IConfiguration configuration) + public ConfigurationFeatureFlagDefinitionProvider(IConfiguration configuration) { _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - _definitions = new ConcurrentDictionary(); + _featureFlagDefinitions = new ConcurrentDictionary(); _changeSubscription = ChangeToken.OnChange( () => _configuration.GetReloadToken(), @@ -42,21 +44,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 +64,24 @@ 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) - { - _definitions.Clear(); - } + EnsureFresh(); // // Iterate over all features registered in the system at initial invocation time - foreach (IConfigurationSection featureSection in GetFeatureDefinitionSections()) + foreach (IConfigurationSection featureSection in GetFeatureFlagDefinitionSections()) { // // Underlying IConfigurationSection data is dynamic so latest feature definitions are returned - yield return _definitions.GetOrAdd(featureSection.Key, (_) => ReadFeatureDefinition(featureSection)); + yield return _featureFlagDefinitions.GetOrAdd(featureSection.Key, (_) => ReadFeatureFlagDefinition(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 +89,10 @@ private FeatureDefinition ReadFeatureDefinition(string featureName) return null; } - return ReadFeatureDefinition(configuration); + return ReadFeatureFlagDefinition(configuration); } - private FeatureDefinition ReadFeatureDefinition(IConfigurationSection configurationSection) + private FeatureFlagDefinition ReadFeatureFlagDefinition(IConfigurationSection configurationSection) { /* @@ -127,11 +123,9 @@ We support */ - var enabledFor = new List(); - - var variants = new List(); + Debug.Assert(configurationSection != null); - string assigner = null; + var enabledFor = new List(); string val = configurationSection.Value; // configuration[$"{featureName}"]; @@ -162,7 +156,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 +165,39 @@ We support }); } } - - IEnumerable variantSections = configurationSection.GetSection(FeatureVariantsSectionName).GetChildren(); - - foreach (IConfigurationSection section in variantSections) - { - if (int.TryParse(section.Key, out int i) && !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)) - }); - } - } - - assigner = configurationSection.GetValue(nameof(FeatureDefinition.Assigner)); } - return new FeatureDefinition() + return new FeatureFlagDefinition() { Name = configurationSection.Key, EnabledFor = enabledFor, - Variants = variants, - Assigner = assigner }; } - private IEnumerable GetFeatureDefinitionSections() + private IEnumerable GetFeatureFlagDefinitionSections() { - const string FeatureManagementSectionName = "FeatureManagement"; + // + // Look for feature definitions under the "FeatureManagement" section + IConfiguration featureManagementSection = _configuration.GetChildren().Any(s => s.Key.Equals(FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase)) ? + _configuration.GetSection(FeatureManagementSectionName) : + _configuration; - 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 + IEnumerable featureManagementChildren = featureManagementSection.GetChildren(); + + 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 void EnsureFresh() + { + if (Interlocked.Exchange(ref _stale, 0) != 0) { - return _configuration.GetChildren(); + _featureFlagDefinitions.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/DynamicFeatureManager.cs b/src/Microsoft.FeatureManagement/DynamicFeatureManager.cs new file mode 100644 index 00000000..4ac7dcd1 --- /dev/null +++ b/src/Microsoft.FeatureManagement/DynamicFeatureManager.cs @@ -0,0 +1,225 @@ +// 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 NameHelper.IsMatchingReference( + reference: assignerName, + metadataName: name, + suffix: 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 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/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..b119d4f8 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,39 +16,29 @@ namespace Microsoft.FeatureManagement /// /// Used to evaluate whether a feature is enabled or disabled. /// - class FeatureManager : IFeatureManager, IFeatureVariantManager + 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)); } @@ -63,135 +52,14 @@ 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 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; - - FeatureDefinition featureDefinition = await _featureDefinitionProvider - .GetFeatureDefinitionAsync(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) @@ -206,7 +74,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) { @@ -321,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) @@ -336,71 +207,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) - { - // - // 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) @@ -422,27 +228,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 46549f4d..2abea404 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs @@ -16,28 +16,28 @@ class FeatureManagerSnapshot : IFeatureManagerSnapshot { private readonly IFeatureManager _featureManager; private readonly IDictionary _flagCache = new Dictionary(); - private IEnumerable _featureNames; + private IEnumerable _featureFlagNames; public FeatureManagerSnapshot(IFeatureManager featureManager) { _featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager)); } - 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; } 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..9dab47de 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. - ValueTask AssignVariantAsync(FeatureVariantAssignmentContext variantAssignmentContext, TContext appContext, CancellationToken cancellationToken); + /// The variant that should be assigned for a given dynamic feature. + 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 new file mode 100644 index 00000000..e1785c08 --- /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 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. + /// 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 dynamicFeature, TContext context, CancellationToken cancellationToken = default); + } +} 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 deleted file mode 100644 index c8c37494..00000000 --- a/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs +++ /dev/null @@ -1,30 +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. - /// - /// The name of the feature to retrieve the definition for. - /// The cancellation token to cancel the operation. - /// The feature's definition. - Task GetFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken = default); - - /// - /// Retrieves definitions for all features. - /// - /// The cancellation token to cancel the operation. - /// An enumerator which provides asynchronous iteration over feature definitions. - IAsyncEnumerable GetAllFeatureDefinitionsAsync(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 53231f89..ff69fb4e 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. - Task IsEnabledAsync(string feature, CancellationToken cancellationToken = default); + /// True if the feature flag is enabled, otherwise false. + Task IsEnabledAsync(string featureFlag, 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. - Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken = default); + /// True if the feature flag is enabled, otherwise false. + Task IsEnabledAsync(string featureFlag, TContext context, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantAssigner.cs b/src/Microsoft.FeatureManagement/IFeatureVariantAssigner.cs index ee6e1627..cae12197 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. - ValueTask AssignVariantAsync(FeatureVariantAssignmentContext variantAssignmentContext, CancellationToken cancellationToken); + /// The variant that should be assigned for a given dynamic feature. + ValueTask AssignVariantAsync(FeatureVariantAssignmentContext variantAssignmentContext, CancellationToken cancellationToken = default); } } 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..760c9358 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 = default); } } diff --git a/src/Microsoft.FeatureManagement/NameHelper.cs b/src/Microsoft.FeatureManagement/NameHelper.cs new file mode 100644 index 00000000..135deabe --- /dev/null +++ b/src/Microsoft.FeatureManagement/NameHelper.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +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); + } + } + } +} diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 233413d1..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/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..4f14944f 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,9 +506,9 @@ public async Task AccumulatesAudience() ServiceProvider serviceProvider = services.BuildServiceProvider(); - IFeatureVariantManager variantManager = serviceProvider.GetRequiredService(); + IDynamicFeatureManager variantManager = serviceProvider.GetRequiredService(); - IFeatureDefinitionProvider featureProvider = serviceProvider.GetRequiredService(); + IFeatureFlagDefinitionProvider featureProvider = serviceProvider.GetRequiredService(); var occurences = new Dictionary(); @@ -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,58 @@ 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 })) + var definitionProvider = new InMemoryFeatureDefinitionProvider( + new FeatureFlagDefinition[] + { + testFeature + }, + new DynamicFeatureDefinition[] + { + dynamicFeature + }); + + services.AddSingleton(definitionProvider) + .AddSingleton(definitionProvider) .AddSingleton(new ConfigurationBuilder().Build()) .AddFeatureManagement() - .AddFeatureFilter(); + .AddFeatureFilter() + .AddFeatureVariantAssigner(); ServiceProvider serviceProvider = services.BuildServiceProvider(); @@ -849,6 +958,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..01e347ab 100644 --- a/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs +++ b/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs @@ -11,28 +11,47 @@ namespace Tests.FeatureManagement { - class InMemoryFeatureDefinitionProvider : IFeatureDefinitionProvider + class InMemoryFeatureDefinitionProvider : IFeatureFlagDefinitionProvider, IDynamicFeatureDefinitionProvider { - 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" + } + } + ] + } + } +}