From d28bf88d25587ed588996b10d7ce419bdcae3d0f Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 3 Jul 2023 14:35:57 -0500 Subject: [PATCH 01/80] in progress, add new classes for variants and define methods in featuremanager --- src/Microsoft.FeatureManagement/Allocation.cs | 29 ++++++++++++ .../FeatureDefinition.cs | 15 +++++++ .../FeatureManager.cs | 19 +++++++- .../FeatureVariant.cs | 32 +++++++++++++ .../IFeatureManager.cs | 7 ++- .../IVariantFeatureManager.cs | 45 +++++++++++++++++++ src/Microsoft.FeatureManagement/Status.cs | 11 +++++ .../StatusOverride.cs | 11 +++++ src/Microsoft.FeatureManagement/Variant.cs | 14 ++++++ 9 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/Allocation.cs create mode 100644 src/Microsoft.FeatureManagement/FeatureVariant.cs create mode 100644 src/Microsoft.FeatureManagement/IVariantFeatureManager.cs create mode 100644 src/Microsoft.FeatureManagement/Status.cs create mode 100644 src/Microsoft.FeatureManagement/StatusOverride.cs create mode 100644 src/Microsoft.FeatureManagement/Variant.cs diff --git a/src/Microsoft.FeatureManagement/Allocation.cs b/src/Microsoft.FeatureManagement/Allocation.cs new file mode 100644 index 00000000..90929c83 --- /dev/null +++ b/src/Microsoft.FeatureManagement/Allocation.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.FeatureManagement.FeatureFilters; +using System.Collections.Generic; + +namespace Microsoft.FeatureManagement +{ + /// + /// + /// + public class Allocation + { + /// + /// Describes a mapping of user id to variant. + /// + public List Users { get; set; } + + /// + /// Describes a mapping of group names to variants. + /// + public List Groups { get; set; } + + /// + /// Allocate a percentage of the user base to variants. + /// + public double Percentile { get; set; } + } +} diff --git a/src/Microsoft.FeatureManagement/FeatureDefinition.cs b/src/Microsoft.FeatureManagement/FeatureDefinition.cs index 6736314f..a86c03a9 100644 --- a/src/Microsoft.FeatureManagement/FeatureDefinition.cs +++ b/src/Microsoft.FeatureManagement/FeatureDefinition.cs @@ -25,5 +25,20 @@ public class FeatureDefinition /// The default value is . /// public RequirementType RequirementType { get; set; } = RequirementType.Any; + + /// + /// kill switch + /// + public Status Status { get; set; } = Status.Conditional; + + /// + /// + /// + public Allocation Allocation { get; set; }; + + /// + /// + /// + public List Variants { get; set; } } } diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 17b94c47..157cba02 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -16,7 +16,7 @@ namespace Microsoft.FeatureManagement /// /// Used to evaluate whether a feature is enabled or disabled. /// - class FeatureManager : IFeatureManager, IDisposable + class FeatureManager : IFeatureManager, IDisposable, IVariantFeatureManager { private readonly TimeSpan ParametersCacheSlidingExpiration = TimeSpan.FromMinutes(5); private readonly TimeSpan ParametersCacheAbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1); @@ -141,6 +141,13 @@ private async Task IsEnabledAsync(string feature, TContext appCo continue; } + // + // Handle On filters for variants + if (string.Equals(featureFilterConfiguration.Name, "On", StringComparison.OrdinalIgnoreCase)) + { + // TODO + } + IFeatureFilterMetadata filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name); if (filter == null) @@ -217,6 +224,16 @@ await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) return enabled; } + public ValueTask GetVariantAsync(string feature) + { + throw new NotImplementedException(); + } + + public ValueTask GetVariantAsync(string feature, TContext context) + { + throw new NotImplementedException(); + } + private void BindSettings(IFeatureFilterMetadata filter, FeatureFilterEvaluationContext context, int filterIndex) { IFilterParametersBinder binder = filter as IFilterParametersBinder; diff --git a/src/Microsoft.FeatureManagement/FeatureVariant.cs b/src/Microsoft.FeatureManagement/FeatureVariant.cs new file mode 100644 index 00000000..6274bba0 --- /dev/null +++ b/src/Microsoft.FeatureManagement/FeatureVariant.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// + +namespace Microsoft.FeatureManagement +{ + /// + /// A variant of a feature. + /// + public class FeatureVariant + { + /// + /// The name of the variant. + /// + public string Name { get; set; } + + /// + /// The value of the configuration for this variant of the feature. + /// + public string ConfigurationValue { get; set; } + + /// + /// A reference pointing to the configuration for this variant of the feature. + /// + public string ConfigurationReference { get; set; } + + /// + /// + /// + public StatusOverride StatusOverride { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/IFeatureManager.cs b/src/Microsoft.FeatureManagement/IFeatureManager.cs index 1b4ea0cf..a0602cba 100644 --- a/src/Microsoft.FeatureManagement/IFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IFeatureManager.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. // using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement @@ -21,15 +22,17 @@ public interface IFeatureManager /// Checks whether a given feature is enabled. /// /// The name of the feature to check. + /// The cancellation token to cancel the operation. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature); + Task IsEnabledAsync(string feature, CancellationToken cancellationToken = default); /// /// Checks whether a given feature 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 cancellation token to cancel the operation. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature, TContext context); + Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs new file mode 100644 index 00000000..20d4f657 --- /dev/null +++ b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System.Threading.Tasks; +using System.Threading; + +namespace Microsoft.FeatureManagement +{ + public interface IVariantFeatureManager + { + /// + /// Checks whether a given feature is enabled. + /// + /// The name of the feature to check. + /// The cancellation token to cancel the operation. + /// True if the feature is enabled, otherwise false. + Task IsEnabledAsync(string feature, CancellationToken cancellationToken); + + /// + /// Checks whether a given feature 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 cancellation token to cancel the operation. + /// True if the feature is enabled, otherwise false. + Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken); + + /// + /// Gets the assigned variant for a specfic feature. + /// + /// The name of the feature from which the variant will be assigned. + /// The cancellation token to cancel the operation. + /// A variant assigned to the user based on the feature's allocation logic. + ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken); + + /// + /// Gets the assigned variant for a specfic feature. + /// + /// The name of the feature from which the variant will be assigned. + /// A context providing information that can be used to evaluate which variant the user will be assigned. + /// The cancellation token to cancel the operation. + /// A variant assigned to the user based on the feature's allocation logic. + ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.FeatureManagement/Status.cs b/src/Microsoft.FeatureManagement/Status.cs new file mode 100644 index 00000000..e19740da --- /dev/null +++ b/src/Microsoft.FeatureManagement/Status.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +namespace Microsoft.FeatureManagement +{ + public enum Status + { + Conditional, + Disabled + } +} diff --git a/src/Microsoft.FeatureManagement/StatusOverride.cs b/src/Microsoft.FeatureManagement/StatusOverride.cs new file mode 100644 index 00000000..17481b58 --- /dev/null +++ b/src/Microsoft.FeatureManagement/StatusOverride.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +namespace Microsoft.FeatureManagement +{ + public enum StatusOverride + { + Enabled, + Disabled + } +} diff --git a/src/Microsoft.FeatureManagement/Variant.cs b/src/Microsoft.FeatureManagement/Variant.cs new file mode 100644 index 00000000..a7397b6e --- /dev/null +++ b/src/Microsoft.FeatureManagement/Variant.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.Extensions.Configuration; + +namespace Microsoft.FeatureManagement +{ + public class Variant + { + public string Name { get; set; } + + public IConfiguration Configuration { get; set; } + } +} From c1451d30fed09ad8e4c33e88ab5f0d7792940f6c Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 3 Jul 2023 14:40:16 -0500 Subject: [PATCH 02/80] Revert "Revert "Add cancellation token parameter to async feature management interfaces. (#131)" (#139)" This reverts commit e53186327fde43423ab91374a107800a17113509. --- .../FeatureFilters/AccountIdFilter.cs | 3 +- examples/ConsoleApp/Program.cs | 3 +- examples/FeatureFlagDemo/BrowserFilter.cs | 3 +- .../Controllers/HomeController.cs | 5 ++- .../HttpContextTargetingContextAccessor.cs | 3 +- examples/FeatureFlagDemo/SuperUserFilter.cs | 3 +- examples/TargetingConsoleApp/Program.cs | 3 +- .../FeatureGateAttribute.cs | 4 +- .../FeatureGatedAsyncActionFilter.cs | 2 +- .../TagHelpers/FeatureTagHelper.cs | 5 ++- .../UseForFeatureExtensions.cs | 2 +- .../ConfigurationFeatureDefinitionProvider.cs | 5 ++- .../ContextualFeatureFilterEvaluator.cs | 30 ++++++++----- .../EmptySessionManager.cs | 5 ++- .../FeatureFilters/PercentageFilter.cs | 4 +- .../FeatureFilters/TimeWindowFilter.cs | 4 +- .../FeatureManager.cs | 28 +++++++----- .../FeatureManagerSnapshot.cs | 8 ++-- .../IContextualFeatureFilter.cs | 4 +- .../IFeatureDefinitionProvider.cs | 7 ++- .../IFeatureFilter.cs | 4 +- .../IFeatureManager.cs | 7 +-- .../ISessionManager.cs | 7 ++- .../Targeting/ContextualTargetingFilter.cs | 4 +- .../Targeting/ITargetingContextAccessor.cs | 4 +- .../Targeting/TargetingFilter.cs | 8 ++-- .../ContextualTestFilter.cs | 3 +- .../FeatureManagement.cs | 44 ++++++++++--------- .../InMemoryFeatureDefinitionProvider.cs | 6 ++- .../InvalidFeatureFilter.cs | 9 ++-- .../OnDemandTargetingContextAccessor.cs | 3 +- tests/Tests.FeatureManagement/TestFilter.cs | 3 +- 32 files changed, 144 insertions(+), 89 deletions(-) diff --git a/examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs b/examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs index 1fd56d4a..3d236eed 100644 --- a/examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs +++ b/examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs @@ -6,6 +6,7 @@ using Microsoft.FeatureManagement; using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Consoto.Banking.AccountService.FeatureManagement @@ -17,7 +18,7 @@ namespace Consoto.Banking.AccountService.FeatureManagement [FilterAlias("AccountId")] class AccountIdFilter : IContextualFeatureFilter { - public Task EvaluateAsync(FeatureFilterEvaluationContext featureEvaluationContext, IAccountContext accountContext) + public Task EvaluateAsync(FeatureFilterEvaluationContext featureEvaluationContext, IAccountContext accountContext, CancellationToken _) { if (string.IsNullOrEmpty(accountContext?.AccountId)) { diff --git a/examples/ConsoleApp/Program.cs b/examples/ConsoleApp/Program.cs index e0ab1021..afe4c52f 100644 --- a/examples/ConsoleApp/Program.cs +++ b/examples/ConsoleApp/Program.cs @@ -8,6 +8,7 @@ using Microsoft.FeatureManagement.FeatureFilters; using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Consoto.Banking.AccountService @@ -58,7 +59,7 @@ public static async Task Main(string[] args) AccountId = account }; - bool enabled = await featureManager.IsEnabledAsync(FeatureName, accountServiceContext); + bool enabled = await featureManager.IsEnabledAsync(FeatureName, accountServiceContext, CancellationToken.None); // // Output results diff --git a/examples/FeatureFlagDemo/BrowserFilter.cs b/examples/FeatureFlagDemo/BrowserFilter.cs index efeb8e70..e33f58c7 100644 --- a/examples/FeatureFlagDemo/BrowserFilter.cs +++ b/examples/FeatureFlagDemo/BrowserFilter.cs @@ -6,6 +6,7 @@ using Microsoft.FeatureManagement; using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace FeatureFlagDemo.FeatureManagement.FeatureFilters @@ -23,7 +24,7 @@ public BrowserFilter(IHttpContextAccessor httpContextAccessor) _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } - public Task EvaluateAsync(FeatureFilterEvaluationContext context) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) { BrowserFilterSettings settings = context.Parameters.Get() ?? new BrowserFilterSettings(); diff --git a/examples/FeatureFlagDemo/Controllers/HomeController.cs b/examples/FeatureFlagDemo/Controllers/HomeController.cs index c4363967..a939a88e 100644 --- a/examples/FeatureFlagDemo/Controllers/HomeController.cs +++ b/examples/FeatureFlagDemo/Controllers/HomeController.cs @@ -8,6 +8,7 @@ using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.Mvc; using System.Threading.Tasks; +using System.Threading; namespace FeatureFlagDemo.Controllers { @@ -26,11 +27,11 @@ public IActionResult Index() return View(); } - public async Task About() + public async Task About(CancellationToken cancellationToken) { ViewData["Message"] = "Your application description page."; - if (await _featureManager.IsEnabledAsync(nameof(MyFeatureFlags.CustomViewData))) + if (await _featureManager.IsEnabledAsync(nameof(MyFeatureFlags.CustomViewData), cancellationToken)) { ViewData["Message"] = $"This is FANCY CONTENT you can see only if '{nameof(MyFeatureFlags.CustomViewData)}' is enabled."; }; diff --git a/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs b/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs index 9f9c8964..931be833 100644 --- a/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs +++ b/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Security.Claims; +using System.Threading; using System.Threading.Tasks; namespace FeatureFlagDemo @@ -23,7 +24,7 @@ public HttpContextTargetingContextAccessor(IHttpContextAccessor httpContextAcces _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } - public ValueTask GetContextAsync() + public ValueTask GetContextAsync(CancellationToken _) { HttpContext httpContext = _httpContextAccessor.HttpContext; diff --git a/examples/FeatureFlagDemo/SuperUserFilter.cs b/examples/FeatureFlagDemo/SuperUserFilter.cs index 25dc8e5f..1e188704 100644 --- a/examples/FeatureFlagDemo/SuperUserFilter.cs +++ b/examples/FeatureFlagDemo/SuperUserFilter.cs @@ -2,13 +2,14 @@ // Licensed under the MIT license. // using Microsoft.FeatureManagement; +using System.Threading; using System.Threading.Tasks; namespace FeatureFlagDemo.FeatureManagement.FeatureFilters { public class SuperUserFilter : IFeatureFilter { - public Task EvaluateAsync(FeatureFilterEvaluationContext context) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) { return Task.FromResult(false); } diff --git a/examples/TargetingConsoleApp/Program.cs b/examples/TargetingConsoleApp/Program.cs index 953f609e..10e64223 100644 --- a/examples/TargetingConsoleApp/Program.cs +++ b/examples/TargetingConsoleApp/Program.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Consoto.Banking.HelpDesk @@ -62,7 +63,7 @@ public static async Task Main(string[] args) Groups = user.Groups }; - bool enabled = await featureManager.IsEnabledAsync(FeatureName, targetingContext); + bool enabled = await featureManager.IsEnabledAsync(FeatureName, targetingContext, CancellationToken.None); // // Output results diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs index c2573b8c..835cc1bd 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs @@ -107,8 +107,8 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context // // Enabled state is determined by either 'any' or 'all' features being enabled. bool enabled = RequirementType == RequirementType.All ? - await Features.All(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false)) : - await Features.Any(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false)); + await Features.All(async feature => await fm.IsEnabledAsync(feature, context.HttpContext.RequestAborted).ConfigureAwait(false)).ConfigureAwait(false) : + await Features.Any(async feature => await fm.IsEnabledAsync(feature, context.HttpContext.RequestAborted).ConfigureAwait(false)).ConfigureAwait(false); if (enabled) { diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs index 594620ae..80a68a7e 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs @@ -30,7 +30,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE { IFeatureManager featureManager = context.HttpContext.RequestServices.GetRequiredService(); - if (await featureManager.IsEnabledAsync(FeatureName).ConfigureAwait(false)) + if (await featureManager.IsEnabledAsync(FeatureName, context.HttpContext.RequestAborted).ConfigureAwait(false)) { IServiceProvider serviceProvider = context.HttpContext.RequestServices.GetRequiredService(); diff --git a/src/Microsoft.FeatureManagement.AspNetCore/TagHelpers/FeatureTagHelper.cs b/src/Microsoft.FeatureManagement.AspNetCore/TagHelpers/FeatureTagHelper.cs index 4a0da1e0..651c0068 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/TagHelpers/FeatureTagHelper.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/TagHelpers/FeatureTagHelper.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Razor.TagHelpers; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.Mvc.TagHelpers @@ -55,8 +56,8 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu IEnumerable names = Name.Split(',').Select(n => n.Trim()); enabled = Requirement == RequirementType.All ? - await names.All(async n => await _featureManager.IsEnabledAsync(n).ConfigureAwait(false)) : - await names.Any(async n => await _featureManager.IsEnabledAsync(n).ConfigureAwait(false)); + await names.All(async n => await _featureManager.IsEnabledAsync(n, CancellationToken.None).ConfigureAwait(false)).ConfigureAwait(false) : + await names.Any(async n => await _featureManager.IsEnabledAsync(n, CancellationToken.None).ConfigureAwait(false)).ConfigureAwait(false); } if (Negate) diff --git a/src/Microsoft.FeatureManagement.AspNetCore/UseForFeatureExtensions.cs b/src/Microsoft.FeatureManagement.AspNetCore/UseForFeatureExtensions.cs index 4774ee9b..c8562b7a 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/UseForFeatureExtensions.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/UseForFeatureExtensions.cs @@ -57,7 +57,7 @@ public static IApplicationBuilder UseForFeature(this IApplicationBuilder app, st { IFeatureManager fm = context.RequestServices.GetRequiredService(); - if (await fm.IsEnabledAsync(featureName).ConfigureAwait(false)) + if (await fm.IsEnabledAsync(featureName, context.RequestAborted).ConfigureAwait(false)) { await branch(context).ConfigureAwait(false); } diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 4774fd5e..f5f99b09 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -7,6 +7,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -45,7 +46,7 @@ public void Dispose() _changeSubscription = null; } - public Task GetFeatureDefinitionAsync(string featureName) + public Task GetFeatureDefinitionAsync(string featureName, CancellationToken _) { if (featureName == null) { @@ -68,7 +69,7 @@ public Task GetFeatureDefinitionAsync(string featureName) // The async key word is necessary for creating IAsyncEnumerable. // The need to disable this warning occurs when implementaing async stream synchronously. #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - public async IAsyncEnumerable GetAllFeatureDefinitionsAsync() + public async IAsyncEnumerable GetAllFeatureDefinitionsAsync([EnumeratorCancellation] CancellationToken _) #pragma warning restore CS1998 { if (Interlocked.Exchange(ref _stale, 0) != 0) diff --git a/src/Microsoft.FeatureManagement/ContextualFeatureFilterEvaluator.cs b/src/Microsoft.FeatureManagement/ContextualFeatureFilterEvaluator.cs index baf9220a..53df9f9c 100644 --- a/src/Microsoft.FeatureManagement/ContextualFeatureFilterEvaluator.cs +++ b/src/Microsoft.FeatureManagement/ContextualFeatureFilterEvaluator.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement @@ -15,7 +16,7 @@ namespace Microsoft.FeatureManagement class ContextualFeatureFilterEvaluator : IContextualFeatureFilter { private IFeatureFilterMetadata _filter; - private Func> _evaluateFunc; + private Func> _evaluateFunc; public ContextualFeatureFilterEvaluator(IFeatureFilterMetadata filter, Type appContextType) { @@ -43,14 +44,14 @@ public ContextualFeatureFilterEvaluator(IFeatureFilterMetadata filter, Type appC _filter = filter; } - public Task EvaluateAsync(FeatureFilterEvaluationContext evaluationContext, object context) + public Task EvaluateAsync(FeatureFilterEvaluationContext evaluationContext, object context, CancellationToken cancellationToken) { if (_evaluateFunc == null) { return Task.FromResult(false); } - return _evaluateFunc(_filter, evaluationContext, context); + return _evaluateFunc(_filter, evaluationContext, context, cancellationToken); } public static bool IsContextualFilter(IFeatureFilterMetadata filter, Type appContextType) @@ -72,7 +73,7 @@ private static Type GetContextualFilterInterface(IFeatureFilterMetadata filter, return targetInterface; } - private static Func> TypeAgnosticEvaluate(Type filterType, MethodInfo method) + private static Func> TypeAgnosticEvaluate(Type filterType, MethodInfo method) { // // Get the generic version of the evaluation helper method @@ -82,23 +83,30 @@ private static Func> // // Create a type specific version of the evaluation helper method MethodInfo constructedHelper = genericHelper.MakeGenericMethod - (filterType, method.GetParameters()[0].ParameterType, method.GetParameters()[1].ParameterType, method.ReturnType); + (filterType, + method.GetParameters()[0].ParameterType, + method.GetParameters()[1].ParameterType, + method.GetParameters()[2].ParameterType, + method.ReturnType); // // Invoke the method to get the func object typeAgnosticDelegate = constructedHelper.Invoke(null, new object[] { method }); - return (Func>)typeAgnosticDelegate; + return (Func>)typeAgnosticDelegate; } - private static Func> GenericTypeAgnosticEvaluate(MethodInfo method) + private static Func> GenericTypeAgnosticEvaluate(MethodInfo method) { - Func> func = (Func>)Delegate.CreateDelegate - (typeof(Func>), method); + Func> func = + (Func>) + Delegate.CreateDelegate(typeof(Func>), method); - Func> genericDelegate = (object target, FeatureFilterEvaluationContext param1, object param2) => func((TTarget)target, param1, (TParam2)param2); + Func> genericDelegate = + (object target, FeatureFilterEvaluationContext param1, object param2, CancellationToken param3) => + func((TTarget)target, param1, (TParam2)param2, param3); return genericDelegate; } } -} +} \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/EmptySessionManager.cs b/src/Microsoft.FeatureManagement/EmptySessionManager.cs index fd4b3d3f..7f09b910 100644 --- a/src/Microsoft.FeatureManagement/EmptySessionManager.cs +++ b/src/Microsoft.FeatureManagement/EmptySessionManager.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement @@ -10,12 +11,12 @@ namespace Microsoft.FeatureManagement /// class EmptySessionManager : ISessionManager { - public Task SetAsync(string featureName, bool enabled) + public Task SetAsync(string featureName, bool enabled, CancellationToken _) { return Task.CompletedTask; } - public Task GetAsync(string featureName) + public Task GetAsync(string featureName, CancellationToken _) { return Task.FromResult((bool?)null); } diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs index c7ea8840..1f672861 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.FeatureManagement.Utils; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters @@ -40,8 +41,9 @@ public object BindParameters(IConfiguration filterParameters) /// Performs a percentage based evaluation to determine whether a feature is enabled. /// /// The feature evaluation context. + /// The cancellation token to cancel the operation. /// True if the feature is enabled, false otherwise. - public Task EvaluateAsync(FeatureFilterEvaluationContext context) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken) { // // Check if prebound settings available, otherwise bind from parameters. diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index 884332b3..48ac7b1c 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters @@ -40,8 +41,9 @@ public object BindParameters(IConfiguration filterParameters) /// Evaluates whether a feature is enabled based on a configurable time window. /// /// The feature evaluation context. + /// The cancellation token to cancel the operation. /// True if the feature is enabled, false otherwise. - public Task EvaluateAsync(FeatureFilterEvaluationContext context) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken) { // // Check if prebound settings available, otherwise bind from parameters. diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 157cba02..f41d1363 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -9,6 +9,8 @@ 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 @@ -54,19 +56,21 @@ public FeatureManager( _parametersCache = new MemoryCache(new MemoryCacheOptions()); } - public Task IsEnabledAsync(string feature) + public Task IsEnabledAsync(string feature, CancellationToken cancellationToken) { - return IsEnabledAsync(feature, null, false); + return IsEnabledAsync(feature, null, false, cancellationToken); } - public Task IsEnabledAsync(string feature, TContext appContext) + public Task IsEnabledAsync(string feature, TContext appContext, CancellationToken cancellationToken) { - return IsEnabledAsync(feature, appContext, true); + return IsEnabledAsync(feature, appContext, true, cancellationToken); } - public async IAsyncEnumerable GetFeatureNamesAsync() + public async IAsyncEnumerable GetFeatureNamesAsync([EnumeratorCancellation] CancellationToken cancellationToken) { - await foreach (FeatureDefinition featureDefintion in _featureDefinitionProvider.GetAllFeatureDefinitionsAsync().ConfigureAwait(false)) + await foreach (FeatureDefinition featureDefintion in _featureDefinitionProvider + .GetAllFeatureDefinitionsAsync(cancellationToken) + .ConfigureAwait(false)) { yield return featureDefintion.Name; } @@ -77,11 +81,11 @@ public void Dispose() _parametersCache.Dispose(); } - private async Task IsEnabledAsync(string feature, TContext appContext, bool useAppContext) + private async Task IsEnabledAsync(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken) { foreach (ISessionManager sessionManager in _sessionManagers) { - bool? readSessionResult = await sessionManager.GetAsync(feature).ConfigureAwait(false); + bool? readSessionResult = await sessionManager.GetAsync(feature, cancellationToken).ConfigureAwait(false); if (readSessionResult.HasValue) { @@ -91,7 +95,9 @@ private async Task IsEnabledAsync(string feature, TContext appCo bool enabled; - FeatureDefinition featureDefinition = await _featureDefinitionProvider.GetFeatureDefinitionAsync(feature).ConfigureAwait(false); + FeatureDefinition featureDefinition = await _featureDefinitionProvider + .GetFeatureDefinitionAsync(feature, cancellationToken) + .ConfigureAwait(false); if (featureDefinition != null) { @@ -179,7 +185,7 @@ private async Task IsEnabledAsync(string feature, TContext appCo BindSettings(filter, context, filterIndex); if (contextualFilter != null && - await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) == targetEvaluation) + await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).ConfigureAwait(false) == targetEvaluation) { enabled = targetEvaluation; @@ -218,7 +224,7 @@ await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) foreach (ISessionManager sessionManager in _sessionManagers) { - await sessionManager.SetAsync(feature, enabled).ConfigureAwait(false); + await sessionManager.SetAsync(feature, enabled, cancellationToken).ConfigureAwait(false); } return enabled; diff --git a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs index 1c676cbb..fa0770b5 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement @@ -22,13 +24,13 @@ public FeatureManagerSnapshot(IFeatureManager featureManager) _featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager)); } - public async IAsyncEnumerable GetFeatureNamesAsync() + public async IAsyncEnumerable GetFeatureNamesAsync([EnumeratorCancellation]CancellationToken cancellationToken) { if (_featureNames == null) { var featureNames = new List(); - await foreach (string featureName in _featureManager.GetFeatureNamesAsync().ConfigureAwait(false)) + await foreach (string featureName in _featureManager.GetFeatureNamesAsync(cancellationToken).ConfigureAwait(false)) { featureNames.Add(featureName); } @@ -42,7 +44,7 @@ public async IAsyncEnumerable GetFeatureNamesAsync() } } - public Task IsEnabledAsync(string feature) + public async Task IsEnabledAsync(string feature, CancellationToken cancellationToken) { return _flagCache.GetOrAdd( feature, diff --git a/src/Microsoft.FeatureManagement/IContextualFeatureFilter.cs b/src/Microsoft.FeatureManagement/IContextualFeatureFilter.cs index 64586334..84deb352 100644 --- a/src/Microsoft.FeatureManagement/IContextualFeatureFilter.cs +++ b/src/Microsoft.FeatureManagement/IContextualFeatureFilter.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement @@ -18,7 +19,8 @@ public interface IContextualFeatureFilter : IFeatureFilterMetadata /// /// A feature filter evaluation context that contains information that may be needed to evaluate the filter. This context includes configuration, if any, for this filter for the feature being evaluated. /// A context defined by the application that is passed in to the feature management system to provide contextual information for evaluating a feature's state. + /// The cancellation token to cancel the operation. /// True if the filter's criteria has been met, false otherwise. - Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, TContext appContext); + Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, TContext appContext, CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs index bc4895b9..3b5adc15 100644 --- a/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. // using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement @@ -15,13 +16,15 @@ 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); + Task GetFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken); /// /// Retrieves definitions for all features. /// + /// The cancellation token to cancel the operation. /// An enumerator which provides asynchronous iteration over feature definitions. - IAsyncEnumerable GetAllFeatureDefinitionsAsync(); + IAsyncEnumerable GetAllFeatureDefinitionsAsync(CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureFilter.cs b/src/Microsoft.FeatureManagement/IFeatureFilter.cs index e6d914df..865a40dc 100644 --- a/src/Microsoft.FeatureManagement/IFeatureFilter.cs +++ b/src/Microsoft.FeatureManagement/IFeatureFilter.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement @@ -14,7 +15,8 @@ public interface IFeatureFilter : IFeatureFilterMetadata /// Evaluates the feature filter to see if the filter's criteria for being enabled has been satisfied. /// /// A feature filter evaluation context that contains information that may be needed to evaluate the filter. This context includes configuration, if any, for this filter for the feature being evaluated. + /// The cancellation token to cancel the operation. /// True if the filter's criteria has been met, false otherwise. - Task EvaluateAsync(FeatureFilterEvaluationContext context); + Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureManager.cs b/src/Microsoft.FeatureManagement/IFeatureManager.cs index a0602cba..d9b07e6a 100644 --- a/src/Microsoft.FeatureManagement/IFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IFeatureManager.cs @@ -15,8 +15,9 @@ public interface IFeatureManager /// /// Retrieves a list of feature 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(); + IAsyncEnumerable GetFeatureNamesAsync(CancellationToken cancellationToken); /// /// Checks whether a given feature is enabled. @@ -24,7 +25,7 @@ public interface IFeatureManager /// The name of the feature to check. /// The cancellation token to cancel the operation. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature, CancellationToken cancellationToken = default); + Task IsEnabledAsync(string feature, CancellationToken cancellationToken); /// /// Checks whether a given feature is enabled. @@ -33,6 +34,6 @@ public interface IFeatureManager /// 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. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken = default); + Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/ISessionManager.cs b/src/Microsoft.FeatureManagement/ISessionManager.cs index 67189375..1c0656f1 100644 --- a/src/Microsoft.FeatureManagement/ISessionManager.cs +++ b/src/Microsoft.FeatureManagement/ISessionManager.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement @@ -15,13 +16,15 @@ public interface ISessionManager /// /// The name of the feature. /// The state of the feature. - Task SetAsync(string featureName, bool enabled); + /// The cancellation token to cancel the operation. + Task SetAsync(string featureName, bool enabled, CancellationToken cancellationToken); /// /// Queries the session manager for the session's feature state, if any, for the given feature. /// /// The name of the feature. + /// The cancellation token to cancel the operation. /// The state of the feature if it is present in the session, otherwise null. - Task GetAsync(string featureName); + Task GetAsync(string featureName, CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs index 1e9fb6ec..16d986f7 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Security.Cryptography; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters @@ -52,9 +53,10 @@ public object BindParameters(IConfiguration filterParameters) /// /// The feature evaluation context. /// The targeting context to use during targeting evaluation. + /// The cancellation token to cancel the operation. /// Thrown if either or is null. /// True if the feature is enabled, false otherwise. - public Task EvaluateAsync(FeatureFilterEvaluationContext context, ITargetingContext targetingContext) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, ITargetingContext targetingContext, CancellationToken cancellationToken) { if (context == null) { diff --git a/src/Microsoft.FeatureManagement/Targeting/ITargetingContextAccessor.cs b/src/Microsoft.FeatureManagement/Targeting/ITargetingContextAccessor.cs index 94c1eebd..2739e628 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ITargetingContextAccessor.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ITargetingContextAccessor.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters @@ -13,7 +14,8 @@ public interface ITargetingContextAccessor /// /// Retrieves the current targeting context. /// + /// The cancellation token to cancel the operation. /// The current targeting context. - ValueTask GetContextAsync(); + ValueTask GetContextAsync(CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs index d57e4b31..4e92205a 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters @@ -47,9 +48,10 @@ public object BindParameters(IConfiguration filterParameters) /// Performs a targeting evaluation using the current to determine if a feature should be enabled. /// /// The feature evaluation context. + /// The cancellation token to cancel the operation. /// Thrown if is null. /// True if the feature is enabled, false otherwise. - public async Task EvaluateAsync(FeatureFilterEvaluationContext context) + public async Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken) { if (context == null) { @@ -58,7 +60,7 @@ public async Task EvaluateAsync(FeatureFilterEvaluationContext context) // // Acquire targeting context via accessor - TargetingContext targetingContext = await _contextAccessor.GetContextAsync().ConfigureAwait(false); + TargetingContext targetingContext = await _contextAccessor.GetContextAsync(cancellationToken).ConfigureAwait(false); // // Ensure targeting can be performed @@ -71,7 +73,7 @@ public async Task EvaluateAsync(FeatureFilterEvaluationContext context) // // Utilize contextual filter for targeting evaluation - return await _contextualFilter.EvaluateAsync(context, targetingContext).ConfigureAwait(false); + return await _contextualFilter.EvaluateAsync(context, targetingContext, cancellationToken).ConfigureAwait(false); } } } diff --git a/tests/Tests.FeatureManagement/ContextualTestFilter.cs b/tests/Tests.FeatureManagement/ContextualTestFilter.cs index 4bad3010..91bd6222 100644 --- a/tests/Tests.FeatureManagement/ContextualTestFilter.cs +++ b/tests/Tests.FeatureManagement/ContextualTestFilter.cs @@ -3,6 +3,7 @@ // using Microsoft.FeatureManagement; using System; +using System.Threading; using System.Threading.Tasks; namespace Tests.FeatureManagement @@ -11,7 +12,7 @@ class ContextualTestFilter : IContextualFeatureFilter { public Func ContextualCallback { get; set; } - public Task EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext, CancellationToken _) { return Task.FromResult(ContextualCallback?.Invoke(context, accountContext) ?? false); } diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index 9ecd3e7f..a3884eef 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -15,6 +15,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -43,9 +44,9 @@ public async Task ReadsConfiguration() IFeatureManager featureManager = serviceProvider.GetRequiredService(); - Assert.True(await featureManager.IsEnabledAsync(OnFeature)); + Assert.True(await featureManager.IsEnabledAsync(OnFeature, CancellationToken.None)); - Assert.False(await featureManager.IsEnabledAsync(OffFeature)); + Assert.False(await featureManager.IsEnabledAsync(OffFeature, CancellationToken.None)); IEnumerable featureFilters = serviceProvider.GetRequiredService>(); @@ -66,7 +67,7 @@ public async Task ReadsConfiguration() return Task.FromResult(true); }; - await featureManager.IsEnabledAsync(ConditionalFeature); + await featureManager.IsEnabledAsync(ConditionalFeature, CancellationToken.None); Assert.True(called); } @@ -280,10 +281,10 @@ public async Task TimeWindow() IFeatureManager featureManager = provider.GetRequiredService(); - Assert.True(await featureManager.IsEnabledAsync(feature1)); - Assert.False(await featureManager.IsEnabledAsync(feature2)); - Assert.True(await featureManager.IsEnabledAsync(feature3)); - Assert.False(await featureManager.IsEnabledAsync(feature4)); + Assert.True(await featureManager.IsEnabledAsync(feature1, CancellationToken.None)); + Assert.False(await featureManager.IsEnabledAsync(feature2, CancellationToken.None)); + Assert.True(await featureManager.IsEnabledAsync(feature3, CancellationToken.None)); + Assert.False(await featureManager.IsEnabledAsync(feature4, CancellationToken.None)); } [Fact] @@ -310,7 +311,7 @@ public async Task Percentage() for (int i = 0; i < 10; i++) { - if (await featureManager.IsEnabledAsync(feature1)) + if (await featureManager.IsEnabledAsync(feature1, CancellationToken.None)) { enabledCount++; } @@ -348,21 +349,21 @@ public async Task Targeting() Assert.True(await featureManager.IsEnabledAsync(targetingTestFeature, new TargetingContext { UserId = "Jeff" - })); + }, CancellationToken.None)); // // Not targeted by user id, but targeted by default rollout Assert.True(await featureManager.IsEnabledAsync(targetingTestFeature, new TargetingContext { UserId = "Anne" - })); + }, CancellationToken.None)); // // Not targeted by user id or default rollout Assert.False(await featureManager.IsEnabledAsync(targetingTestFeature, new TargetingContext { UserId = "Patty" - })); + }, CancellationToken.None)); // // Targeted by group rollout @@ -370,7 +371,7 @@ public async Task Targeting() { UserId = "Patty", Groups = new List() { "Ring1" } - })); + }, CancellationToken.None)); // // Not targeted by user id, default rollout or group rollout @@ -378,7 +379,7 @@ public async Task Targeting() { UserId = "Isaac", Groups = new List() { "Ring1" } - })); + }, CancellationToken.None)); } [Fact] @@ -416,7 +417,7 @@ public async Task TargetingAccessor() UserId = "Jeff" }; - Assert.True(await featureManager.IsEnabledAsync(beta)); + Assert.True(await featureManager.IsEnabledAsync(beta, CancellationToken.None)); // // Not targeted by user id or default rollout @@ -425,7 +426,7 @@ public async Task TargetingAccessor() UserId = "Patty" }; - Assert.False(await featureManager.IsEnabledAsync(beta)); + Assert.False(await featureManager.IsEnabledAsync(beta, CancellationToken.None)); } [Fact] @@ -458,11 +459,11 @@ public async Task UsesContext() context.AccountId = "NotEnabledAccount"; - Assert.False(await featureManager.IsEnabledAsync(ContextualFeature, context)); + Assert.False(await featureManager.IsEnabledAsync(ContextualFeature, context, CancellationToken.None)); context.AccountId = "abc"; - Assert.True(await featureManager.IsEnabledAsync(ContextualFeature, context)); + Assert.True(await featureManager.IsEnabledAsync(ContextualFeature, context, CancellationToken.None)); } [Fact] @@ -504,7 +505,7 @@ public async Task ListsFeatures() bool hasItems = false; - await foreach (string feature in featureManager.GetFeatureNamesAsync()) + await foreach (string feature in featureManager.GetFeatureNamesAsync(CancellationToken.None)) { hasItems = true; @@ -530,7 +531,8 @@ public async Task ThrowsExceptionForMissingFeatureFilter() IFeatureManager featureManager = serviceProvider.GetRequiredService(); - FeatureManagementException e = await Assert.ThrowsAsync(async () => await featureManager.IsEnabledAsync(ConditionalFeature)); + FeatureManagementException e = await Assert.ThrowsAsync( + async () => await featureManager.IsEnabledAsync(ConditionalFeature, CancellationToken.None)); Assert.Equal(FeatureManagementError.MissingFeatureFilter, e.Error); } @@ -556,7 +558,7 @@ public async Task SwallowsExceptionForMissingFeatureFilter() IFeatureManager featureManager = serviceProvider.GetRequiredService(); - var isEnabled = await featureManager.IsEnabledAsync(ConditionalFeature); + var isEnabled = await featureManager.IsEnabledAsync(ConditionalFeature, CancellationToken.None); Assert.False(isEnabled); } @@ -634,7 +636,7 @@ public async Task CustomFeatureDefinitionProvider() return Task.FromResult(true); }; - await featureManager.IsEnabledAsync(ConditionalFeature); + await featureManager.IsEnabledAsync(ConditionalFeature, CancellationToken.None); Assert.True(called); } diff --git a/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs b/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs index fe613254..76d64a10 100644 --- a/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs +++ b/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; namespace Tests.FeatureManagement @@ -16,7 +18,7 @@ public InMemoryFeatureDefinitionProvider(IEnumerable featureD } #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - public async IAsyncEnumerable GetAllFeatureDefinitionsAsync() + public async IAsyncEnumerable GetAllFeatureDefinitionsAsync([EnumeratorCancellation] CancellationToken _) #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { foreach (FeatureDefinition definition in _definitions) @@ -25,7 +27,7 @@ public async IAsyncEnumerable GetAllFeatureDefinitionsAsync() } } - public Task GetFeatureDefinitionAsync(string featureName) + public Task GetFeatureDefinitionAsync(string featureName, CancellationToken _) { return Task.FromResult(_definitions.FirstOrDefault(definitions => definitions.Name.Equals(featureName, StringComparison.OrdinalIgnoreCase))); } diff --git a/tests/Tests.FeatureManagement/InvalidFeatureFilter.cs b/tests/Tests.FeatureManagement/InvalidFeatureFilter.cs index 0fab5063..4f81e3e2 100644 --- a/tests/Tests.FeatureManagement/InvalidFeatureFilter.cs +++ b/tests/Tests.FeatureManagement/InvalidFeatureFilter.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. // using Microsoft.FeatureManagement; +using System.Threading; using System.Threading.Tasks; namespace Tests.FeatureManagement @@ -10,12 +11,12 @@ namespace Tests.FeatureManagement // Cannot implement more than one IFeatureFilter interface class InvalidFeatureFilter : IContextualFeatureFilter, IContextualFeatureFilter { - public Task EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext, CancellationToken _) { return Task.FromResult(false); } - public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, object appContext) + public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, object appContext, CancellationToken _) { return Task.FromResult(false); } @@ -25,12 +26,12 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterCont // Cannot implement more than one IFeatureFilter interface class InvalidFeatureFilter2 : IFeatureFilter, IContextualFeatureFilter { - public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, object appContext) + public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, object appContext, CancellationToken _) { return Task.FromResult(false); } - public Task EvaluateAsync(FeatureFilterEvaluationContext context) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) { return Task.FromResult(false); } diff --git a/tests/Tests.FeatureManagement/OnDemandTargetingContextAccessor.cs b/tests/Tests.FeatureManagement/OnDemandTargetingContextAccessor.cs index 7eb7e971..aaff4a41 100644 --- a/tests/Tests.FeatureManagement/OnDemandTargetingContextAccessor.cs +++ b/tests/Tests.FeatureManagement/OnDemandTargetingContextAccessor.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. // using Microsoft.FeatureManagement.FeatureFilters; +using System.Threading; using System.Threading.Tasks; namespace Tests.FeatureManagement @@ -10,7 +11,7 @@ class OnDemandTargetingContextAccessor : ITargetingContextAccessor { public TargetingContext Current { get; set; } - public ValueTask GetContextAsync() + public ValueTask GetContextAsync(CancellationToken _) { return new ValueTask(Current); } diff --git a/tests/Tests.FeatureManagement/TestFilter.cs b/tests/Tests.FeatureManagement/TestFilter.cs index 2dbf0f71..b2de169d 100644 --- a/tests/Tests.FeatureManagement/TestFilter.cs +++ b/tests/Tests.FeatureManagement/TestFilter.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.FeatureManagement; using System; +using System.Threading; using System.Threading.Tasks; namespace Tests.FeatureManagement @@ -24,7 +25,7 @@ public object BindParameters(IConfiguration parameters) return parameters; } - public Task EvaluateAsync(FeatureFilterEvaluationContext context) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) { return Callback?.Invoke(context) ?? Task.FromResult(false); } From d087e7bb1bc50028cf9c7835930d50bb19fffc75 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 3 Jul 2023 14:42:39 -0500 Subject: [PATCH 03/80] Revert "Revert "Added default value for cancellation token in interfaces to keep existing usage possible. (#133)" (#138)" This reverts commit 8f9a7e4052ac8113ea3834b527d4ec47db42032c. --- examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs | 2 +- examples/FeatureFlagDemo/BrowserFilter.cs | 2 +- .../HttpContextTargetingContextAccessor.cs | 2 +- examples/FeatureFlagDemo/SuperUserFilter.cs | 2 +- .../ConfigurationFeatureDefinitionProvider.cs | 4 ++-- src/Microsoft.FeatureManagement/EmptySessionManager.cs | 4 ++-- .../IContextualFeatureFilter.cs | 2 +- .../IFeatureDefinitionProvider.cs | 4 ++-- src/Microsoft.FeatureManagement/IFeatureFilter.cs | 2 +- src/Microsoft.FeatureManagement/IFeatureManager.cs | 6 +++--- src/Microsoft.FeatureManagement/ISessionManager.cs | 4 ++-- .../Targeting/ITargetingContextAccessor.cs | 2 +- tests/Tests.FeatureManagement/ContextualTestFilter.cs | 2 +- .../InMemoryFeatureDefinitionProvider.cs | 4 ++-- tests/Tests.FeatureManagement/InvalidFeatureFilter.cs | 8 ++++---- .../OnDemandTargetingContextAccessor.cs | 2 +- tests/Tests.FeatureManagement/TestFilter.cs | 2 +- 17 files changed, 27 insertions(+), 27 deletions(-) diff --git a/examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs b/examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs index 3d236eed..193e87d4 100644 --- a/examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs +++ b/examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs @@ -18,7 +18,7 @@ namespace Consoto.Banking.AccountService.FeatureManagement [FilterAlias("AccountId")] class AccountIdFilter : IContextualFeatureFilter { - public Task EvaluateAsync(FeatureFilterEvaluationContext featureEvaluationContext, IAccountContext accountContext, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext featureEvaluationContext, IAccountContext accountContext, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(accountContext?.AccountId)) { diff --git a/examples/FeatureFlagDemo/BrowserFilter.cs b/examples/FeatureFlagDemo/BrowserFilter.cs index e33f58c7..cd701078 100644 --- a/examples/FeatureFlagDemo/BrowserFilter.cs +++ b/examples/FeatureFlagDemo/BrowserFilter.cs @@ -24,7 +24,7 @@ public BrowserFilter(IHttpContextAccessor httpContextAccessor) _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } - public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken) { BrowserFilterSettings settings = context.Parameters.Get() ?? new BrowserFilterSettings(); diff --git a/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs b/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs index 931be833..097605b6 100644 --- a/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs +++ b/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs @@ -24,7 +24,7 @@ public HttpContextTargetingContextAccessor(IHttpContextAccessor httpContextAcces _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } - public ValueTask GetContextAsync(CancellationToken _) + public ValueTask GetContextAsync(CancellationToken cancellationToken) { HttpContext httpContext = _httpContextAccessor.HttpContext; diff --git a/examples/FeatureFlagDemo/SuperUserFilter.cs b/examples/FeatureFlagDemo/SuperUserFilter.cs index 1e188704..48ab33ec 100644 --- a/examples/FeatureFlagDemo/SuperUserFilter.cs +++ b/examples/FeatureFlagDemo/SuperUserFilter.cs @@ -9,7 +9,7 @@ namespace FeatureFlagDemo.FeatureManagement.FeatureFilters { public class SuperUserFilter : IFeatureFilter { - public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken) { return Task.FromResult(false); } diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index f5f99b09..062c9300 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -46,7 +46,7 @@ public void Dispose() _changeSubscription = null; } - public Task GetFeatureDefinitionAsync(string featureName, CancellationToken _) + public Task GetFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken) { if (featureName == null) { @@ -69,7 +69,7 @@ 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 _) + public async IAsyncEnumerable GetAllFeatureDefinitionsAsync([EnumeratorCancellation] CancellationToken cancellationToken) #pragma warning restore CS1998 { if (Interlocked.Exchange(ref _stale, 0) != 0) diff --git a/src/Microsoft.FeatureManagement/EmptySessionManager.cs b/src/Microsoft.FeatureManagement/EmptySessionManager.cs index 7f09b910..7658a0e4 100644 --- a/src/Microsoft.FeatureManagement/EmptySessionManager.cs +++ b/src/Microsoft.FeatureManagement/EmptySessionManager.cs @@ -11,12 +11,12 @@ namespace Microsoft.FeatureManagement /// class EmptySessionManager : ISessionManager { - public Task SetAsync(string featureName, bool enabled, CancellationToken _) + public Task SetAsync(string featureName, bool enabled, CancellationToken cancellationToken) { return Task.CompletedTask; } - public Task GetAsync(string featureName, CancellationToken _) + public Task GetAsync(string featureName, CancellationToken cancellationToken) { return Task.FromResult((bool?)null); } diff --git a/src/Microsoft.FeatureManagement/IContextualFeatureFilter.cs b/src/Microsoft.FeatureManagement/IContextualFeatureFilter.cs index 84deb352..5df783d0 100644 --- a/src/Microsoft.FeatureManagement/IContextualFeatureFilter.cs +++ b/src/Microsoft.FeatureManagement/IContextualFeatureFilter.cs @@ -21,6 +21,6 @@ public interface IContextualFeatureFilter : IFeatureFilterMetadata /// A context defined by the application that is passed in to the feature management system to provide contextual information for evaluating a feature's state. /// The cancellation token to cancel the operation. /// True if the filter's criteria has been met, false otherwise. - Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, TContext appContext, CancellationToken cancellationToken); + Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, TContext appContext, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs index 3b5adc15..c8c37494 100644 --- a/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs @@ -18,13 +18,13 @@ public interface IFeatureDefinitionProvider /// 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); + 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); + IAsyncEnumerable GetAllFeatureDefinitionsAsync(CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureFilter.cs b/src/Microsoft.FeatureManagement/IFeatureFilter.cs index 865a40dc..416772db 100644 --- a/src/Microsoft.FeatureManagement/IFeatureFilter.cs +++ b/src/Microsoft.FeatureManagement/IFeatureFilter.cs @@ -17,6 +17,6 @@ public interface IFeatureFilter : IFeatureFilterMetadata /// A feature filter evaluation context that contains information that may be needed to evaluate the filter. This context includes configuration, if any, for this filter for the feature being evaluated. /// The cancellation token to cancel the operation. /// True if the filter's criteria has been met, false otherwise. - Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken); + Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureManager.cs b/src/Microsoft.FeatureManagement/IFeatureManager.cs index d9b07e6a..53231f89 100644 --- a/src/Microsoft.FeatureManagement/IFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IFeatureManager.cs @@ -17,7 +17,7 @@ public interface IFeatureManager /// /// 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); + IAsyncEnumerable GetFeatureNamesAsync(CancellationToken cancellationToken = default); /// /// Checks whether a given feature is enabled. @@ -25,7 +25,7 @@ public interface IFeatureManager /// The name of the feature to check. /// The cancellation token to cancel the operation. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature, CancellationToken cancellationToken); + Task IsEnabledAsync(string feature, CancellationToken cancellationToken = default); /// /// Checks whether a given feature is enabled. @@ -34,6 +34,6 @@ public interface IFeatureManager /// 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. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken); + Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/ISessionManager.cs b/src/Microsoft.FeatureManagement/ISessionManager.cs index 1c0656f1..f6327932 100644 --- a/src/Microsoft.FeatureManagement/ISessionManager.cs +++ b/src/Microsoft.FeatureManagement/ISessionManager.cs @@ -17,7 +17,7 @@ public interface ISessionManager /// The name of the feature. /// The state of the feature. /// The cancellation token to cancel the operation. - Task SetAsync(string featureName, bool enabled, CancellationToken cancellationToken); + Task SetAsync(string featureName, bool enabled, CancellationToken cancellationToken = default); /// /// Queries the session manager for the session's feature state, if any, for the given feature. @@ -25,6 +25,6 @@ public interface ISessionManager /// The name of the feature. /// The cancellation token to cancel the operation. /// The state of the feature if it is present in the session, otherwise null. - Task GetAsync(string featureName, CancellationToken cancellationToken); + Task GetAsync(string featureName, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/Targeting/ITargetingContextAccessor.cs b/src/Microsoft.FeatureManagement/Targeting/ITargetingContextAccessor.cs index 2739e628..04e5f709 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ITargetingContextAccessor.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ITargetingContextAccessor.cs @@ -16,6 +16,6 @@ public interface ITargetingContextAccessor /// /// The cancellation token to cancel the operation. /// The current targeting context. - ValueTask GetContextAsync(CancellationToken cancellationToken); + ValueTask GetContextAsync(CancellationToken cancellationToken = default); } } diff --git a/tests/Tests.FeatureManagement/ContextualTestFilter.cs b/tests/Tests.FeatureManagement/ContextualTestFilter.cs index 91bd6222..87241891 100644 --- a/tests/Tests.FeatureManagement/ContextualTestFilter.cs +++ b/tests/Tests.FeatureManagement/ContextualTestFilter.cs @@ -12,7 +12,7 @@ class ContextualTestFilter : IContextualFeatureFilter { public Func ContextualCallback { get; set; } - public Task EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext, CancellationToken cancellationToken) { return Task.FromResult(ContextualCallback?.Invoke(context, accountContext) ?? false); } diff --git a/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs b/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs index 76d64a10..ae02858b 100644 --- a/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs +++ b/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs @@ -18,7 +18,7 @@ public InMemoryFeatureDefinitionProvider(IEnumerable featureD } #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - public async IAsyncEnumerable GetAllFeatureDefinitionsAsync([EnumeratorCancellation] CancellationToken _) + public async IAsyncEnumerable GetAllFeatureDefinitionsAsync([EnumeratorCancellation] CancellationToken cancellationToken) #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { foreach (FeatureDefinition definition in _definitions) @@ -27,7 +27,7 @@ public async IAsyncEnumerable GetAllFeatureDefinitionsAsync([ } } - public Task GetFeatureDefinitionAsync(string featureName, CancellationToken _) + public Task GetFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken) { return Task.FromResult(_definitions.FirstOrDefault(definitions => definitions.Name.Equals(featureName, StringComparison.OrdinalIgnoreCase))); } diff --git a/tests/Tests.FeatureManagement/InvalidFeatureFilter.cs b/tests/Tests.FeatureManagement/InvalidFeatureFilter.cs index 4f81e3e2..cc42aaef 100644 --- a/tests/Tests.FeatureManagement/InvalidFeatureFilter.cs +++ b/tests/Tests.FeatureManagement/InvalidFeatureFilter.cs @@ -11,12 +11,12 @@ namespace Tests.FeatureManagement // Cannot implement more than one IFeatureFilter interface class InvalidFeatureFilter : IContextualFeatureFilter, IContextualFeatureFilter { - public Task EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext, CancellationToken cancellationToken) { return Task.FromResult(false); } - public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, object appContext, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, object appContext, CancellationToken cancellationToken) { return Task.FromResult(false); } @@ -26,12 +26,12 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterCont // Cannot implement more than one IFeatureFilter interface class InvalidFeatureFilter2 : IFeatureFilter, IContextualFeatureFilter { - public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, object appContext, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, object appContext, CancellationToken cancellationToken) { return Task.FromResult(false); } - public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken) { return Task.FromResult(false); } diff --git a/tests/Tests.FeatureManagement/OnDemandTargetingContextAccessor.cs b/tests/Tests.FeatureManagement/OnDemandTargetingContextAccessor.cs index aaff4a41..5ba376fb 100644 --- a/tests/Tests.FeatureManagement/OnDemandTargetingContextAccessor.cs +++ b/tests/Tests.FeatureManagement/OnDemandTargetingContextAccessor.cs @@ -11,7 +11,7 @@ class OnDemandTargetingContextAccessor : ITargetingContextAccessor { public TargetingContext Current { get; set; } - public ValueTask GetContextAsync(CancellationToken _) + public ValueTask GetContextAsync(CancellationToken cancellationToken) { return new ValueTask(Current); } diff --git a/tests/Tests.FeatureManagement/TestFilter.cs b/tests/Tests.FeatureManagement/TestFilter.cs index b2de169d..6a4c5c01 100644 --- a/tests/Tests.FeatureManagement/TestFilter.cs +++ b/tests/Tests.FeatureManagement/TestFilter.cs @@ -25,7 +25,7 @@ public object BindParameters(IConfiguration parameters) return parameters; } - public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken) { return Callback?.Invoke(context) ?? Task.FromResult(false); } From 87aa5a808834abcf7cd88315683a2efad77af507 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 3 Jul 2023 15:04:33 -0500 Subject: [PATCH 04/80] fix any conflicts left from adding cancellationToken back --- src/Microsoft.FeatureManagement/FeatureDefinition.cs | 2 +- src/Microsoft.FeatureManagement/FeatureManager.cs | 6 +++--- src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs | 6 +++--- tests/Tests.FeatureManagement/CustomTargetingFilter.cs | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureDefinition.cs b/src/Microsoft.FeatureManagement/FeatureDefinition.cs index a86c03a9..68c79360 100644 --- a/src/Microsoft.FeatureManagement/FeatureDefinition.cs +++ b/src/Microsoft.FeatureManagement/FeatureDefinition.cs @@ -34,7 +34,7 @@ public class FeatureDefinition /// /// /// - public Allocation Allocation { get; set; }; + public Allocation Allocation { get; set; } /// /// diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index f41d1363..f2c63419 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -199,7 +199,7 @@ await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).Con { BindSettings(filter, context, filterIndex); - if (await featureFilter.EvaluateAsync(context).ConfigureAwait(false) == targetEvaluation) { + if (await featureFilter.EvaluateAsync(context, cancellationToken).ConfigureAwait(false) == targetEvaluation) { enabled = targetEvaluation; break; @@ -230,12 +230,12 @@ await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).Con return enabled; } - public ValueTask GetVariantAsync(string feature) + public ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public ValueTask GetVariantAsync(string feature, TContext context) + public ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs index fa0770b5..056ce79e 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs @@ -44,14 +44,14 @@ public async IAsyncEnumerable GetFeatureNamesAsync([EnumeratorCancellati } } - public async Task IsEnabledAsync(string feature, CancellationToken cancellationToken) + public Task IsEnabledAsync(string feature, CancellationToken cancellationToken) { return _flagCache.GetOrAdd( feature, - (key) => _featureManager.IsEnabledAsync(key)); + (key) => _featureManager.IsEnabledAsync(key, cancellationToken)); } - public Task IsEnabledAsync(string feature, TContext context) + public Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken) { return _flagCache.GetOrAdd( feature, diff --git a/tests/Tests.FeatureManagement/CustomTargetingFilter.cs b/tests/Tests.FeatureManagement/CustomTargetingFilter.cs index cc89482f..daf970fd 100644 --- a/tests/Tests.FeatureManagement/CustomTargetingFilter.cs +++ b/tests/Tests.FeatureManagement/CustomTargetingFilter.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.FeatureFilters; using System; +using System.Threading; using System.Threading.Tasks; namespace Tests.FeatureManagement @@ -24,9 +24,9 @@ public CustomTargetingFilter(IOptions options, ILogg public Func> Callback { get; set; } - public Task EvaluateAsync(FeatureFilterEvaluationContext context) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken) { - return _contextualFilter.EvaluateAsync(context, new TargetingContext(){ UserId = "Jeff" }); + return _contextualFilter.EvaluateAsync(context, new TargetingContext(){ UserId = "Jeff" }, cancellationToken); } } } From 55e226417e7b7b58a1cf4f34c9891b7d1a95552a Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 3 Jul 2023 18:25:14 -0500 Subject: [PATCH 05/80] add in progress changes to allocation and featuredefinitionprovider --- examples/TargetingConsoleApp/Program.cs | 2 +- src/Microsoft.FeatureManagement/Allocation.cs | 22 ++++++++-- .../ConfigurationFeatureDefinitionProvider.cs | 40 +++++++++++++------ .../FeatureDefinition.cs | 3 +- src/Microsoft.FeatureManagement/Group.cs | 24 +++++++++++ src/Microsoft.FeatureManagement/Percentile.cs | 27 +++++++++++++ src/Microsoft.FeatureManagement/Status.cs | 9 +++++ .../StatusOverride.cs | 9 +++++ src/Microsoft.FeatureManagement/User.cs | 24 +++++++++++ src/Microsoft.FeatureManagement/Variant.cs | 9 +++++ 10 files changed, 151 insertions(+), 18 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/Group.cs create mode 100644 src/Microsoft.FeatureManagement/Percentile.cs create mode 100644 src/Microsoft.FeatureManagement/User.cs diff --git a/examples/TargetingConsoleApp/Program.cs b/examples/TargetingConsoleApp/Program.cs index 10e64223..15e35eb7 100644 --- a/examples/TargetingConsoleApp/Program.cs +++ b/examples/TargetingConsoleApp/Program.cs @@ -53,7 +53,7 @@ public static async Task Main(string[] args) // // Get user - User user = await userRepository.GetUser(userId); + AccountService.Identity.User user = await userRepository.GetUser(userId); // // Check if feature enabled diff --git a/src/Microsoft.FeatureManagement/Allocation.cs b/src/Microsoft.FeatureManagement/Allocation.cs index 90929c83..7e23823c 100644 --- a/src/Microsoft.FeatureManagement/Allocation.cs +++ b/src/Microsoft.FeatureManagement/Allocation.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using Microsoft.FeatureManagement.FeatureFilters; using System.Collections.Generic; namespace Microsoft.FeatureManagement @@ -11,19 +10,34 @@ namespace Microsoft.FeatureManagement /// public class Allocation { + /// + /// + /// + public string DefaultWhenEnabled { get; set; } + + /// + /// + /// + public string DefaultWhenDisabled { get; set; } + /// /// Describes a mapping of user id to variant. /// - public List Users { get; set; } + public IEnumerable User { get; set; } /// /// Describes a mapping of group names to variants. /// - public List Groups { get; set; } + public IEnumerable Group { get; set; } /// /// Allocate a percentage of the user base to variants. /// - public double Percentile { get; set; } + public IEnumerable Percentile { get; set; } + + /// + /// Maps users to the same percentile across multiple feature flags. + /// + public int Seed { get; set; } } } diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 062c9300..e35739d7 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -23,7 +23,6 @@ sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionProvider // provider to be marked for caching as well. private const string FeatureFiltersSectionName = "EnabledFor"; - private const string RequirementTypeKeyword = "RequirementType"; private readonly IConfiguration _configuration; private readonly ConcurrentDictionary _definitions; private IDisposable _changeSubscription; @@ -136,6 +135,9 @@ We support */ RequirementType requirementType = RequirementType.Any; + Status status = Status.Conditional; + Allocation allocation = null; + IEnumerable variants = Enumerable.Empty(); var enabledFor = new List(); @@ -161,16 +163,10 @@ We support } else { - string rawRequirementType = configurationSection[RequirementTypeKeyword]; - - // - // If requirement type is specified, parse it and set the requirementType variable - if (!string.IsNullOrEmpty(rawRequirementType) && !Enum.TryParse(rawRequirementType, ignoreCase: true, out requirementType)) - { - throw new FeatureManagementException( - FeatureManagementError.InvalidConfigurationSetting, - $"Invalid requirement type '{rawRequirementType}' for feature '{configurationSection.Key}'."); - } + requirementType = ParseFeatureDefinitionSectionEnum(configurationSection, nameof(RequirementType), requirementType); + status = ParseFeatureDefinitionSectionEnum(configurationSection, nameof(FeatureDefinition.Status), status); + allocation = configurationSection.GetValue(nameof(FeatureDefinition.Allocation)); + variants = configurationSection.GetValue>(nameof(FeatureDefinition.Variants)); IEnumerable filterSections = configurationSection.GetSection(FeatureFiltersSectionName).GetChildren(); @@ -194,7 +190,10 @@ We support { Name = configurationSection.Key, EnabledFor = enabledFor, - RequirementType = requirementType + RequirementType = requirementType, + Status = status, + Allocation = allocation, + Variants = variants }; } @@ -213,5 +212,22 @@ private IEnumerable GetFeatureDefinitionSections() return _configuration.GetChildren(); } } + + private T ParseFeatureDefinitionSectionEnum(IConfigurationSection configurationSection, string keyword, T enumValue) + where T : struct, Enum + { + string rawValue = configurationSection[keyword]; + + // + // If requirement type is specified, parse it and set the requirementType variable + if (!string.IsNullOrEmpty(rawValue) && !Enum.TryParse(rawValue, ignoreCase: true, out enumValue)) + { + throw new FeatureManagementException( + FeatureManagementError.InvalidConfigurationSetting, + $"Invalid requirement type '{rawValue}' for feature '{configurationSection.Key}'."); + } + + return enumValue; + } } } diff --git a/src/Microsoft.FeatureManagement/FeatureDefinition.cs b/src/Microsoft.FeatureManagement/FeatureDefinition.cs index 68c79360..049fa906 100644 --- a/src/Microsoft.FeatureManagement/FeatureDefinition.cs +++ b/src/Microsoft.FeatureManagement/FeatureDefinition.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. // using System.Collections.Generic; +using System.Linq; namespace Microsoft.FeatureManagement { @@ -39,6 +40,6 @@ public class FeatureDefinition /// /// /// - public List Variants { get; set; } + public IEnumerable Variants { get; set; } = Enumerable.Empty(); } } diff --git a/src/Microsoft.FeatureManagement/Group.cs b/src/Microsoft.FeatureManagement/Group.cs new file mode 100644 index 00000000..311c476f --- /dev/null +++ b/src/Microsoft.FeatureManagement/Group.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// + +using System.Collections.Generic; + +namespace Microsoft.FeatureManagement +{ + /// + /// + /// + public class Group + { + /// + /// The name of the variant. + /// + public string Name { get; set; } + + /// + /// + /// + public IEnumerable Groups { get; set; } + } +} diff --git a/src/Microsoft.FeatureManagement/Percentile.cs b/src/Microsoft.FeatureManagement/Percentile.cs new file mode 100644 index 00000000..e7ff8e8c --- /dev/null +++ b/src/Microsoft.FeatureManagement/Percentile.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// + +namespace Microsoft.FeatureManagement +{ + /// + /// + /// + public class Percentile + { + /// + /// The name of the variant. + /// + public string Name { get; set; } + + /// + /// + /// + public double From { get; set; } + + /// + /// + /// + public double To { get; set; } + } +} diff --git a/src/Microsoft.FeatureManagement/Status.cs b/src/Microsoft.FeatureManagement/Status.cs index e19740da..5824b643 100644 --- a/src/Microsoft.FeatureManagement/Status.cs +++ b/src/Microsoft.FeatureManagement/Status.cs @@ -3,9 +3,18 @@ // namespace Microsoft.FeatureManagement { + /// + /// + /// public enum Status { + /// + /// + /// Conditional, + /// + /// + /// Disabled } } diff --git a/src/Microsoft.FeatureManagement/StatusOverride.cs b/src/Microsoft.FeatureManagement/StatusOverride.cs index 17481b58..fc9f78cf 100644 --- a/src/Microsoft.FeatureManagement/StatusOverride.cs +++ b/src/Microsoft.FeatureManagement/StatusOverride.cs @@ -3,9 +3,18 @@ // namespace Microsoft.FeatureManagement { + /// + /// + /// public enum StatusOverride { + /// + /// + /// Enabled, + /// + /// + /// Disabled } } diff --git a/src/Microsoft.FeatureManagement/User.cs b/src/Microsoft.FeatureManagement/User.cs new file mode 100644 index 00000000..4336e420 --- /dev/null +++ b/src/Microsoft.FeatureManagement/User.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// + +using System.Collections.Generic; + +namespace Microsoft.FeatureManagement +{ + /// + /// + /// + public class User + { + /// + /// The name of the variant. + /// + public string Name { get; set; } + + /// + /// + /// + public IEnumerable Users { get; set; } + } +} diff --git a/src/Microsoft.FeatureManagement/Variant.cs b/src/Microsoft.FeatureManagement/Variant.cs index a7397b6e..2edeae5c 100644 --- a/src/Microsoft.FeatureManagement/Variant.cs +++ b/src/Microsoft.FeatureManagement/Variant.cs @@ -5,10 +5,19 @@ namespace Microsoft.FeatureManagement { + /// + /// + /// public class Variant { + /// + /// + /// public string Name { get; set; } + /// + /// + /// public IConfiguration Configuration { get; set; } } } From 1918bf95a5d500f4125b11e9f7516cd87ad51af9 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 3 Jul 2023 18:41:25 -0500 Subject: [PATCH 06/80] add examples for testing --- .../Controllers/HomeController.cs | 3 +- examples/FeatureFlagDemo/MyFeatureFlags.cs | 3 +- examples/FeatureFlagDemo/appsettings.json | 50 ++++++++++++++++++- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/examples/FeatureFlagDemo/Controllers/HomeController.cs b/examples/FeatureFlagDemo/Controllers/HomeController.cs index a939a88e..a455d987 100644 --- a/examples/FeatureFlagDemo/Controllers/HomeController.cs +++ b/examples/FeatureFlagDemo/Controllers/HomeController.cs @@ -22,8 +22,9 @@ public HomeController(IFeatureManagerSnapshot featureSnapshot) } [FeatureGate(MyFeatureFlags.Home)] - public IActionResult Index() + public async Task Index() { + bool test = await _featureManager.IsEnabledAsync(nameof(MyFeatureFlags.Banner)); return View(); } diff --git a/examples/FeatureFlagDemo/MyFeatureFlags.cs b/examples/FeatureFlagDemo/MyFeatureFlags.cs index 9441c531..c648e827 100644 --- a/examples/FeatureFlagDemo/MyFeatureFlags.cs +++ b/examples/FeatureFlagDemo/MyFeatureFlags.cs @@ -11,6 +11,7 @@ public enum MyFeatureFlags Beta, CustomViewData, ContentEnhancement, - EnhancedPipeline + EnhancedPipeline, + Banner } } diff --git a/examples/FeatureFlagDemo/appsettings.json b/examples/FeatureFlagDemo/appsettings.json index d0a37270..296b5bc6 100644 --- a/examples/FeatureFlagDemo/appsettings.json +++ b/examples/FeatureFlagDemo/appsettings.json @@ -36,7 +36,7 @@ } ] }, - "CustomViewData": { + "CustomViewData": { "EnabledFor": [ { "Name": "Browser", @@ -66,6 +66,54 @@ } } ] + }, + "Banner": { + "Allocation": { + "DefaultWhenEnabled": "Small", + "DefaultWhenDisabled": "Small", + "User": [ + { + "Variant": "Big", + "Users": [ + "Marsha" + ] + } + ], + "Group": [ + { + "Variant": "Big", + "Groups": [ + "Ring1" + ] + } + ], + "Percentile": [ + { + "Variant": "Big", + "From": 0, + "To": 10 + } + ], + "Seed": 13973240 + }, + "Variants": [ + { + "Name": "Big", + "ConfigurationValue": "1200px" + }, + { + "Name": "Small", + "ConfigurationValue": "300px" + } + ], + "EnabledFor": [ + { + "Name": "Microsoft.Percentage", + "Parameters": { + "Value": 100 + } + } + ] } } } From 3e3ab2e58de68774cc1cc8e6f2001146977fe49f Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 5 Jul 2023 13:48:12 -0500 Subject: [PATCH 07/80] fix adding new featuredefinition properties from featuremanagement definition --- .../ConfigurationFeatureDefinitionProvider.cs | 22 +++++++++-- .../FeatureManagementError.cs | 7 +++- .../FeatureManager.cs | 39 ++++++++++++++++++- src/Microsoft.FeatureManagement/Group.cs | 2 +- src/Microsoft.FeatureManagement/Percentile.cs | 2 +- src/Microsoft.FeatureManagement/User.cs | 2 +- 6 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index e35739d7..97af20dc 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -137,7 +137,7 @@ We support RequirementType requirementType = RequirementType.Any; Status status = Status.Conditional; Allocation allocation = null; - IEnumerable variants = Enumerable.Empty(); + List variants = new List(); var enabledFor = new List(); @@ -165,8 +165,6 @@ We support { requirementType = ParseFeatureDefinitionSectionEnum(configurationSection, nameof(RequirementType), requirementType); status = ParseFeatureDefinitionSectionEnum(configurationSection, nameof(FeatureDefinition.Status), status); - allocation = configurationSection.GetValue(nameof(FeatureDefinition.Allocation)); - variants = configurationSection.GetValue>(nameof(FeatureDefinition.Variants)); IEnumerable filterSections = configurationSection.GetSection(FeatureFiltersSectionName).GetChildren(); @@ -175,7 +173,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() { @@ -184,6 +182,22 @@ We support }); } } + + IConfigurationSection allocationSection = configurationSection.GetSection(nameof(FeatureDefinition.Allocation)); + allocation = new Allocation(); + allocationSection.Bind(allocation); + + IEnumerable variantsSections = configurationSection.GetSection(nameof(FeatureDefinition.Variants)).GetChildren(); + + foreach (IConfigurationSection section in variantsSections) + { + if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[nameof(FeatureVariant.Name)])) + { + FeatureVariant variant = new FeatureVariant(); + section.Bind(variant); + variants.Add(variant); + } + } } return new FeatureDefinition() diff --git a/src/Microsoft.FeatureManagement/FeatureManagementError.cs b/src/Microsoft.FeatureManagement/FeatureManagementError.cs index d67c0b16..2c389eb7 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementError.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementError.cs @@ -31,6 +31,11 @@ public enum FeatureManagementError /// /// The given configuration setting was invalid. /// - InvalidConfigurationSetting + InvalidConfigurationSetting, + + /// + /// A feature does not have any feature variants registered when attempting to resolve the variant. + /// + MissingFeatureVariant, } } diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index f2c63419..7db9f5c6 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -232,12 +232,47 @@ await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).Con public ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken) { - throw new NotImplementedException(); + if (string.IsNullOrEmpty(feature)) + { + throw new ArgumentNullException(nameof(feature)); + } + + return GetVariantAsync(feature, null, false, cancellationToken); } public ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken) { - throw new NotImplementedException(); + if (string.IsNullOrEmpty(feature)) + { + throw new ArgumentNullException(nameof(feature)); + } + + return GetVariantAsync(feature, context, true, cancellationToken); + } + + private async ValueTask GetVariantAsync(string feature, TContext context, bool useContext, CancellationToken cancellationToken) + { + 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 (featureDefinition.Variants == null) + { + throw new FeatureManagementException( + FeatureManagementError.MissingFeatureVariant, + $"No variants are registered for the feature {feature}"); + } + + FeatureVariant variant = null; + + } private void BindSettings(IFeatureFilterMetadata filter, FeatureFilterEvaluationContext context, int filterIndex) diff --git a/src/Microsoft.FeatureManagement/Group.cs b/src/Microsoft.FeatureManagement/Group.cs index 311c476f..c72f6135 100644 --- a/src/Microsoft.FeatureManagement/Group.cs +++ b/src/Microsoft.FeatureManagement/Group.cs @@ -14,7 +14,7 @@ public class Group /// /// The name of the variant. /// - public string Name { get; set; } + public string Variant { get; set; } /// /// diff --git a/src/Microsoft.FeatureManagement/Percentile.cs b/src/Microsoft.FeatureManagement/Percentile.cs index e7ff8e8c..fa104948 100644 --- a/src/Microsoft.FeatureManagement/Percentile.cs +++ b/src/Microsoft.FeatureManagement/Percentile.cs @@ -12,7 +12,7 @@ public class Percentile /// /// The name of the variant. /// - public string Name { get; set; } + public string Variant { get; set; } /// /// diff --git a/src/Microsoft.FeatureManagement/User.cs b/src/Microsoft.FeatureManagement/User.cs index 4336e420..bc56bf10 100644 --- a/src/Microsoft.FeatureManagement/User.cs +++ b/src/Microsoft.FeatureManagement/User.cs @@ -14,7 +14,7 @@ public class User /// /// The name of the variant. /// - public string Name { get; set; } + public string Variant { get; set; } /// /// From 703f89a77f2577cf8f3a2e5435670e8fd7c84aa0 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 5 Jul 2023 14:51:31 -0500 Subject: [PATCH 08/80] progress adding getvariant logic classes --- ...textualFeatureVariantAllocatorEvaluator.cs | 127 ++++++++++++++++++ .../FeatureVariantAllocationContext.cs | 16 +++ .../IContextualFeatureVariantAllocator.cs | 24 ++++ .../IFeatureVariantAllocator.cs | 22 +++ .../IFeatureVariantAllocatorMetadata.cs | 12 ++ 5 files changed, 201 insertions(+) create mode 100644 src/Microsoft.FeatureManagement/ContextualFeatureVariantAllocatorEvaluator.cs create mode 100644 src/Microsoft.FeatureManagement/FeatureVariantAllocationContext.cs create mode 100644 src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs create mode 100644 src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs create mode 100644 src/Microsoft.FeatureManagement/IFeatureVariantAllocatorMetadata.cs diff --git a/src/Microsoft.FeatureManagement/ContextualFeatureVariantAllocatorEvaluator.cs b/src/Microsoft.FeatureManagement/ContextualFeatureVariantAllocatorEvaluator.cs new file mode 100644 index 00000000..092ecc89 --- /dev/null +++ b/src/Microsoft.FeatureManagement/ContextualFeatureVariantAllocatorEvaluator.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement +{ + /// + /// Provides a performance efficient method of evaluating without knowing what the generic type parameter is. + /// + sealed class ContextualFeatureVariantAllocatorEvaluator : IContextualFeatureVariantAllocator + { + private IFeatureVariantAllocatorMetadata _allocator; + private Func> _evaluateFunc; + + public ContextualFeatureVariantAllocatorEvaluator(IFeatureVariantAllocatorMetadata allocator, Type appContextType) + { + if (allocator == null) + { + throw new ArgumentNullException(nameof(allocator)); + } + + if (appContextType == null) + { + throw new ArgumentNullException(nameof(appContextType)); + } + + Type targetInterface = GetContextualAllocatorInterface(allocator, appContextType); + + // + // Extract IContextualFeatureVariantAllocator.AllocateVariantAsync method. + if (targetInterface != null) + { + MethodInfo evaluateMethod = targetInterface.GetMethod(nameof(IContextualFeatureVariantAllocator.AllocateVariantAsync), BindingFlags.Public | BindingFlags.Instance); + + _evaluateFunc = TypeAgnosticEvaluate(allocator.GetType(), evaluateMethod); + } + + _allocator = allocator; + } + + public ValueTask AllocateVariantAsync(FeatureVariantAllocationContext allocationContext, object context, CancellationToken cancellationToken) + { + if (allocationContext == null) + { + throw new ArgumentNullException(nameof(allocationContext)); + } + + if (_evaluateFunc == null) + { + return new ValueTask((FeatureVariant)null); + } + + return _evaluateFunc(_allocator, allocationContext, context, cancellationToken); + } + + public static bool IsContextualVariantAllocator(IFeatureVariantAllocatorMetadata allocator, Type appContextType) + { + if (allocator == null) + { + throw new ArgumentNullException(nameof(allocator)); + } + + if (appContextType == null) + { + throw new ArgumentNullException(nameof(appContextType)); + } + + return GetContextualAllocatorInterface(allocator, appContextType) != null; + } + + private static Type GetContextualAllocatorInterface(IFeatureVariantAllocatorMetadata allocator, Type appContextType) + { + IEnumerable contextualAllocatorInterfaces = allocator.GetType().GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition().IsAssignableFrom(typeof(IContextualFeatureVariantAllocator<>))); + + Type targetInterface = null; + + if (contextualAllocatorInterfaces != null) + { + targetInterface = contextualAllocatorInterfaces.FirstOrDefault(i => i.GetGenericArguments()[0].IsAssignableFrom(appContextType)); + } + + return targetInterface; + } + + private static Func> TypeAgnosticEvaluate(Type allocatorType, MethodInfo method) + { + // + // Get the generic version of the evaluation helper method + MethodInfo genericHelper = typeof(ContextualFeatureVariantAllocatorEvaluator).GetMethod(nameof(GenericTypeAgnosticEvaluate), + BindingFlags.Static | BindingFlags.NonPublic); + + // + // Create a type specific version of the evaluation helper method + MethodInfo constructedHelper = genericHelper.MakeGenericMethod + (allocatorType, + method.GetParameters()[0].ParameterType, + method.GetParameters()[1].ParameterType, + method.GetParameters()[2].ParameterType, + method.ReturnType); + + // + // Invoke the method to get the func + object typeAgnosticDelegate = constructedHelper.Invoke(null, new object[] { method }); + + return (Func>)typeAgnosticDelegate; + } + + private static Func> GenericTypeAgnosticEvaluate(MethodInfo method) + { + Func> func = + (Func>) + Delegate.CreateDelegate(typeof(Func>), method); + + Func> genericDelegate = + (object target, FeatureVariantAllocationContext param1, object param2, CancellationToken param3) => + func((TTarget)target, param1, (TParam2)param2, param3); + + return genericDelegate; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/FeatureVariantAllocationContext.cs b/src/Microsoft.FeatureManagement/FeatureVariantAllocationContext.cs new file mode 100644 index 00000000..1e5fe921 --- /dev/null +++ b/src/Microsoft.FeatureManagement/FeatureVariantAllocationContext.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +namespace Microsoft.FeatureManagement +{ + /// + /// Contextual information needed during the process of feature variant allocation + /// + public class FeatureVariantAllocationContext + { + /// + /// The definition of the feature in need of an allocated variant + /// + public FeatureDefinition FeatureDefinition { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs new file mode 100644 index 00000000..a59f7416 --- /dev/null +++ b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement +{ + /// + /// Provides a method to allocate a variant of a feature to be used based off of custom conditions. + /// + /// A custom type that the allocator requires to perform allocation + public interface IContextualFeatureVariantAllocator : IFeatureVariantAllocatorMetadata + { + /// + /// Allocate a variant of a feature to be used based off of customized criteria. + /// + /// A variant allocation context that contains information needed to allocate 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 allocating a variant of a feature. + /// The cancellation token to cancel the operation. + /// The variant that should be allocated for a given feature. + ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, TContext appContext, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs new file mode 100644 index 00000000..1ae9cc67 --- /dev/null +++ b/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement +{ + /// + /// Provides a method to allocate a variant of a dynamic feature to be used based off of custom conditions. + /// + public interface IFeatureVariantAllocator : IFeatureVariantAllocatorMetadata + { + /// + /// Allocate a variant of a dynamic feature to be used based off of customized criteria. + /// + /// A variant allocation context that contains information needed to allocate a variant for a dynamic feature. + /// The cancellation token to cancel the operation. + /// The variant that should be allocated for a given dynamic feature. + ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantAllocatorMetadata.cs b/src/Microsoft.FeatureManagement/IFeatureVariantAllocatorMetadata.cs new file mode 100644 index 00000000..78f956b0 --- /dev/null +++ b/src/Microsoft.FeatureManagement/IFeatureVariantAllocatorMetadata.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +namespace Microsoft.FeatureManagement +{ + /// + /// Marker interface for feature variant allocators used to allocate which variant should be used for a dynamic feature. + /// + public interface IFeatureVariantAllocatorMetadata + { + } +} \ No newline at end of file From e225d2271eec9271d2713baedcb307a06011701d Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 5 Jul 2023 15:12:59 -0500 Subject: [PATCH 09/80] continued --- .../AllocatorAliasAttribute.cs | 32 ++ .../FeatureManager.cs | 2 +- ...textualTargetingFeatureVariantAllocator.cs | 105 +++++++ .../Targeting/TargetingEvaluator.cs | 274 ++++++++++++++++++ .../TargetingFeatureVariantAllocator.cs | 68 +++++ 5 files changed, 480 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.FeatureManagement/AllocatorAliasAttribute.cs create mode 100644 src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs create mode 100644 src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs create mode 100644 src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs diff --git a/src/Microsoft.FeatureManagement/AllocatorAliasAttribute.cs b/src/Microsoft.FeatureManagement/AllocatorAliasAttribute.cs new file mode 100644 index 00000000..ae3c5584 --- /dev/null +++ b/src/Microsoft.FeatureManagement/AllocatorAliasAttribute.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System; + +namespace Microsoft.FeatureManagement +{ + /// + /// Allows the name of an to be customized to relate to the name specified in configuration. + /// + public class AllocatorAliasAttribute : Attribute + { + /// + /// Creates an allocator alias using the provided alias. + /// + /// The alias of the feature variant allocator. + public AllocatorAliasAttribute(string alias) + { + if (string.IsNullOrEmpty(alias)) + { + throw new ArgumentNullException(nameof(alias)); + } + + Alias = alias; + } + + /// + /// The name that will be used to match feature feature variant allocator specified in the configuration. + /// + public string Alias { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 7db9f5c6..825fcb3f 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -272,7 +272,7 @@ private async ValueTask GetVariantAsync(string featu FeatureVariant variant = null; - + //TODO } private void BindSettings(IFeatureFilterMetadata filter, FeatureFilterEvaluationContext context, int filterIndex) diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs new file mode 100644 index 00000000..f496b06f --- /dev/null +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.Extensions.Options; +using Microsoft.FeatureManagement.FeatureFilters; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement.Allocators +{ + /// + /// A feature variant allocator that can be used to allocate a variant based on targeted audiences. + /// + [AllocatorAlias(Alias)] + public class ContextualTargetingFeatureVariantAllocator : IContextualFeatureVariantAllocator + { + private const string Alias = "Microsoft.Targeting"; + private readonly TargetingEvaluationOptions _options; + + /// + /// Creates a targeting contextual feature filter. + /// + /// Options controlling the behavior of the targeting evaluation performed by the filter. + public ContextualTargetingFeatureVariantAllocator(IOptions options) + { + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Allocates one of the variants configured for a feature based off the provided targeting context. + /// + /// Contextual information available for use during the allocation process. + /// The targeting context used to determine which variant should be allocated. + /// The cancellation token to cancel the operation. + /// + public ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, ITargetingContext targetingContext, CancellationToken cancellationToken) + { + if (variantAllocationContext == null) + { + throw new ArgumentNullException(nameof(variantAllocationContext)); + } + + if (targetingContext == null) + { + throw new ArgumentNullException(nameof(targetingContext)); + } + + FeatureDefinition featureDefinition = variantAllocationContext.FeatureDefinition; + + if (featureDefinition == null) + { + throw new ArgumentException( + $"{nameof(variantAllocationContext)}.{nameof(variantAllocationContext.FeatureDefinition)} cannot be null.", + nameof(variantAllocationContext)); + } + + if (featureDefinition.Variants == null) + { + throw new ArgumentException( + $"{nameof(variantAllocationContext)}.{nameof(variantAllocationContext.FeatureDefinition)}.{nameof(featureDefinition.Variants)} cannot be null.", + nameof(variantAllocationContext)); + } + + //TODO + + return new ValueTask((FeatureVariant)null); + } + + /// + /// Accumulates percentages for groups of an audience. + /// + /// The groups that will have their percentages updated based on currently accumulated percentages + /// The current cumulative rollout percentage for each group + private static void AccumulateGroups(IEnumerable groups, Dictionary cumulativeGroups) + { + foreach (GroupRollout gr in groups) + { + double percentage = gr.RolloutPercentage; + + if (cumulativeGroups.TryGetValue(gr.Name, out double p)) + { + percentage += p; + } + + cumulativeGroups[gr.Name] = percentage; + + gr.RolloutPercentage = percentage; + } + } + + /// + /// Accumulates percentages for the default rollout of an audience. + /// + /// The audience that will have its percentages updated based on currently accumulated percentages + /// The current cumulative default rollout percentage + private static void AccumulateDefaultRollout(Audience audience, ref double cumulativeDefaultPercentage) + { + cumulativeDefaultPercentage = cumulativeDefaultPercentage + audience.DefaultRolloutPercentage; + + audience.DefaultRolloutPercentage = cumulativeDefaultPercentage; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs new file mode 100644 index 00000000..85d913dc --- /dev/null +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs @@ -0,0 +1,274 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.FeatureManagement.FeatureFilters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace Microsoft.FeatureManagement.Targeting +{ + static class TargetingEvaluator + { + private static StringComparison GetComparisonType(bool ignoreCase) => + ignoreCase ? + StringComparison.OrdinalIgnoreCase : + StringComparison.Ordinal; + + /// + /// Checks if a provided targeting context should be targeted given targeting settings. + /// + public static bool IsTargeted(ITargetingContext targetingContext, TargetingFilterSettings settings, bool ignoreCase, string hint) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (targetingContext == null) + { + throw new ArgumentNullException(nameof(targetingContext)); + } + + if (!TryValidateSettings(settings, out string paramName, out string reason)) + { + throw new ArgumentException(reason, paramName); + } + + // + // Check if the user is being targeted directly + if (settings.Audience.Users != null && + IsTargeted( + targetingContext, + settings.Audience.Users, + ignoreCase)) + { + return true; + } + + // + // Check if the user is in a group that is being targeted + if (settings.Audience.Groups != null && + IsTargeted( + targetingContext, + settings.Audience.Groups, + ignoreCase, + hint)) + { + return true; + } + + // + // Check if the user is being targeted by a default rollout percentage + return IsTargeted( + targetingContext, + settings.Audience.DefaultRolloutPercentage, + ignoreCase, + hint); + } + + /// + /// Determines if a targeting context is targeted by presence in a list of users + /// + public static bool IsTargeted( + ITargetingContext targetingContext, + IEnumerable users, + bool ignoreCase) + { + if (targetingContext == null) + { + throw new ArgumentNullException(nameof(targetingContext)); + } + + if (users == null) + { + throw new ArgumentNullException(nameof(users)); + } + + if (targetingContext.UserId != null && + users.Any(user => targetingContext.UserId.Equals(user, GetComparisonType(ignoreCase)))) + { + return true; + } + + return false; + } + + /// + /// Determine if a targeting context is targeted by presence in a group + /// + public static bool IsTargeted( + ITargetingContext targetingContext, + IEnumerable groups, + bool ignoreCase, + string hint) + { + if (targetingContext == null) + { + throw new ArgumentNullException(nameof(targetingContext)); + } + + if (groups == null) + { + throw new ArgumentNullException(nameof(groups)); + } + + if (string.IsNullOrEmpty(hint)) + { + throw new ArgumentNullException(nameof(hint)); + } + + string userId = ignoreCase ? + targetingContext.UserId.ToLower() : + targetingContext.UserId; + + if (targetingContext.Groups != null) + { + IEnumerable normalizedGroups = ignoreCase ? + targetingContext.Groups.Select(g => g.ToLower()) : + targetingContext.Groups; + + foreach (string group in normalizedGroups) + { + GroupRollout groupRollout = groups.FirstOrDefault(g => g.Name.Equals(group, GetComparisonType(ignoreCase))); + + if (groupRollout != null) + { + string audienceContextId = $"{userId}\n{hint}\n{group}"; + + if (IsTargeted(audienceContextId, groupRollout.RolloutPercentage)) + { + return true; + } + } + } + } + + return false; + } + + /// + /// Determines if a targeting context is targeted by presence in a default rollout percentage. + /// + public static bool IsTargeted( + ITargetingContext targetingContext, + double defaultRolloutPercentage, + bool ignoreCase, + string hint) + { + if (targetingContext == null) + { + throw new ArgumentNullException(nameof(targetingContext)); + } + + if (string.IsNullOrEmpty(hint)) + { + throw new ArgumentNullException(nameof(hint)); + } + + string userId = ignoreCase ? + targetingContext.UserId.ToLower() : + targetingContext.UserId; + + string defaultContextId = $"{userId}\n{hint}"; + + return IsTargeted(defaultContextId, defaultRolloutPercentage); + } + + /// + /// Performs validation of targeting settings. + /// + /// The settings to validate. + /// The name of the invalid setting, if any. + /// The reason that the setting is invalid. + /// True if the provided settings are valid. False if the provided settings are invalid. + public static bool TryValidateSettings(TargetingFilterSettings targetingSettings, out string paramName, out string reason) + { + const string OutOfRange = "The value is out of the accepted range."; + + const string RequiredParameter = "Value cannot be null."; + + paramName = null; + + reason = null; + + if (targetingSettings == null) + { + paramName = nameof(targetingSettings); + + reason = RequiredParameter; + + return false; + } + + if (targetingSettings.Audience == null) + { + paramName = nameof(targetingSettings.Audience); + + reason = RequiredParameter; + + return false; + } + + if (targetingSettings.Audience.DefaultRolloutPercentage < 0 || targetingSettings.Audience.DefaultRolloutPercentage > 100) + { + paramName = $"{targetingSettings.Audience}.{targetingSettings.Audience.DefaultRolloutPercentage}"; + + reason = OutOfRange; + + return false; + } + + if (targetingSettings.Audience.Groups != null) + { + int index = 0; + + foreach (GroupRollout groupRollout in targetingSettings.Audience.Groups) + { + index++; + + if (groupRollout.RolloutPercentage < 0 || groupRollout.RolloutPercentage > 100) + { + // + // Audience.Groups[1].RolloutPercentage + paramName = $"{targetingSettings.Audience}.{targetingSettings.Audience.Groups}[{index}].{groupRollout.RolloutPercentage}"; + + reason = OutOfRange; + + return false; + } + } + } + + return true; + } + + + /// + /// Determines if a given context id should be targeted based off the provided percentage + /// + /// A context identifier that determines what the percentage is applicable for + /// The total percentage of possible context identifiers that should be targeted + /// A boolean representing if the context identifier should be targeted + private static bool IsTargeted(string contextId, double percentage) + { + byte[] hash; + + using (HashAlgorithm hashAlgorithm = SHA256.Create()) + { + hash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(contextId)); + } + + // + // Use first 4 bytes for percentage calculation + // Cryptographic hashing algorithms ensure adequate entropy across hash + uint contextMarker = BitConverter.ToUInt32(hash, 0); + + double contextPercentage = (contextMarker / (double)uint.MaxValue) * 100; + + return contextPercentage < percentage; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs new file mode 100644 index 00000000..31802f35 --- /dev/null +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.FeatureManagement.FeatureFilters; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement.Allocators +{ + /// + /// A feature variant allocator that can be used to allocate a variant based on targeted audiences. + /// + [AllocatorAlias(Alias)] + public class TargetingFeatureVariantAllocator : IFeatureVariantAllocator + { + private const string Alias = "Microsoft.Targeting"; + private readonly ITargetingContextAccessor _contextAccessor; + private readonly IContextualFeatureVariantAllocator _contextualResolver; + private readonly ILogger _logger; + + /// + /// Creates a feature variant allocator that uses targeting to allocate which of a dynamic feature's registered variants should be used. + /// + /// The options controlling how targeting is performed. + /// An accessor for the targeting context required to perform a targeting evaluation. + /// A logger factory for producing logs. + public TargetingFeatureVariantAllocator(IOptions options, + ITargetingContextAccessor contextAccessor, + ILoggerFactory loggerFactory) + { + _contextAccessor = contextAccessor ?? throw new ArgumentNullException(nameof(contextAccessor)); + _contextualResolver = new ContextualTargetingFeatureVariantAllocator(options); + _logger = loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)); + } + + /// + /// Allocates one of the variants configured for a feature based off the provided targeting context. + /// + /// Contextual information available for use during the allocation process. + /// The cancellation token to cancel the operation. + /// + public async ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, CancellationToken cancellationToken) + { + if (variantAllocationContext == null) + { + throw new ArgumentNullException(nameof(variantAllocationContext)); + } + + // + // Acquire targeting context via accessor + TargetingContext targetingContext = await _contextAccessor.GetContextAsync(cancellationToken).ConfigureAwait(false); + + // + // Ensure targeting can be performed + if (targetingContext == null) + { + _logger.LogWarning("No targeting context available for targeting evaluation."); + + return null; + } + + return await _contextualResolver.AllocateVariantAsync(variantAllocationContext, targetingContext, cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file From c233ec6230ad4cc5c5ce2fcfbdb8d8f46d8e636c Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 5 Jul 2023 15:21:27 -0500 Subject: [PATCH 10/80] remove repeated code in contextual targeting --- .../Targeting/ContextualTargetingFilter.cs | 185 +----------------- 1 file changed, 9 insertions(+), 176 deletions(-) diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs index 16d986f7..205e73da 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs @@ -2,60 +2,40 @@ // Licensed under the MIT license. // using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.FeatureManagement.Targeting; using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Text; using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters { /// - /// A feature filter that can be used to activate features for targeted audiences. + /// A feature filter that can be used to activate feature flags for targeted audiences. /// [FilterAlias(Alias)] - public class ContextualTargetingFilter : IContextualFeatureFilter, IFilterParametersBinder + public class ContextualTargetingFilter : IContextualFeatureFilter { private const string Alias = "Microsoft.Targeting"; private readonly TargetingEvaluationOptions _options; - private readonly ILogger _logger; /// /// Creates a targeting contextual feature filter. /// /// Options controlling the behavior of the targeting evaluation performed by the filter. - /// A logger factory for creating loggers. - public ContextualTargetingFilter(IOptions options, ILoggerFactory loggerFactory) + public ContextualTargetingFilter(IOptions options) { _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); - _logger = loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)); - } - - private StringComparison ComparisonType => _options.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - private StringComparer ComparerType => _options.IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; - - /// - /// Binds configuration representing filter parameters to . - /// - /// The configuration representing filter parameters that should be bound to . - /// that can later be used in targeting. - public object BindParameters(IConfiguration filterParameters) - { - return filterParameters.Get() ?? new TargetingFilterSettings(); } /// - /// Performs a targeting evaluation using the provided to determine if a feature should be enabled. + /// Performs a targeting evaluation using the provided to determine if a feature flag should be enabled. /// /// The feature evaluation context. /// The targeting context to use during targeting evaluation. /// The cancellation token to cancel the operation. /// Thrown if either or is null. - /// True if the feature is enabled, false otherwise. + /// True if the feature flag is enabled, false otherwise. public Task EvaluateAsync(FeatureFilterEvaluationContext context, ITargetingContext targetingContext, CancellationToken cancellationToken) { if (context == null) @@ -68,156 +48,9 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context, ITargeti throw new ArgumentNullException(nameof(targetingContext)); } - // - // Check if prebound settings available, otherwise bind from parameters. - TargetingFilterSettings settings = (TargetingFilterSettings)context.Settings ?? (TargetingFilterSettings)BindParameters(context.Parameters); - - if (!TryValidateSettings(settings, out string paramName, out string message)) - { - throw new ArgumentException(message, paramName); - } - - if (settings.Audience.Exclusion != null) - { - // - // Check if the user is in the exclusion directly - if (targetingContext.UserId != null && - settings.Audience.Exclusion.Users != null && - settings.Audience.Exclusion.Users.Any(user => targetingContext.UserId.Equals(user, ComparisonType))) - { - return Task.FromResult(false); - } - - // - // Check if the user is in a group within exclusion - if (targetingContext.Groups != null && - settings.Audience.Exclusion.Groups != null && - settings.Audience.Exclusion.Groups.Any(group => targetingContext.Groups.Contains(group, ComparerType))) - { - return Task.FromResult(false); - } - } - - // - // Check if the user is being targeted directly - if (targetingContext.UserId != null && - settings.Audience.Users != null && - settings.Audience.Users.Any(user => targetingContext.UserId.Equals(user, ComparisonType))) - { - return Task.FromResult(true); - } - - // - // Check if the user is in a group that is being targeted - if (targetingContext.Groups != null && - settings.Audience.Groups != null) - { - foreach (string group in targetingContext.Groups) - { - GroupRollout groupRollout = settings.Audience.Groups.FirstOrDefault(g => g.Name.Equals(group, ComparisonType)); - - if (groupRollout != null) - { - string audienceContextId = $"{targetingContext.UserId}\n{context.FeatureName}\n{group}"; - - if (IsTargeted(audienceContextId, groupRollout.RolloutPercentage)) - { - return Task.FromResult(true); - } - } - } - } - - // - // Check if the user is being targeted by a default rollout percentage - string defaultContextId = $"{targetingContext.UserId}\n{context.FeatureName}"; - - return Task.FromResult(IsTargeted(defaultContextId, settings.Audience.DefaultRolloutPercentage)); - } - - - /// - /// Determines if a given context id should be targeted based off the provided percentage - /// - /// A context identifier that determines what the percentage is applicable for - /// The total percentage of possible context identifiers that should be targeted - /// A boolean representing if the context identifier should be targeted - private bool IsTargeted(string contextId, double percentage) - { - byte[] hash; - - using (HashAlgorithm hashAlgorithm = SHA256.Create()) - { - hash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(contextId)); - } - - // - // Use first 4 bytes for percentage calculation - // Cryptographic hashing algorithms ensure adequate entropy across hash - uint contextMarker = BitConverter.ToUInt32(hash, 0); - - double contextPercentage = (contextMarker / (double)uint.MaxValue) * 100; - - return contextPercentage < percentage; - } - - /// - /// Performs validation of targeting settings. - /// - /// The settings to validate. - /// The name of the invalid setting, if any. - /// The reason that the setting is invalid. - /// True if the provided settings are valid. False if the provided settings are invalid. - private bool TryValidateSettings(TargetingFilterSettings settings, out string paramName, out string reason) - { - const string OutOfRange = "The value is out of the accepted range."; - - const string RequiredParameter = "Value cannot be null."; - - paramName = null; - - reason = null; - - if (settings.Audience == null) - { - paramName = nameof(settings.Audience); - - reason = RequiredParameter; - - return false; - } - - if (settings.Audience.DefaultRolloutPercentage < 0 || settings.Audience.DefaultRolloutPercentage > 100) - { - paramName = $"{settings.Audience}.{settings.Audience.DefaultRolloutPercentage}"; - - reason = OutOfRange; - - return false; - } - - if (settings.Audience.Groups != null) - { - int index = 0; - - foreach (GroupRollout groupRollout in settings.Audience.Groups) - { - index++; - - if (groupRollout.RolloutPercentage < 0 || groupRollout.RolloutPercentage > 100) - { - // - // Audience.Groups[1].RolloutPercentage - paramName = $"{settings.Audience}.{settings.Audience.Groups}[{index}].{groupRollout.RolloutPercentage}"; - - reason = OutOfRange; - - return false; - } - } - } + TargetingFilterSettings settings = context.Parameters.Get() ?? new TargetingFilterSettings(); - return true; + return Task.FromResult(TargetingEvaluator.IsTargeted(targetingContext, settings, _options.IgnoreCase, context.FeatureName)); } } -} +} \ No newline at end of file From 3a8bac1e20eed91b939ae6ae6e90c7b1f0bd1783 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 5 Jul 2023 16:30:00 -0500 Subject: [PATCH 11/80] fix version of contextual filter --- .../FeatureManager.cs | 57 +++- ...textualTargetingFeatureVariantAllocator.cs | 37 +-- .../Targeting/ContextualTargetingFilter.cs | 183 +++++++++++- .../Targeting/TargetingEvaluator.cs | 274 ------------------ 4 files changed, 232 insertions(+), 319 deletions(-) delete mode 100644 src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 825fcb3f..c7258670 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -272,7 +272,62 @@ private async ValueTask GetVariantAsync(string featu FeatureVariant variant = null; - //TODO + IFeatureVariantAllocatorMetadata allocator = GetFeatureVariantAllocatorMetadata(feature); + + if (allocator == null) + { + throw new FeatureManagementException( + FeatureManagementError.MissingFeatureVariantAllocator, + $"The feature variant allocator for feature '{feature}' was not found."); + } + + var allocationContext = new FeatureVariantAllocationContext() + { + FeatureDefinition = featureDefinition + }; + + // + // IFeatureVariantAllocator + if (allocator is IFeatureVariantAllocator featureVariantAllocator) + { + variant = await featureVariantAllocator.AllocateVariantAsync(allocationContext, cancellationToken).ConfigureAwait(false); + } + // + // IContextualFeatureVariantAllocator + else if (useContext && + TryGetContextualFeatureVariantAllocator(featureDefinition.Name, typeof(TContext), out ContextualFeatureVariantAllocatorEvaluator contextualAllocator)) + { + variant = await contextualAllocator.AllocateVariantAsync(allocationContext, context, cancellationToken).ConfigureAwait(false); + } + // + // The allocator doesn't implement a feature variant allocator interface capable of performing the evaluation + else + { + throw new FeatureManagementException( + FeatureManagementError.InvalidFeatureVariantAllocator, + useContext ? + $"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."); + } + + // TODO + + if (variant == null) + { + // throw something? + } + + // logic to figure out whether to return ConfigurationValue or resolve ConfigurationReference. + } + + private IFeatureVariantAllocatorMetadata GetFeatureVariantAllocatorMetadata(string featureName) + { + throw new NotImplementedException(); + } + + private bool TryGetContextualFeatureVariantAllocator(string featureName, Type appContextType, out ContextualFeatureVariantAllocatorEvaluator contextualAllocator) + { + throw new NotImplementedException(); } private void BindSettings(IFeatureFilterMetadata filter, FeatureFilterEvaluationContext context, int filterIndex) diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs index f496b06f..67293706 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Options; using Microsoft.FeatureManagement.FeatureFilters; using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -63,43 +62,11 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo nameof(variantAllocationContext)); } - //TODO - - return new ValueTask((FeatureVariant)null); - } - - /// - /// Accumulates percentages for groups of an audience. - /// - /// The groups that will have their percentages updated based on currently accumulated percentages - /// The current cumulative rollout percentage for each group - private static void AccumulateGroups(IEnumerable groups, Dictionary cumulativeGroups) - { - foreach (GroupRollout gr in groups) - { - double percentage = gr.RolloutPercentage; - if (cumulativeGroups.TryGetValue(gr.Name, out double p)) - { - percentage += p; - } - cumulativeGroups[gr.Name] = percentage; - - gr.RolloutPercentage = percentage; - } - } - - /// - /// Accumulates percentages for the default rollout of an audience. - /// - /// The audience that will have its percentages updated based on currently accumulated percentages - /// The current cumulative default rollout percentage - private static void AccumulateDefaultRollout(Audience audience, ref double cumulativeDefaultPercentage) - { - cumulativeDefaultPercentage = cumulativeDefaultPercentage + audience.DefaultRolloutPercentage; + //TODO - audience.DefaultRolloutPercentage = cumulativeDefaultPercentage; + return new ValueTask((FeatureVariant)null); } } } \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs index 205e73da..ee603a2c 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs @@ -2,40 +2,58 @@ // Licensed under the MIT license. // using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.FeatureManagement.Targeting; using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters { /// - /// A feature filter that can be used to activate feature flags for targeted audiences. + /// A feature filter that can be used to activate features for targeted audiences. /// [FilterAlias(Alias)] - public class ContextualTargetingFilter : IContextualFeatureFilter + public class ContextualTargetingFilter : IContextualFeatureFilter, IFilterParametersBinder { private const string Alias = "Microsoft.Targeting"; private readonly TargetingEvaluationOptions _options; + private readonly ILogger _logger; /// /// Creates a targeting contextual feature filter. /// /// Options controlling the behavior of the targeting evaluation performed by the filter. - public ContextualTargetingFilter(IOptions options) + /// A logger factory for creating loggers. + public ContextualTargetingFilter(IOptions options, ILoggerFactory loggerFactory) { _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + _logger = loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)); + } + + private StringComparison ComparisonType => _options.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + private StringComparer ComparerType => _options.IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; + + /// + /// Binds configuration representing filter parameters to . + /// + /// The configuration representing filter parameters that should be bound to . + /// that can later be used in targeting. + public object BindParameters(IConfiguration filterParameters) + { + return filterParameters.Get() ?? new TargetingFilterSettings(); } /// - /// Performs a targeting evaluation using the provided to determine if a feature flag should be enabled. + /// Performs a targeting evaluation using the provided to determine if a feature should be enabled. /// /// The feature evaluation context. /// The targeting context to use during targeting evaluation. - /// The cancellation token to cancel the operation. /// Thrown if either or is null. - /// True if the feature flag is enabled, false otherwise. + /// True if the feature is enabled, false otherwise. public Task EvaluateAsync(FeatureFilterEvaluationContext context, ITargetingContext targetingContext, CancellationToken cancellationToken) { if (context == null) @@ -48,9 +66,156 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context, ITargeti throw new ArgumentNullException(nameof(targetingContext)); } - TargetingFilterSettings settings = context.Parameters.Get() ?? new TargetingFilterSettings(); + // + // Check if prebound settings available, otherwise bind from parameters. + TargetingFilterSettings settings = (TargetingFilterSettings)context.Settings ?? (TargetingFilterSettings)BindParameters(context.Parameters); + + if (!TryValidateSettings(settings, out string paramName, out string message)) + { + throw new ArgumentException(message, paramName); + } + + if (settings.Audience.Exclusion != null) + { + // + // Check if the user is in the exclusion directly + if (targetingContext.UserId != null && + settings.Audience.Exclusion.Users != null && + settings.Audience.Exclusion.Users.Any(user => targetingContext.UserId.Equals(user, ComparisonType))) + { + return Task.FromResult(false); + } + + // + // Check if the user is in a group within exclusion + if (targetingContext.Groups != null && + settings.Audience.Exclusion.Groups != null && + settings.Audience.Exclusion.Groups.Any(group => targetingContext.Groups.Contains(group, ComparerType))) + { + return Task.FromResult(false); + } + } + + // + // Check if the user is being targeted directly + if (targetingContext.UserId != null && + settings.Audience.Users != null && + settings.Audience.Users.Any(user => targetingContext.UserId.Equals(user, ComparisonType))) + { + return Task.FromResult(true); + } + + // + // Check if the user is in a group that is being targeted + if (targetingContext.Groups != null && + settings.Audience.Groups != null) + { + foreach (string group in targetingContext.Groups) + { + GroupRollout groupRollout = settings.Audience.Groups.FirstOrDefault(g => g.Name.Equals(group, ComparisonType)); + + if (groupRollout != null) + { + string audienceContextId = $"{targetingContext.UserId}\n{context.FeatureName}\n{group}"; + + if (IsTargeted(audienceContextId, groupRollout.RolloutPercentage)) + { + return Task.FromResult(true); + } + } + } + } + + // + // Check if the user is being targeted by a default rollout percentage + string defaultContextId = $"{targetingContext.UserId}\n{context.FeatureName}"; + + return Task.FromResult(IsTargeted(defaultContextId, settings.Audience.DefaultRolloutPercentage)); + } + + + /// + /// Determines if a given context id should be targeted based off the provided percentage + /// + /// A context identifier that determines what the percentage is applicable for + /// The total percentage of possible context identifiers that should be targeted + /// A boolean representing if the context identifier should be targeted + private bool IsTargeted(string contextId, double percentage) + { + byte[] hash; + + using (HashAlgorithm hashAlgorithm = SHA256.Create()) + { + hash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(contextId)); + } + + // + // Use first 4 bytes for percentage calculation + // Cryptographic hashing algorithms ensure adequate entropy across hash + uint contextMarker = BitConverter.ToUInt32(hash, 0); + + double contextPercentage = (contextMarker / (double)uint.MaxValue) * 100; + + return contextPercentage < percentage; + } + + /// + /// Performs validation of targeting settings. + /// + /// The settings to validate. + /// The name of the invalid setting, if any. + /// The reason that the setting is invalid. + /// True if the provided settings are valid. False if the provided settings are invalid. + private bool TryValidateSettings(TargetingFilterSettings settings, out string paramName, out string reason) + { + const string OutOfRange = "The value is out of the accepted range."; + + const string RequiredParameter = "Value cannot be null."; + + paramName = null; + + reason = null; + + if (settings.Audience == null) + { + paramName = nameof(settings.Audience); + + reason = RequiredParameter; + + return false; + } + + if (settings.Audience.DefaultRolloutPercentage < 0 || settings.Audience.DefaultRolloutPercentage > 100) + { + paramName = $"{settings.Audience}.{settings.Audience.DefaultRolloutPercentage}"; + + reason = OutOfRange; + + return false; + } + + if (settings.Audience.Groups != null) + { + int index = 0; + + foreach (GroupRollout groupRollout in settings.Audience.Groups) + { + index++; + + if (groupRollout.RolloutPercentage < 0 || groupRollout.RolloutPercentage > 100) + { + // + // Audience.Groups[1].RolloutPercentage + paramName = $"{settings.Audience}.{settings.Audience.Groups}[{index}].{groupRollout.RolloutPercentage}"; + + reason = OutOfRange; + + return false; + } + } + } - return Task.FromResult(TargetingEvaluator.IsTargeted(targetingContext, settings, _options.IgnoreCase, context.FeatureName)); + return true; } } } \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs deleted file mode 100644 index 85d913dc..00000000 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Microsoft.FeatureManagement.FeatureFilters; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Text; - -namespace Microsoft.FeatureManagement.Targeting -{ - static class TargetingEvaluator - { - private static StringComparison GetComparisonType(bool ignoreCase) => - ignoreCase ? - StringComparison.OrdinalIgnoreCase : - StringComparison.Ordinal; - - /// - /// Checks if a provided targeting context should be targeted given targeting settings. - /// - public static bool IsTargeted(ITargetingContext targetingContext, TargetingFilterSettings settings, bool ignoreCase, string hint) - { - if (settings == null) - { - throw new ArgumentNullException(nameof(settings)); - } - - if (targetingContext == null) - { - throw new ArgumentNullException(nameof(targetingContext)); - } - - if (!TryValidateSettings(settings, out string paramName, out string reason)) - { - throw new ArgumentException(reason, paramName); - } - - // - // Check if the user is being targeted directly - if (settings.Audience.Users != null && - IsTargeted( - targetingContext, - settings.Audience.Users, - ignoreCase)) - { - return true; - } - - // - // Check if the user is in a group that is being targeted - if (settings.Audience.Groups != null && - IsTargeted( - targetingContext, - settings.Audience.Groups, - ignoreCase, - hint)) - { - return true; - } - - // - // Check if the user is being targeted by a default rollout percentage - return IsTargeted( - targetingContext, - settings.Audience.DefaultRolloutPercentage, - ignoreCase, - hint); - } - - /// - /// Determines if a targeting context is targeted by presence in a list of users - /// - public static bool IsTargeted( - ITargetingContext targetingContext, - IEnumerable users, - bool ignoreCase) - { - if (targetingContext == null) - { - throw new ArgumentNullException(nameof(targetingContext)); - } - - if (users == null) - { - throw new ArgumentNullException(nameof(users)); - } - - if (targetingContext.UserId != null && - users.Any(user => targetingContext.UserId.Equals(user, GetComparisonType(ignoreCase)))) - { - return true; - } - - return false; - } - - /// - /// Determine if a targeting context is targeted by presence in a group - /// - public static bool IsTargeted( - ITargetingContext targetingContext, - IEnumerable groups, - bool ignoreCase, - string hint) - { - if (targetingContext == null) - { - throw new ArgumentNullException(nameof(targetingContext)); - } - - if (groups == null) - { - throw new ArgumentNullException(nameof(groups)); - } - - if (string.IsNullOrEmpty(hint)) - { - throw new ArgumentNullException(nameof(hint)); - } - - string userId = ignoreCase ? - targetingContext.UserId.ToLower() : - targetingContext.UserId; - - if (targetingContext.Groups != null) - { - IEnumerable normalizedGroups = ignoreCase ? - targetingContext.Groups.Select(g => g.ToLower()) : - targetingContext.Groups; - - foreach (string group in normalizedGroups) - { - GroupRollout groupRollout = groups.FirstOrDefault(g => g.Name.Equals(group, GetComparisonType(ignoreCase))); - - if (groupRollout != null) - { - string audienceContextId = $"{userId}\n{hint}\n{group}"; - - if (IsTargeted(audienceContextId, groupRollout.RolloutPercentage)) - { - return true; - } - } - } - } - - return false; - } - - /// - /// Determines if a targeting context is targeted by presence in a default rollout percentage. - /// - public static bool IsTargeted( - ITargetingContext targetingContext, - double defaultRolloutPercentage, - bool ignoreCase, - string hint) - { - if (targetingContext == null) - { - throw new ArgumentNullException(nameof(targetingContext)); - } - - if (string.IsNullOrEmpty(hint)) - { - throw new ArgumentNullException(nameof(hint)); - } - - string userId = ignoreCase ? - targetingContext.UserId.ToLower() : - targetingContext.UserId; - - string defaultContextId = $"{userId}\n{hint}"; - - return IsTargeted(defaultContextId, defaultRolloutPercentage); - } - - /// - /// Performs validation of targeting settings. - /// - /// The settings to validate. - /// The name of the invalid setting, if any. - /// The reason that the setting is invalid. - /// True if the provided settings are valid. False if the provided settings are invalid. - public static bool TryValidateSettings(TargetingFilterSettings targetingSettings, out string paramName, out string reason) - { - const string OutOfRange = "The value is out of the accepted range."; - - const string RequiredParameter = "Value cannot be null."; - - paramName = null; - - reason = null; - - if (targetingSettings == null) - { - paramName = nameof(targetingSettings); - - reason = RequiredParameter; - - return false; - } - - if (targetingSettings.Audience == null) - { - paramName = nameof(targetingSettings.Audience); - - reason = RequiredParameter; - - return false; - } - - if (targetingSettings.Audience.DefaultRolloutPercentage < 0 || targetingSettings.Audience.DefaultRolloutPercentage > 100) - { - paramName = $"{targetingSettings.Audience}.{targetingSettings.Audience.DefaultRolloutPercentage}"; - - reason = OutOfRange; - - return false; - } - - if (targetingSettings.Audience.Groups != null) - { - int index = 0; - - foreach (GroupRollout groupRollout in targetingSettings.Audience.Groups) - { - index++; - - if (groupRollout.RolloutPercentage < 0 || groupRollout.RolloutPercentage > 100) - { - // - // Audience.Groups[1].RolloutPercentage - paramName = $"{targetingSettings.Audience}.{targetingSettings.Audience.Groups}[{index}].{groupRollout.RolloutPercentage}"; - - reason = OutOfRange; - - return false; - } - } - } - - return true; - } - - - /// - /// Determines if a given context id should be targeted based off the provided percentage - /// - /// A context identifier that determines what the percentage is applicable for - /// The total percentage of possible context identifiers that should be targeted - /// A boolean representing if the context identifier should be targeted - private static bool IsTargeted(string contextId, double percentage) - { - byte[] hash; - - using (HashAlgorithm hashAlgorithm = SHA256.Create()) - { - hash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(contextId)); - } - - // - // Use first 4 bytes for percentage calculation - // Cryptographic hashing algorithms ensure adequate entropy across hash - uint contextMarker = BitConverter.ToUInt32(hash, 0); - - double contextPercentage = (contextMarker / (double)uint.MaxValue) * 100; - - return contextPercentage < percentage; - } - } -} \ No newline at end of file From fa2133caa93892d1f9d40529fbda68b89e54ea3e Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 6 Jul 2023 14:46:16 -0500 Subject: [PATCH 12/80] more progress on getting the contextual allocator to work --- .../FeatureManagementError.cs | 9 ++ .../FeatureManager.cs | 88 ++++++++++++++++--- src/Microsoft.FeatureManagement/NameHelper.cs | 59 +++++++++++++ ...textualTargetingFeatureVariantAllocator.cs | 5 +- 4 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/NameHelper.cs diff --git a/src/Microsoft.FeatureManagement/FeatureManagementError.cs b/src/Microsoft.FeatureManagement/FeatureManagementError.cs index 2c389eb7..e08460cc 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementError.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementError.cs @@ -37,5 +37,14 @@ public enum FeatureManagementError /// A feature does not have any feature variants registered when attempting to resolve the variant. /// MissingFeatureVariant, + + /// + /// The feature variant allocator configured for the feature being evaluated is an ambiguous reference to multiple registered feature variant allocators. + /// + AmbiguousFeatureVariantAllocator, + + MissingFeatureVariantAllocator, + + InvalidFeatureVariantAllocator } } diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index c7258670..6dee3986 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.WindowsRuntime; using System.Threading; using System.Threading.Tasks; @@ -26,6 +27,9 @@ class FeatureManager : IFeatureManager, IDisposable, IVariantFeatureManager private readonly IFeatureDefinitionProvider _featureDefinitionProvider; private readonly IEnumerable _featureFilters; private readonly IEnumerable _sessionManagers; + private readonly IEnumerable _variantAllocators; + private readonly ConcurrentDictionary _allocatorMetadataCache; + private readonly ConcurrentDictionary _contextualFeatureVariantAllocatorCache; private readonly ILogger _logger; private readonly ConcurrentDictionary _filterMetadataCache; private readonly ConcurrentDictionary _contextualFeatureFilterCache; @@ -43,12 +47,16 @@ public FeatureManager( IFeatureDefinitionProvider featureDefinitionProvider, IEnumerable featureFilters, IEnumerable sessionManagers, + IEnumerable variantAllocators, ILoggerFactory loggerFactory, IOptions options) { _featureDefinitionProvider = featureDefinitionProvider; _featureFilters = featureFilters ?? throw new ArgumentNullException(nameof(featureFilters)); _sessionManagers = sessionManagers ?? throw new ArgumentNullException(nameof(sessionManagers)); + _variantAllocators = variantAllocators ?? throw new ArgumentNullException(nameof(variantAllocators)); + _allocatorMetadataCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + _contextualFeatureVariantAllocatorCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); _logger = loggerFactory.CreateLogger(); _filterMetadataCache = new ConcurrentDictionary(); _contextualFeatureFilterCache = new ConcurrentDictionary(); @@ -237,7 +245,7 @@ public ValueTask GetVariantAsync(string feature, CancellationToken canc throw new ArgumentNullException(nameof(feature)); } - return GetVariantAsync(feature, null, false, cancellationToken); + return GetVariantAsync(feature, null, false, cancellationToken); } public ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken) @@ -247,10 +255,10 @@ public ValueTask GetVariantAsync(string feature, TContext con throw new ArgumentNullException(nameof(feature)); } - return GetVariantAsync(feature, context, true, cancellationToken); + return GetVariantAsync(feature, context, true, cancellationToken); } - private async ValueTask GetVariantAsync(string feature, TContext context, bool useContext, CancellationToken cancellationToken) + private async ValueTask GetVariantAsync(string feature, TContext context, bool useContext, CancellationToken cancellationToken) { FeatureDefinition featureDefinition = await _featureDefinitionProvider .GetFeatureDefinitionAsync(feature, cancellationToken) @@ -272,7 +280,9 @@ private async ValueTask GetVariantAsync(string featu FeatureVariant variant = null; - IFeatureVariantAllocatorMetadata allocator = GetFeatureVariantAllocatorMetadata(feature); + const string allocatorName = "Targeting"; + + IFeatureVariantAllocatorMetadata allocator = GetFeatureVariantAllocatorMetadata(allocatorName); if (allocator == null) { @@ -295,7 +305,7 @@ private async ValueTask GetVariantAsync(string featu // // IContextualFeatureVariantAllocator else if (useContext && - TryGetContextualFeatureVariantAllocator(featureDefinition.Name, typeof(TContext), out ContextualFeatureVariantAllocatorEvaluator contextualAllocator)) + TryGetContextualFeatureVariantAllocator(allocatorName, typeof(TContext), out ContextualFeatureVariantAllocatorEvaluator contextualAllocator)) { variant = await contextualAllocator.AllocateVariantAsync(allocationContext, context, cancellationToken).ConfigureAwait(false); } @@ -306,8 +316,8 @@ private async ValueTask GetVariantAsync(string featu throw new FeatureManagementException( FeatureManagementError.InvalidFeatureVariantAllocator, useContext ? - $"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."); + $"The feature variant allocator '{allocatorName}' specified for the feature '{feature}' is not capable of evaluating the requested feature with the provided context." : + $"The feature variant allocator '{allocatorName}' specified for the feature '{feature}' is not capable of evaluating the requested feature."); } // TODO @@ -317,17 +327,71 @@ private async ValueTask GetVariantAsync(string featu // throw something? } - // logic to figure out whether to return ConfigurationValue or resolve ConfigurationReference. + // logic to figure out whether to return ConfigurationValue or resolve ConfigurationReference + + + Variant returnVariant = new Variant(); + + return returnVariant; } - private IFeatureVariantAllocatorMetadata GetFeatureVariantAllocatorMetadata(string featureName) + private IFeatureVariantAllocatorMetadata GetFeatureVariantAllocatorMetadata(string allocatorName) { - throw new NotImplementedException(); + const string allocatorSuffix = "allocator"; + + IFeatureVariantAllocatorMetadata allocator = _allocatorMetadataCache.GetOrAdd( + allocatorName, + (_) => { + + IEnumerable matchingAllocators = _variantAllocators.Where(a => + { + Type allocatorType = a.GetType(); + + string name = ((AllocatorAliasAttribute)Attribute.GetCustomAttribute(allocatorType, typeof(AllocatorAliasAttribute)))?.Alias; + + if (name == null) + { + name = allocatorType.Name; + } + + return NameHelper.IsMatchingReference( + reference: allocatorName, + metadataName: name, + suffix: allocatorSuffix); + }); + + if (matchingAllocators.Count() > 1) + { + throw new FeatureManagementException(FeatureManagementError.AmbiguousFeatureVariantAllocator, $"Multiple feature variant allocators match the configured allocator named '{allocatorName}'."); + } + + return matchingAllocators.FirstOrDefault(); + } + ); + + return allocator; } - private bool TryGetContextualFeatureVariantAllocator(string featureName, Type appContextType, out ContextualFeatureVariantAllocatorEvaluator contextualAllocator) + private bool TryGetContextualFeatureVariantAllocator(string allocatorName, Type appContextType, out ContextualFeatureVariantAllocatorEvaluator contextualAllocator) { - throw new NotImplementedException(); + if (appContextType == null) + { + throw new ArgumentNullException(nameof(appContextType)); + } + + contextualAllocator = _contextualFeatureVariantAllocatorCache.GetOrAdd( + $"{allocatorName}{Environment.NewLine}{appContextType.FullName}", + (_) => { + + IFeatureVariantAllocatorMetadata metadata = GetFeatureVariantAllocatorMetadata(allocatorName); + + return ContextualFeatureVariantAllocatorEvaluator.IsContextualVariantAllocator(metadata, appContextType) ? + new ContextualFeatureVariantAllocatorEvaluator(metadata, appContextType) : + null; + } + ); + + return contextualAllocator != null; } private void BindSettings(IFeatureFilterMetadata filter, FeatureFilterEvaluationContext context, int filterIndex) diff --git a/src/Microsoft.FeatureManagement/NameHelper.cs b/src/Microsoft.FeatureManagement/NameHelper.cs new file mode 100644 index 00000000..d36b9cfa --- /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 allocator reference matches a given feature filter/allocator 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 allocator + /// An optional suffix that may be included when referencing the metadata type. E.g. "filter" or "allocator". + /// 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/allocator can be referenced with or without their associated suffix ('filter' or 'allocator') + // 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/allocators 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); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs index 67293706..8645541a 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Options; using Microsoft.FeatureManagement.FeatureFilters; using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -62,11 +63,11 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo nameof(variantAllocationContext)); } - + FeatureVariant variant = featureDefinition.Variants.First(); //TODO - return new ValueTask((FeatureVariant)null); + return new ValueTask(variant); } } } \ No newline at end of file From 86ff346d188cb518e16cca34476c65a331ac0d69 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 6 Jul 2023 15:47:43 -0500 Subject: [PATCH 13/80] about to test getvariant --- examples/FeatureFlagDemo/Controllers/HomeController.cs | 4 ++-- .../ConfigurationFeatureDefinitionProvider.cs | 1 + src/Microsoft.FeatureManagement/FeatureManager.cs | 7 +++++-- src/Microsoft.FeatureManagement/Variant.cs | 5 +++++ 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/examples/FeatureFlagDemo/Controllers/HomeController.cs b/examples/FeatureFlagDemo/Controllers/HomeController.cs index a455d987..2aa56778 100644 --- a/examples/FeatureFlagDemo/Controllers/HomeController.cs +++ b/examples/FeatureFlagDemo/Controllers/HomeController.cs @@ -14,7 +14,7 @@ namespace FeatureFlagDemo.Controllers { public class HomeController : Controller { - private readonly IFeatureManager _featureManager; + private readonly IVariantFeatureManager _featureManager; public HomeController(IFeatureManagerSnapshot featureSnapshot) { @@ -24,7 +24,7 @@ public HomeController(IFeatureManagerSnapshot featureSnapshot) [FeatureGate(MyFeatureFlags.Home)] public async Task Index() { - bool test = await _featureManager.IsEnabledAsync(nameof(MyFeatureFlags.Banner)); + bool test = await _featureManager.GetVariantAsync(nameof(MyFeatureFlags.Banner)); return View(); } diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 97af20dc..1649ec1e 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -183,6 +183,7 @@ We support } } + // fix logic for when not to set allocation/detect when not set IConfigurationSection allocationSection = configurationSection.GetSection(nameof(FeatureDefinition.Allocation)); allocation = new Allocation(); allocationSection.Bind(allocation); diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 6dee3986..26814dbc 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -329,8 +329,11 @@ private async ValueTask GetVariantAsync(string feature, TCont // logic to figure out whether to return ConfigurationValue or resolve ConfigurationReference - - Variant returnVariant = new Variant(); + Variant returnVariant = new Variant() + { + Name = variant.Name, + ConfigurationValue = variant.ConfigurationValue + }; return returnVariant; } diff --git a/src/Microsoft.FeatureManagement/Variant.cs b/src/Microsoft.FeatureManagement/Variant.cs index 2edeae5c..356925ba 100644 --- a/src/Microsoft.FeatureManagement/Variant.cs +++ b/src/Microsoft.FeatureManagement/Variant.cs @@ -19,5 +19,10 @@ public class Variant /// /// public IConfiguration Configuration { get; set; } + + /// + /// + /// + public string ConfigurationValue { get; set; } } } From c445e469e6f93504182cd01533681817e7268936 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 10 Jul 2023 11:28:46 -0500 Subject: [PATCH 14/80] add example to test --- examples/FeatureFlagDemo/Controllers/HomeController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/FeatureFlagDemo/Controllers/HomeController.cs b/examples/FeatureFlagDemo/Controllers/HomeController.cs index a455d987..854ae80f 100644 --- a/examples/FeatureFlagDemo/Controllers/HomeController.cs +++ b/examples/FeatureFlagDemo/Controllers/HomeController.cs @@ -24,7 +24,7 @@ public HomeController(IFeatureManagerSnapshot featureSnapshot) [FeatureGate(MyFeatureFlags.Home)] public async Task Index() { - bool test = await _featureManager.IsEnabledAsync(nameof(MyFeatureFlags.Banner)); + bool test = await _featureManager.IsEnabledAsync(nameof(MyFeatureFlags.Banner), CancellationToken.None); return View(); } From 0a4dcfb840c5dd5a226590cb76725de637de739b Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 10 Jul 2023 11:31:58 -0500 Subject: [PATCH 15/80] add snapshot changes --- .../Controllers/HomeController.cs | 4 +- .../FeatureManager.cs | 1 - .../FeatureManagerSnapshot.cs | 50 +++++++++++++++++-- .../IVariantFeatureManagerSnapshot.cs | 12 +++++ 4 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/IVariantFeatureManagerSnapshot.cs diff --git a/examples/FeatureFlagDemo/Controllers/HomeController.cs b/examples/FeatureFlagDemo/Controllers/HomeController.cs index 2aa56778..4c377fac 100644 --- a/examples/FeatureFlagDemo/Controllers/HomeController.cs +++ b/examples/FeatureFlagDemo/Controllers/HomeController.cs @@ -16,7 +16,7 @@ public class HomeController : Controller { private readonly IVariantFeatureManager _featureManager; - public HomeController(IFeatureManagerSnapshot featureSnapshot) + public HomeController(IVariantFeatureManagerSnapshot featureSnapshot) { _featureManager = featureSnapshot; } @@ -24,7 +24,7 @@ public HomeController(IFeatureManagerSnapshot featureSnapshot) [FeatureGate(MyFeatureFlags.Home)] public async Task Index() { - bool test = await _featureManager.GetVariantAsync(nameof(MyFeatureFlags.Banner)); + Variant test = await _featureManager.GetVariantAsync(nameof(MyFeatureFlags.Banner), CancellationToken.None); return View(); } diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 26814dbc..ebb1ac27 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -10,7 +10,6 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices.WindowsRuntime; using System.Threading; using System.Threading.Tasks; diff --git a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs index 056ce79e..3ff2181e 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs @@ -13,13 +13,14 @@ namespace Microsoft.FeatureManagement /// /// Provides a snapshot of feature state to ensure consistency across a given request. /// - class FeatureManagerSnapshot : IFeatureManagerSnapshot + class FeatureManagerSnapshot : IFeatureManagerSnapshot, IVariantFeatureManagerSnapshot { - private readonly IFeatureManager _featureManager; + private readonly FeatureManager _featureManager; private readonly ConcurrentDictionary> _flagCache = new ConcurrentDictionary>(); + private readonly IDictionary _variantCache = new Dictionary(); private IEnumerable _featureNames; - public FeatureManagerSnapshot(IFeatureManager featureManager) + public FeatureManagerSnapshot(FeatureManager featureManager) { _featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager)); } @@ -55,7 +56,48 @@ public Task IsEnabledAsync(string feature, TContext context, Can { return _flagCache.GetOrAdd( feature, - (key) => _featureManager.IsEnabledAsync(key, context)); + (key) => _featureManager.IsEnabledAsync(key, context, cancellationToken)); + } + + public async ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken) + { + string cacheKey = GetVariantCacheKey(feature); + + // + // First, check local cache + if (_variantCache.ContainsKey(feature)) + { + return _variantCache[cacheKey]; + } + + Variant variant = await _featureManager.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 _variantCache[cacheKey]; + } + + Variant variant = await _featureManager.GetVariantAsync(feature, context, cancellationToken).ConfigureAwait(false); + + _variantCache[cacheKey] = variant; + + return variant; + } + + private string GetVariantCacheKey(string feature) + { + return $"{typeof(Variant).FullName}\n{feature}"; } } } diff --git a/src/Microsoft.FeatureManagement/IVariantFeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/IVariantFeatureManagerSnapshot.cs new file mode 100644 index 00000000..a54c3ce7 --- /dev/null +++ b/src/Microsoft.FeatureManagement/IVariantFeatureManagerSnapshot.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 IVariantFeatureManagerSnapshot : IVariantFeatureManager + { + } +} \ No newline at end of file From 34375e601664977e625cca6730db4cd713da41b4 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 10 Jul 2023 13:37:11 -0500 Subject: [PATCH 16/80] variant can be detected and retrieved from getvariantasync --- examples/FeatureFlagDemo/Startup.cs | 2 ++ .../FeatureManagementBuilder.cs | 23 +++++++++++++++++++ .../IFeatureManagementBuilder.cs | 9 ++++++++ .../IVariantFeatureManager.cs | 8 +++---- .../ServiceCollectionExtensions.cs | 12 ++++++++-- 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/examples/FeatureFlagDemo/Startup.cs b/examples/FeatureFlagDemo/Startup.cs index 65c0777c..68c726b5 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.Allocators; using Microsoft.FeatureManagement.FeatureFilters; namespace FeatureFlagDemo @@ -59,6 +60,7 @@ public void ConfigureServices(IServiceCollection services) .AddFeatureFilter() .AddFeatureFilter() .AddFeatureFilter() + .AddFeatureVariantAllocator() .UseDisabledFeaturesHandler(new FeatureNotEnabledDisabledHandler()); services.AddMvc(o => diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs index a433b2bc..a17e1c64 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs @@ -43,6 +43,29 @@ public IFeatureManagementBuilder AddFeatureFilter() where T : IFeatureFilterM return this; } + public IFeatureManagementBuilder AddFeatureVariantAllocator() where T : IFeatureVariantAllocatorMetadata + { + Type serviceType = typeof(IFeatureVariantAllocatorMetadata); + + Type implementationType = typeof(T); + + IEnumerable featureVariantAssignerImplementations = implementationType.GetInterfaces() + .Where(i => i == typeof(IFeatureVariantAllocator) || + (i.IsGenericType && i.GetGenericTypeDefinition().IsAssignableFrom(typeof(IContextualFeatureVariantAllocator<>)))); + + if (featureVariantAssignerImplementations.Count() > 1) + { + throw new ArgumentException($"A single feature variant allocator cannot implement more than one feature variant allocator interface.", nameof(T)); + } + + if (!Services.Any(descriptor => descriptor.ServiceType == serviceType && descriptor.ImplementationType == implementationType)) + { + Services.AddSingleton(typeof(IFeatureVariantAllocatorMetadata), typeof(T)); + } + + return this; + } + public IFeatureManagementBuilder AddSessionManager() where T : ISessionManager { Services.AddSingleton(typeof(ISessionManager), typeof(T)); diff --git a/src/Microsoft.FeatureManagement/IFeatureManagementBuilder.cs b/src/Microsoft.FeatureManagement/IFeatureManagementBuilder.cs index 6365c098..885a0d9e 100644 --- a/src/Microsoft.FeatureManagement/IFeatureManagementBuilder.cs +++ b/src/Microsoft.FeatureManagement/IFeatureManagementBuilder.cs @@ -24,6 +24,15 @@ public interface IFeatureManagementBuilder /// The feature management builder. IFeatureManagementBuilder AddFeatureFilter() where T : IFeatureFilterMetadata; + /// + /// Adds a given feature variant allocator to the list of feature variant allocators that will be available to allocate feature variants during runtime. + /// Possible feature variant allocator metadata types include and + /// Only one feature variant allocator interface can be implemented by a single type. + /// + /// An implementation of + /// The feature management builder. + IFeatureManagementBuilder AddFeatureVariantAllocator() where T : IFeatureVariantAllocatorMetadata; + /// /// Adds an to be used for storing feature state in a session. /// diff --git a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs index 20d4f657..dda96275 100644 --- a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs @@ -14,7 +14,7 @@ public interface IVariantFeatureManager /// The name of the feature to check. /// The cancellation token to cancel the operation. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature, CancellationToken cancellationToken); + Task IsEnabledAsync(string feature, CancellationToken cancellationToken = default); /// /// Checks whether a given feature is enabled. @@ -23,7 +23,7 @@ public interface IVariantFeatureManager /// 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. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken); + Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken = default); /// /// Gets the assigned variant for a specfic feature. @@ -31,7 +31,7 @@ public interface IVariantFeatureManager /// The name of the feature from which the variant will be assigned. /// The cancellation token to cancel the operation. /// A variant assigned to the user based on the feature's allocation logic. - ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken); + ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken = default); /// /// Gets the assigned variant for a specfic feature. @@ -40,6 +40,6 @@ public interface IVariantFeatureManager /// A context providing information that can be used to evaluate which variant the user will be assigned. /// The cancellation token to cancel the operation. /// A variant assigned to the user based on the feature's allocation logic. - ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken); + ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 507f1394..8f368728 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -26,11 +26,19 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec // Add required services services.TryAddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + + services.TryAddSingleton(sp => sp.GetRequiredService()); + + services.TryAddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(); - services.AddScoped(); + services.AddSingleton(); + + services.TryAddSingleton(sp => sp.GetRequiredService()); + + services.TryAddSingleton(sp => sp.GetRequiredService()); return new FeatureManagementBuilder(services); } From 1f6318be330e1a355ed9e7d21735df74bd7efec1 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 10 Jul 2023 17:10:06 -0500 Subject: [PATCH 17/80] progress on allocation logic, add comments where consideration needed --- examples/FeatureFlagDemo/appsettings.json | 2 +- .../FeatureManager.cs | 1 + ...textualTargetingFeatureVariantAllocator.cs | 69 +++- .../Targeting/ContextualTargetingFilter.cs | 157 +------- .../Targeting/TargetingEvaluator.cs | 343 ++++++++++++++++++ 5 files changed, 416 insertions(+), 156 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs diff --git a/examples/FeatureFlagDemo/appsettings.json b/examples/FeatureFlagDemo/appsettings.json index 296b5bc6..6fb5ad88 100644 --- a/examples/FeatureFlagDemo/appsettings.json +++ b/examples/FeatureFlagDemo/appsettings.json @@ -91,7 +91,7 @@ { "Variant": "Big", "From": 0, - "To": 10 + "To": 100 } ], "Seed": 13973240 diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index ebb1ac27..8d533be5 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -88,6 +88,7 @@ public void Dispose() _parametersCache.Dispose(); } + // need way to differentiate between using IsEnabledAsync for variants and not variants private async Task IsEnabledAsync(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken) { foreach (ISessionManager sessionManager in _sessionManagers) diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs index 8645541a..247cdac7 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs @@ -3,6 +3,7 @@ // using Microsoft.Extensions.Options; using Microsoft.FeatureManagement.FeatureFilters; +using Microsoft.FeatureManagement.Targeting; using System; using System.Linq; using System.Threading; @@ -63,11 +64,75 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo nameof(variantAllocationContext)); } - FeatureVariant variant = featureDefinition.Variants.First(); + // check if feature is disabled, and if so just return DefaultWhenDisabled variant? IsEnabledAsync or just EvaluateAsync somewhere? don't want it to be a loop anyway + if (featureDefinition.Status == Status.Disabled || ) + { + if (!string.IsNullOrEmpty(featureDefinition.Allocation.DefaultWhenDisabled)) + { + FeatureVariant variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(featureDefinition.Allocation.DefaultWhenDisabled)); + + if (!string.IsNullOrEmpty(variant.Name)) + { + return new ValueTask(variant); + } + } + + return new ValueTask((FeatureVariant)null); + } + + foreach (User user in featureDefinition.Allocation.User) + { + if (TargetingEvaluator.IsTargeted(targetingContext, user.Users, _options.IgnoreCase)) + { + FeatureVariant variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(user.Variant)); + + if (!string.IsNullOrEmpty(variant.Name)) + { + return new ValueTask(variant); + } + } + } + + foreach (Group group in featureDefinition.Allocation.Group) + { + if (TargetingEvaluator.IsGroupTargeted(targetingContext, group.Groups, _options.IgnoreCase)) + { + FeatureVariant variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(group.Variant)); + + if (!string.IsNullOrEmpty(variant.Name)) + { + return new ValueTask(variant); + } + } + } + + // what to do if seed not specified? random int? + foreach (Percentile percentile in featureDefinition.Allocation.Percentile) + { + if (TargetingEvaluator.IsTargeted(targetingContext, percentile.From, percentile.To, featureDefinition.Allocation.Seed, _options.IgnoreCase, featureDefinition.Name)) + { + FeatureVariant variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(percentile.Variant)); + + if (!string.IsNullOrEmpty(variant.Name)) + { + return new ValueTask(variant); + } + } + } + + if (!string.IsNullOrEmpty(featureDefinition.Allocation.DefaultWhenEnabled)) + { + FeatureVariant variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(featureDefinition.Allocation.DefaultWhenEnabled)); + + if (!string.IsNullOrEmpty(variant.Name)) + { + return new ValueTask(variant); + } + } //TODO - return new ValueTask(variant); + return new ValueTask((FeatureVariant)null); } } } \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs index ee603a2c..f9feb873 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs @@ -4,17 +4,15 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.FeatureManagement.Targeting; using System; -using System.Linq; -using System.Security.Cryptography; -using System.Text; using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters { /// - /// A feature filter that can be used to activate features for targeted audiences. + /// A feature filter that can be used to activate feature for targeted audiences. /// [FilterAlias(Alias)] public class ContextualTargetingFilter : IContextualFeatureFilter, IFilterParametersBinder @@ -34,9 +32,6 @@ public ContextualTargetingFilter(IOptions options, I _logger = loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)); } - private StringComparison ComparisonType => _options.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - private StringComparer ComparerType => _options.IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; - /// /// Binds configuration representing filter parameters to . /// @@ -52,6 +47,7 @@ public object BindParameters(IConfiguration filterParameters) /// /// The feature evaluation context. /// The targeting context to use during targeting evaluation. + /// The cancellation token to cancel the operation. /// Thrown if either or is null. /// True if the feature is enabled, false otherwise. public Task EvaluateAsync(FeatureFilterEvaluationContext context, ITargetingContext targetingContext, CancellationToken cancellationToken) @@ -70,152 +66,7 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context, ITargeti // Check if prebound settings available, otherwise bind from parameters. TargetingFilterSettings settings = (TargetingFilterSettings)context.Settings ?? (TargetingFilterSettings)BindParameters(context.Parameters); - if (!TryValidateSettings(settings, out string paramName, out string message)) - { - throw new ArgumentException(message, paramName); - } - - if (settings.Audience.Exclusion != null) - { - // - // Check if the user is in the exclusion directly - if (targetingContext.UserId != null && - settings.Audience.Exclusion.Users != null && - settings.Audience.Exclusion.Users.Any(user => targetingContext.UserId.Equals(user, ComparisonType))) - { - return Task.FromResult(false); - } - - // - // Check if the user is in a group within exclusion - if (targetingContext.Groups != null && - settings.Audience.Exclusion.Groups != null && - settings.Audience.Exclusion.Groups.Any(group => targetingContext.Groups.Contains(group, ComparerType))) - { - return Task.FromResult(false); - } - } - - // - // Check if the user is being targeted directly - if (targetingContext.UserId != null && - settings.Audience.Users != null && - settings.Audience.Users.Any(user => targetingContext.UserId.Equals(user, ComparisonType))) - { - return Task.FromResult(true); - } - - // - // Check if the user is in a group that is being targeted - if (targetingContext.Groups != null && - settings.Audience.Groups != null) - { - foreach (string group in targetingContext.Groups) - { - GroupRollout groupRollout = settings.Audience.Groups.FirstOrDefault(g => g.Name.Equals(group, ComparisonType)); - - if (groupRollout != null) - { - string audienceContextId = $"{targetingContext.UserId}\n{context.FeatureName}\n{group}"; - - if (IsTargeted(audienceContextId, groupRollout.RolloutPercentage)) - { - return Task.FromResult(true); - } - } - } - } - - // - // Check if the user is being targeted by a default rollout percentage - string defaultContextId = $"{targetingContext.UserId}\n{context.FeatureName}"; - - return Task.FromResult(IsTargeted(defaultContextId, settings.Audience.DefaultRolloutPercentage)); - } - - - /// - /// Determines if a given context id should be targeted based off the provided percentage - /// - /// A context identifier that determines what the percentage is applicable for - /// The total percentage of possible context identifiers that should be targeted - /// A boolean representing if the context identifier should be targeted - private bool IsTargeted(string contextId, double percentage) - { - byte[] hash; - - using (HashAlgorithm hashAlgorithm = SHA256.Create()) - { - hash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(contextId)); - } - - // - // Use first 4 bytes for percentage calculation - // Cryptographic hashing algorithms ensure adequate entropy across hash - uint contextMarker = BitConverter.ToUInt32(hash, 0); - - double contextPercentage = (contextMarker / (double)uint.MaxValue) * 100; - - return contextPercentage < percentage; - } - - /// - /// Performs validation of targeting settings. - /// - /// The settings to validate. - /// The name of the invalid setting, if any. - /// The reason that the setting is invalid. - /// True if the provided settings are valid. False if the provided settings are invalid. - private bool TryValidateSettings(TargetingFilterSettings settings, out string paramName, out string reason) - { - const string OutOfRange = "The value is out of the accepted range."; - - const string RequiredParameter = "Value cannot be null."; - - paramName = null; - - reason = null; - - if (settings.Audience == null) - { - paramName = nameof(settings.Audience); - - reason = RequiredParameter; - - return false; - } - - if (settings.Audience.DefaultRolloutPercentage < 0 || settings.Audience.DefaultRolloutPercentage > 100) - { - paramName = $"{settings.Audience}.{settings.Audience.DefaultRolloutPercentage}"; - - reason = OutOfRange; - - return false; - } - - if (settings.Audience.Groups != null) - { - int index = 0; - - foreach (GroupRollout groupRollout in settings.Audience.Groups) - { - index++; - - if (groupRollout.RolloutPercentage < 0 || groupRollout.RolloutPercentage > 100) - { - // - // Audience.Groups[1].RolloutPercentage - paramName = $"{settings.Audience}.{settings.Audience.Groups}[{index}].{groupRollout.RolloutPercentage}"; - - reason = OutOfRange; - - return false; - } - } - } - - return true; + return Task.FromResult(TargetingEvaluator.IsTargeted(targetingContext, settings, _options.IgnoreCase, context.FeatureName)); } } } \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs new file mode 100644 index 00000000..c267a3bb --- /dev/null +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs @@ -0,0 +1,343 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.FeatureManagement.FeatureFilters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.FeatureManagement.Targeting +{ + static class TargetingEvaluator + { + private static StringComparison GetComparisonType(bool ignoreCase) => + ignoreCase ? + StringComparison.OrdinalIgnoreCase : + StringComparison.Ordinal; + + /// + /// Checks if a provided targeting context should be targeted given targeting settings. + /// + public static bool IsTargeted(ITargetingContext targetingContext, TargetingFilterSettings settings, bool ignoreCase, string hint) + { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (targetingContext == null) + { + throw new ArgumentNullException(nameof(targetingContext)); + } + + if (!TryValidateSettings(settings, out string paramName, out string reason)) + { + throw new ArgumentException(reason, paramName); + } + + // + // Check if the user is being targeted directly + if (settings.Audience.Users != null && + IsTargeted( + targetingContext, + settings.Audience.Users, + ignoreCase)) + { + return true; + } + + // + // Check if the user is in a group that is being targeted + if (settings.Audience.Groups != null && + IsTargeted( + targetingContext, + settings.Audience.Groups, + ignoreCase, + hint)) + { + return true; + } + + // + // Check if the user is being targeted by a default rollout percentage + return IsTargeted( + targetingContext, + settings.Audience.DefaultRolloutPercentage, + ignoreCase, + hint); + } + + /// + /// Determines if a targeting context is targeted by presence in a list of users + /// + public static bool IsTargeted( + ITargetingContext targetingContext, + IEnumerable users, + bool ignoreCase) + { + if (targetingContext == null) + { + throw new ArgumentNullException(nameof(targetingContext)); + } + + if (users == null) + { + throw new ArgumentNullException(nameof(users)); + } + + if (targetingContext.UserId != null && + users.Any(user => targetingContext.UserId.Equals(user, GetComparisonType(ignoreCase)))) + { + return true; + } + + return false; + } + + /// + /// Determines if a targeting context is targeted by presence in a list of groups + /// + public static bool IsGroupTargeted( + ITargetingContext targetingContext, + IEnumerable groups, + bool ignoreCase) + { + if (targetingContext == null) + { + throw new ArgumentNullException(nameof(targetingContext)); + } + + if (groups == null) + { + throw new ArgumentNullException(nameof(groups)); + } + + if (targetingContext.Groups != null) + { + IEnumerable normalizedGroups = ignoreCase ? + targetingContext.Groups.Select(g => g.ToLower()) : + targetingContext.Groups; + + foreach (string group in normalizedGroups) + { + string allocationGroup = groups.FirstOrDefault(g => g.Equals(group, GetComparisonType(ignoreCase))); + + if (allocationGroup != null) + { + return true; + } + } + } + + return false; + } + + /// + /// Determine if a targeting context is targeted by presence in a group + /// + public static bool IsTargeted( + ITargetingContext targetingContext, + IEnumerable groups, + bool ignoreCase, + string hint) + { + if (targetingContext == null) + { + throw new ArgumentNullException(nameof(targetingContext)); + } + + if (groups == null) + { + throw new ArgumentNullException(nameof(groups)); + } + + if (string.IsNullOrEmpty(hint)) + { + throw new ArgumentNullException(nameof(hint)); + } + + string userId = ignoreCase ? + targetingContext.UserId.ToLower() : + targetingContext.UserId; + + if (targetingContext.Groups != null) + { + IEnumerable normalizedGroups = ignoreCase ? + targetingContext.Groups.Select(g => g.ToLower()) : + targetingContext.Groups; + + foreach (string group in normalizedGroups) + { + GroupRollout groupRollout = groups.FirstOrDefault(g => g.Name.Equals(group, GetComparisonType(ignoreCase))); + + if (groupRollout != null) + { + string audienceContextId = $"{userId}\n{hint}\n{group}"; + + if (IsTargeted(audienceContextId, groupRollout.RolloutPercentage)) + { + return true; + } + } + } + } + + return false; + } + + /// + /// Determines if a targeting context is targeted by presence in a default rollout percentage. + /// + public static bool IsTargeted( + ITargetingContext targetingContext, + double defaultRolloutPercentage, + bool ignoreCase, + string hint) + { + if (targetingContext == null) + { + throw new ArgumentNullException(nameof(targetingContext)); + } + + if (string.IsNullOrEmpty(hint)) + { + throw new ArgumentNullException(nameof(hint)); + } + + string userId = ignoreCase ? + targetingContext.UserId.ToLower() : + targetingContext.UserId; + + string defaultContextId = $"{userId}\n{hint}"; + + return IsTargeted(defaultContextId, defaultRolloutPercentage); + } + + /// + /// Performs validation of targeting settings. + /// + /// The settings to validate. + /// The name of the invalid setting, if any. + /// The reason that the setting is invalid. + /// True if the provided settings are valid. False if the provided settings are invalid. + public static bool TryValidateSettings(TargetingFilterSettings targetingSettings, out string paramName, out string reason) + { + const string OutOfRange = "The value is out of the accepted range."; + + const string RequiredParameter = "Value cannot be null."; + + paramName = null; + + reason = null; + + if (targetingSettings == null) + { + paramName = nameof(targetingSettings); + + reason = RequiredParameter; + + return false; + } + + if (targetingSettings.Audience == null) + { + paramName = nameof(targetingSettings.Audience); + + reason = RequiredParameter; + + return false; + } + + if (targetingSettings.Audience.DefaultRolloutPercentage < 0 || targetingSettings.Audience.DefaultRolloutPercentage > 100) + { + paramName = $"{targetingSettings.Audience}.{targetingSettings.Audience.DefaultRolloutPercentage}"; + + reason = OutOfRange; + + return false; + } + + if (targetingSettings.Audience.Groups != null) + { + int index = 0; + + foreach (GroupRollout groupRollout in targetingSettings.Audience.Groups) + { + index++; + + if (groupRollout.RolloutPercentage < 0 || groupRollout.RolloutPercentage > 100) + { + // + // Audience.Groups[1].RolloutPercentage + paramName = $"{targetingSettings.Audience}.{targetingSettings.Audience.Groups}[{index}].{groupRollout.RolloutPercentage}"; + + reason = OutOfRange; + + return false; + } + } + } + + return true; + } + + /// + /// Determines if a given context id should be targeted based off the percentage range given + /// + public static bool IsTargeted(ITargetingContext targetingContext, double from, double to, int seed, bool ignoreCase, string hint) + { + byte[] hash; + + byte[] seedBytes = BitConverter.GetBytes(seed); + + string userId = ignoreCase ? + targetingContext.UserId.ToLower() : + targetingContext.UserId; + + string contextId = $"{userId}\n{hint}"; + + using (HashAlgorithm hashAlgorithm = SHA256.Create()) + { + hash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(contextId).Concat(seedBytes).ToArray()); + } + + // + // Use first 4 bytes for percentage calculation + // Cryptographic hashing algorithms ensure adequate entropy across hash + uint contextMarker = BitConverter.ToUInt32(hash, 0); + + double contextPercentage = (contextMarker / (double)uint.MaxValue) * 100; + + return contextPercentage >= from && contextPercentage <= to; + } + + /// + /// Determines if a given context id should be targeted based off the provided percentage + /// + /// A context identifier that determines what the percentage is applicable for + /// The total percentage of possible context identifiers that should be targeted + /// A boolean representing if the context identifier should be targeted + private static bool IsTargeted(string contextId, double percentage) + { + byte[] hash; + + using (HashAlgorithm hashAlgorithm = SHA256.Create()) + { + hash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(contextId)); + } + + // + // Use first 4 bytes for percentage calculation + // Cryptographic hashing algorithms ensure adequate entropy across hash + uint contextMarker = BitConverter.ToUInt32(hash, 0); + + double contextPercentage = (contextMarker / (double)uint.MaxValue) * 100; + + return contextPercentage < percentage; + } + } +} \ No newline at end of file From 1f76adc66ad52392994b5cd07d155b49129b4d99 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 24 Jul 2023 14:10:00 -0500 Subject: [PATCH 18/80] add use of optionsresolver for reference, todo work on isenabledasync between customer use and variant use --- ...figurationFeatureVariantOptionsResolver.cs | 33 +++++++++++++++++++ .../FeatureManager.cs | 23 +++++++++---- .../IFeatureVariantOptionsResolver.cs | 24 ++++++++++++++ .../ServiceCollectionExtensions.cs | 2 ++ ...textualTargetingFeatureVariantAllocator.cs | 16 +++++---- 5 files changed, 84 insertions(+), 14 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs create mode 100644 src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs new file mode 100644 index 00000000..ee09ee3b --- /dev/null +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.Extensions.Configuration; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement +{ + /// + /// A feature variant options resolver that resolves options by reading configuration from the .NET Core system. + /// + sealed class ConfigurationFeatureVariantOptionsResolver : IFeatureVariantOptionsResolver + { + private readonly IConfiguration _configuration; + + public ConfigurationFeatureVariantOptionsResolver(IConfiguration configuration) + { + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + } + + public ValueTask GetOptionsAsync(FeatureDefinition featureDefinition, FeatureVariant variant, CancellationToken cancellationToken) + { + if (variant == null) + { + throw new ArgumentNullException(nameof(variant)); + } + + return new ValueTask(_configuration.GetSection($"{variant.ConfigurationReference}")); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 8d533be5..82cfbfaa 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -32,6 +32,7 @@ class FeatureManager : IFeatureManager, IDisposable, IVariantFeatureManager private readonly ILogger _logger; private readonly ConcurrentDictionary _filterMetadataCache; private readonly ConcurrentDictionary _contextualFeatureFilterCache; + private readonly IFeatureVariantOptionsResolver _variantOptionsResolver; private readonly FeatureManagementOptions _options; private readonly IMemoryCache _parametersCache; @@ -47,6 +48,7 @@ public FeatureManager( IEnumerable featureFilters, IEnumerable sessionManagers, IEnumerable variantAllocators, + IFeatureVariantOptionsResolver variantOptionsResolver, ILoggerFactory loggerFactory, IOptions options) { @@ -54,6 +56,7 @@ public FeatureManager( _featureFilters = featureFilters ?? throw new ArgumentNullException(nameof(featureFilters)); _sessionManagers = sessionManagers ?? throw new ArgumentNullException(nameof(sessionManagers)); _variantAllocators = variantAllocators ?? throw new ArgumentNullException(nameof(variantAllocators)); + _variantOptionsResolver = variantOptionsResolver ?? throw new ArgumentNullException(nameof(variantOptionsResolver)); _allocatorMetadataCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); _contextualFeatureVariantAllocatorCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); _logger = loggerFactory.CreateLogger(); @@ -65,12 +68,17 @@ public FeatureManager( public Task IsEnabledAsync(string feature, CancellationToken cancellationToken) { - return IsEnabledAsync(feature, null, false, cancellationToken); + return IsEnabledAsync(feature, null, false, false, cancellationToken); } public Task IsEnabledAsync(string feature, TContext appContext, CancellationToken cancellationToken) { - return IsEnabledAsync(feature, appContext, true, cancellationToken); + return IsEnabledAsync(feature, appContext, true, false, cancellationToken); + } + + public Task IsEnabledAsync(string feature, TContext appContext, bool ignoreVariant, CancellationToken cancellationToken) + { + return IsEnabledAsync(feature, appContext, true, ignoreVariant, cancellationToken); } public async IAsyncEnumerable GetFeatureNamesAsync([EnumeratorCancellation] CancellationToken cancellationToken) @@ -88,8 +96,8 @@ public void Dispose() _parametersCache.Dispose(); } - // need way to differentiate between using IsEnabledAsync for variants and not variants - private async Task IsEnabledAsync(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken) + // TODO use ignoreVariant to differentiate between customer use and variant use + private async Task IsEnabledAsync(string feature, TContext appContext, bool useAppContext, bool ignoreVariant, CancellationToken cancellationToken) { foreach (ISessionManager sessionManager in _sessionManagers) { @@ -278,7 +286,7 @@ private async ValueTask GetVariantAsync(string feature, TCont $"No variants are registered for the feature {feature}"); } - FeatureVariant variant = null; + FeatureVariant variant; const string allocatorName = "Targeting"; @@ -327,12 +335,13 @@ private async ValueTask GetVariantAsync(string feature, TCont // throw something? } - // logic to figure out whether to return ConfigurationValue or resolve ConfigurationReference + // logic to figure out how to resolve Configuration to one variable betwen value/reference Variant returnVariant = new Variant() { Name = variant.Name, - ConfigurationValue = variant.ConfigurationValue + ConfigurationValue = variant.ConfigurationValue, + Configuration = await _variantOptionsResolver.GetOptionsAsync(featureDefinition, variant, cancellationToken).ConfigureAwait(false) }; return returnVariant; diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs b/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs new file mode 100644 index 00000000..0de9c304 --- /dev/null +++ b/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.Extensions.Configuration; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement +{ + /// + /// Performs the resolution and binding necessary in the feature variant resolution process. + /// + public interface IFeatureVariantOptionsResolver + { + /// + /// Retrieves typed options for a given feature definition and chosen variant. + /// + /// The definition of the feature that the resolution is being performed for. + /// The chosen variant of the 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 = default); + } +} \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 8f368728..61ba3bfc 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -32,6 +32,8 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec services.TryAddSingleton(sp => sp.GetRequiredService()); + services.TryAddSingleton(); + services.AddSingleton(); services.AddSingleton(); diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs index 247cdac7..e9f80136 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs @@ -64,12 +64,14 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo nameof(variantAllocationContext)); } + FeatureVariant variant = null; + // check if feature is disabled, and if so just return DefaultWhenDisabled variant? IsEnabledAsync or just EvaluateAsync somewhere? don't want it to be a loop anyway - if (featureDefinition.Status == Status.Disabled || ) + if (featureDefinition.Status == Status.Disabled) // || what? TODO { if (!string.IsNullOrEmpty(featureDefinition.Allocation.DefaultWhenDisabled)) { - FeatureVariant variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(featureDefinition.Allocation.DefaultWhenDisabled)); + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(featureDefinition.Allocation.DefaultWhenDisabled)); if (!string.IsNullOrEmpty(variant.Name)) { @@ -84,7 +86,7 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo { if (TargetingEvaluator.IsTargeted(targetingContext, user.Users, _options.IgnoreCase)) { - FeatureVariant variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(user.Variant)); + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(user.Variant)); if (!string.IsNullOrEmpty(variant.Name)) { @@ -97,7 +99,7 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo { if (TargetingEvaluator.IsGroupTargeted(targetingContext, group.Groups, _options.IgnoreCase)) { - FeatureVariant variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(group.Variant)); + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(group.Variant)); if (!string.IsNullOrEmpty(variant.Name)) { @@ -111,7 +113,7 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo { if (TargetingEvaluator.IsTargeted(targetingContext, percentile.From, percentile.To, featureDefinition.Allocation.Seed, _options.IgnoreCase, featureDefinition.Name)) { - FeatureVariant variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(percentile.Variant)); + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(percentile.Variant)); if (!string.IsNullOrEmpty(variant.Name)) { @@ -122,7 +124,7 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo if (!string.IsNullOrEmpty(featureDefinition.Allocation.DefaultWhenEnabled)) { - FeatureVariant variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(featureDefinition.Allocation.DefaultWhenEnabled)); + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(featureDefinition.Allocation.DefaultWhenEnabled)); if (!string.IsNullOrEmpty(variant.Name)) { @@ -132,7 +134,7 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo //TODO - return new ValueTask((FeatureVariant)null); + return new ValueTask(variant); } } } \ No newline at end of file From ee98bbe5a510c2edc0920fdfe3c2a6bc97aa2e35 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 26 Jul 2023 12:57:57 -0500 Subject: [PATCH 19/80] All working except couple TODOs, need to add unit tests --- .../ConfigurationFeatureDefinitionProvider.cs | 2 +- ...textualFeatureVariantAllocatorEvaluator.cs | 24 +++--- .../FeatureManager.cs | 81 ++++++++++++------- .../IContextualFeatureVariantAllocator.cs | 3 +- .../IFeatureVariantAllocator.cs | 11 +-- ...textualTargetingFeatureVariantAllocator.cs | 11 ++- .../TargetingFeatureVariantAllocator.cs | 5 +- 7 files changed, 80 insertions(+), 57 deletions(-) diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 1649ec1e..6d568518 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -183,7 +183,7 @@ We support } } - // fix logic for when not to set allocation/detect when not set + // fix logic for when not to set allocation/detect when not set TODO IConfigurationSection allocationSection = configurationSection.GetSection(nameof(FeatureDefinition.Allocation)); allocation = new Allocation(); allocationSection.Bind(allocation); diff --git a/src/Microsoft.FeatureManagement/ContextualFeatureVariantAllocatorEvaluator.cs b/src/Microsoft.FeatureManagement/ContextualFeatureVariantAllocatorEvaluator.cs index 092ecc89..6d12b021 100644 --- a/src/Microsoft.FeatureManagement/ContextualFeatureVariantAllocatorEvaluator.cs +++ b/src/Microsoft.FeatureManagement/ContextualFeatureVariantAllocatorEvaluator.cs @@ -16,7 +16,7 @@ namespace Microsoft.FeatureManagement sealed class ContextualFeatureVariantAllocatorEvaluator : IContextualFeatureVariantAllocator { private IFeatureVariantAllocatorMetadata _allocator; - private Func> _evaluateFunc; + private Func> _evaluateFunc; public ContextualFeatureVariantAllocatorEvaluator(IFeatureVariantAllocatorMetadata allocator, Type appContextType) { @@ -44,7 +44,7 @@ public ContextualFeatureVariantAllocatorEvaluator(IFeatureVariantAllocatorMetada _allocator = allocator; } - public ValueTask AllocateVariantAsync(FeatureVariantAllocationContext allocationContext, object context, CancellationToken cancellationToken) + public ValueTask AllocateVariantAsync(FeatureVariantAllocationContext allocationContext, object context, bool isFeatureEnabled, CancellationToken cancellationToken) { if (allocationContext == null) { @@ -56,7 +56,7 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo return new ValueTask((FeatureVariant)null); } - return _evaluateFunc(_allocator, allocationContext, context, cancellationToken); + return _evaluateFunc(_allocator, allocationContext, context, isFeatureEnabled, cancellationToken); } public static bool IsContextualVariantAllocator(IFeatureVariantAllocatorMetadata allocator, Type appContextType) @@ -88,7 +88,7 @@ private static Type GetContextualAllocatorInterface(IFeatureVariantAllocatorMeta return targetInterface; } - private static Func> TypeAgnosticEvaluate(Type allocatorType, MethodInfo method) + private static Func> TypeAgnosticEvaluate(Type allocatorType, MethodInfo method) { // // Get the generic version of the evaluation helper method @@ -108,18 +108,18 @@ private static Func>)typeAgnosticDelegate; + return (Func>)typeAgnosticDelegate; } - private static Func> GenericTypeAgnosticEvaluate(MethodInfo method) + private static Func> GenericTypeAgnosticEvaluate(MethodInfo method) { - Func> func = - (Func>) - Delegate.CreateDelegate(typeof(Func>), method); + Func> func = + (Func>) + Delegate.CreateDelegate(typeof(Func>), method); - Func> genericDelegate = - (object target, FeatureVariantAllocationContext param1, object param2, CancellationToken param3) => - func((TTarget)target, param1, (TParam2)param2, param3); + Func> genericDelegate = + (object target, FeatureVariantAllocationContext param1, object param2, bool param3, CancellationToken param4) => + func((TTarget)target, param1, (TParam2)param2, param3, param4); return genericDelegate; } diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 82cfbfaa..adcb804f 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -125,8 +125,8 @@ private async Task IsEnabledAsync(string feature, TContext appCo } // - // Treat an empty list of enabled filters as a disabled feature - if (featureDefinition.EnabledFor == null || !featureDefinition.EnabledFor.Any()) + // Treat an empty list of enabled filters or if status is disabled as a disabled feature + if (featureDefinition.EnabledFor == null || !featureDefinition.EnabledFor.Any() || featureDefinition.Status == Status.Disabled) { enabled = false; } @@ -159,16 +159,16 @@ private async Task IsEnabledAsync(string feature, TContext appCo enabled = true; break; } - + continue; } - // - // Handle On filters for variants - if (string.Equals(featureFilterConfiguration.Name, "On", StringComparison.OrdinalIgnoreCase)) - { - // TODO - } + //// + //// Handle On filters for variants + //if (string.Equals(featureFilterConfiguration.Name, "On", StringComparison.OrdinalIgnoreCase)) + //{ + // // TODO + //} IFeatureFilterMetadata filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name); @@ -215,7 +215,8 @@ await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).Con { BindSettings(filter, context, filterIndex); - if (await featureFilter.EvaluateAsync(context, cancellationToken).ConfigureAwait(false) == targetEvaluation) { + if (await featureFilter.EvaluateAsync(context, cancellationToken).ConfigureAwait(false) == targetEvaluation) + { enabled = targetEvaluation; break; @@ -223,6 +224,20 @@ await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).Con } } } + + if (!ignoreVariant && (featureDefinition.Variants?.Any() ?? false)) + { + FeatureVariant featureVariant = await GetFeatureVariantAsync(feature, featureDefinition, appContext, useAppContext, enabled, cancellationToken); + + if (featureVariant.StatusOverride == StatusOverride.Enabled) + { + enabled = true; + } + else if (featureVariant.StatusOverride == StatusOverride.Disabled) + { + enabled = false; + } + } } else { @@ -286,7 +301,29 @@ private async ValueTask GetVariantAsync(string feature, TCont $"No variants are registered for the feature {feature}"); } - FeatureVariant variant; + bool isFeatureEnabled = await IsEnabledAsync(feature, context, true, cancellationToken).ConfigureAwait(false); + + FeatureVariant featureVariant = await GetFeatureVariantAsync(feature, featureDefinition, context, useContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); + + if (featureVariant == null) + { + return null; + } + + // logic to figure out how to resolve Configuration to one variable betwen value/reference TODO + Variant returnVariant = new Variant() + { + Name = featureVariant.Name, + ConfigurationValue = featureVariant.ConfigurationValue, + Configuration = await _variantOptionsResolver.GetOptionsAsync(featureDefinition, featureVariant, cancellationToken).ConfigureAwait(false) + }; + + return returnVariant; + } + + private async ValueTask GetFeatureVariantAsync(string feature, FeatureDefinition featureDefinition, TContext context, bool useContext, bool isFeatureEnabled, CancellationToken cancellationToken) + { + FeatureVariant featureVariant; const string allocatorName = "Targeting"; @@ -308,14 +345,14 @@ private async ValueTask GetVariantAsync(string feature, TCont // IFeatureVariantAllocator if (allocator is IFeatureVariantAllocator featureVariantAllocator) { - variant = await featureVariantAllocator.AllocateVariantAsync(allocationContext, cancellationToken).ConfigureAwait(false); + featureVariant = await featureVariantAllocator.AllocateVariantAsync(allocationContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); } // // IContextualFeatureVariantAllocator else if (useContext && TryGetContextualFeatureVariantAllocator(allocatorName, typeof(TContext), out ContextualFeatureVariantAllocatorEvaluator contextualAllocator)) { - variant = await contextualAllocator.AllocateVariantAsync(allocationContext, context, cancellationToken).ConfigureAwait(false); + featureVariant = await contextualAllocator.AllocateVariantAsync(allocationContext, context, isFeatureEnabled, cancellationToken).ConfigureAwait(false); } // // The allocator doesn't implement a feature variant allocator interface capable of performing the evaluation @@ -328,23 +365,7 @@ private async ValueTask GetVariantAsync(string feature, TCont $"The feature variant allocator '{allocatorName}' specified for the feature '{feature}' is not capable of evaluating the requested feature."); } - // TODO - - if (variant == null) - { - // throw something? - } - - // logic to figure out how to resolve Configuration to one variable betwen value/reference - - Variant returnVariant = new Variant() - { - Name = variant.Name, - ConfigurationValue = variant.ConfigurationValue, - Configuration = await _variantOptionsResolver.GetOptionsAsync(featureDefinition, variant, cancellationToken).ConfigureAwait(false) - }; - - return returnVariant; + return featureVariant; } private IFeatureVariantAllocatorMetadata GetFeatureVariantAllocatorMetadata(string allocatorName) diff --git a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs index a59f7416..ac04bdba 100644 --- a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs @@ -17,8 +17,9 @@ public interface IContextualFeatureVariantAllocator : IFeatureVariantA /// /// A variant allocation context that contains information needed to allocate 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 allocating a variant of a feature. + /// A boolean indicating whether the feature the variant is being allocated to is enabled. /// The cancellation token to cancel the operation. /// The variant that should be allocated for a given feature. - ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, TContext appContext, CancellationToken cancellationToken = default); + ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, TContext appContext, bool isFeatureEnabled, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs index 1ae9cc67..11765efc 100644 --- a/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs @@ -7,16 +7,17 @@ namespace Microsoft.FeatureManagement { /// - /// Provides a method to allocate a variant of a dynamic feature to be used based off of custom conditions. + /// Provides a method to allocate a variant of a feature to be used based off of custom conditions. /// public interface IFeatureVariantAllocator : IFeatureVariantAllocatorMetadata { /// - /// Allocate a variant of a dynamic feature to be used based off of customized criteria. + /// Allocate a variant of a feature to be used based off of customized criteria. /// - /// A variant allocation context that contains information needed to allocate a variant for a dynamic feature. + /// A variant allocation context that contains information needed to allocate a variant for a feature. + /// A boolean indicating whether the feature the variant is being allocated to is enabled. /// The cancellation token to cancel the operation. - /// The variant that should be allocated for a given dynamic feature. - ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, CancellationToken cancellationToken = default); + /// The variant that should be allocated for a given feature. + ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, bool isFeatureEnabled, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs index e9f80136..d0e507b5 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs @@ -34,9 +34,10 @@ public ContextualTargetingFeatureVariantAllocator(IOptions /// Contextual information available for use during the allocation process. /// The targeting context used to determine which variant should be allocated. + /// A boolean indicating whether the feature the variant is being allocated to is enabled. /// The cancellation token to cancel the operation. /// - public ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, ITargetingContext targetingContext, CancellationToken cancellationToken) + public ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, ITargetingContext targetingContext, bool isFeatureEnabled, CancellationToken cancellationToken) { if (variantAllocationContext == null) { @@ -66,8 +67,8 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo FeatureVariant variant = null; - // check if feature is disabled, and if so just return DefaultWhenDisabled variant? IsEnabledAsync or just EvaluateAsync somewhere? don't want it to be a loop anyway - if (featureDefinition.Status == Status.Disabled) // || what? TODO + // check if feature is disabled, and if so just return DefaultWhenDisabled variant? + if (!isFeatureEnabled) { if (!string.IsNullOrEmpty(featureDefinition.Allocation.DefaultWhenDisabled)) { @@ -108,7 +109,7 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo } } - // what to do if seed not specified? random int? + // what to do if seed not specified? random int? TODO foreach (Percentile percentile in featureDefinition.Allocation.Percentile) { if (TargetingEvaluator.IsTargeted(targetingContext, percentile.From, percentile.To, featureDefinition.Allocation.Seed, _options.IgnoreCase, featureDefinition.Name)) @@ -132,8 +133,6 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo } } - //TODO - return new ValueTask(variant); } } diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs index 31802f35..aaa41e80 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs @@ -40,9 +40,10 @@ public TargetingFeatureVariantAllocator(IOptions opt /// Allocates one of the variants configured for a feature based off the provided targeting context. /// /// Contextual information available for use during the allocation process. + /// A boolean indicating whether the feature the variant is being allocated to is enabled. /// The cancellation token to cancel the operation. /// - public async ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, CancellationToken cancellationToken) + public async ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, bool isFeatureEnabled, CancellationToken cancellationToken) { if (variantAllocationContext == null) { @@ -62,7 +63,7 @@ public async ValueTask AllocateVariantAsync(FeatureVariantAlloca return null; } - return await _contextualResolver.AllocateVariantAsync(variantAllocationContext, targetingContext, cancellationToken).ConfigureAwait(false); + return await _contextualResolver.AllocateVariantAsync(variantAllocationContext, targetingContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); } } } \ No newline at end of file From 42a7cbc90cf810a1d30d3a44d04150a077aa107f Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 26 Jul 2023 14:20:09 -0500 Subject: [PATCH 20/80] remove some comments, add null check where needed --- .../Controllers/HomeController.cs | 1 + examples/FeatureFlagDemo/appsettings.json | 3 ++- .../FeatureManager.cs | 16 +++++++++------- ...ContextualTargetingFeatureVariantAllocator.cs | 1 - 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/examples/FeatureFlagDemo/Controllers/HomeController.cs b/examples/FeatureFlagDemo/Controllers/HomeController.cs index 4c377fac..e694900f 100644 --- a/examples/FeatureFlagDemo/Controllers/HomeController.cs +++ b/examples/FeatureFlagDemo/Controllers/HomeController.cs @@ -25,6 +25,7 @@ public HomeController(IVariantFeatureManagerSnapshot featureSnapshot) public async Task Index() { Variant test = await _featureManager.GetVariantAsync(nameof(MyFeatureFlags.Banner), CancellationToken.None); + bool isEnabled = await _featureManager.IsEnabledAsync(nameof(MyFeatureFlags.Banner), CancellationToken.None); return View(); } diff --git a/examples/FeatureFlagDemo/appsettings.json b/examples/FeatureFlagDemo/appsettings.json index 6fb5ad88..1cb76654 100644 --- a/examples/FeatureFlagDemo/appsettings.json +++ b/examples/FeatureFlagDemo/appsettings.json @@ -99,7 +99,8 @@ "Variants": [ { "Name": "Big", - "ConfigurationValue": "1200px" + "ConfigurationValue": "1200px", + "StatusOverride": "Disabled" }, { "Name": "Small", diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index adcb804f..c96a365c 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -96,7 +96,6 @@ public void Dispose() _parametersCache.Dispose(); } - // TODO use ignoreVariant to differentiate between customer use and variant use private async Task IsEnabledAsync(string feature, TContext appContext, bool useAppContext, bool ignoreVariant, CancellationToken cancellationToken) { foreach (ISessionManager sessionManager in _sessionManagers) @@ -229,13 +228,16 @@ await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).Con { FeatureVariant featureVariant = await GetFeatureVariantAsync(feature, featureDefinition, appContext, useAppContext, enabled, cancellationToken); - if (featureVariant.StatusOverride == StatusOverride.Enabled) + if (featureVariant != null) { - enabled = true; - } - else if (featureVariant.StatusOverride == StatusOverride.Disabled) - { - enabled = false; + if (featureVariant.StatusOverride == StatusOverride.Enabled) + { + enabled = true; + } + else if (featureVariant.StatusOverride == StatusOverride.Disabled) + { + enabled = false; + } } } } diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs index d0e507b5..8badd5b8 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs @@ -67,7 +67,6 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo FeatureVariant variant = null; - // check if feature is disabled, and if so just return DefaultWhenDisabled variant? if (!isFeatureEnabled) { if (!string.IsNullOrEmpty(featureDefinition.Allocation.DefaultWhenDisabled)) From 3df6f3264f9207801f3a70e208e155dbd1d6d7de Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 26 Jul 2023 14:37:39 -0500 Subject: [PATCH 21/80] update todo comments --- .../ConfigurationFeatureDefinitionProvider.cs | 2 +- src/Microsoft.FeatureManagement/FeatureManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 6d568518..dfb9e2fc 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -183,7 +183,7 @@ We support } } - // fix logic for when not to set allocation/detect when not set TODO + // fix logic for when not to set allocation, shouldn't always be not null? TODO IConfigurationSection allocationSection = configurationSection.GetSection(nameof(FeatureDefinition.Allocation)); allocation = new Allocation(); allocationSection.Bind(allocation); diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index c96a365c..5d66a9a0 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -312,7 +312,7 @@ private async ValueTask GetVariantAsync(string feature, TCont return null; } - // logic to figure out how to resolve Configuration to one variable betwen value/reference TODO + // logic to resolve Configuration to one variable betwen value/reference TODO Variant returnVariant = new Variant() { Name = featureVariant.Name, From cbc322ebec9a599ee5caf3b6fd9dde67de75faa9 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 26 Jul 2023 14:44:38 -0500 Subject: [PATCH 22/80] fix line eols --- src/Microsoft.FeatureManagement/AllocatorAliasAttribute.cs | 2 +- .../ConfigurationFeatureVariantOptionsResolver.cs | 2 +- .../ContextualFeatureFilterEvaluator.cs | 2 +- .../ContextualFeatureVariantAllocatorEvaluator.cs | 2 +- src/Microsoft.FeatureManagement/FeatureVariant.cs | 2 +- .../FeatureVariantAllocationContext.cs | 2 +- .../IContextualFeatureVariantAllocator.cs | 2 +- src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs | 2 +- .../IFeatureVariantAllocatorMetadata.cs | 2 +- .../IFeatureVariantOptionsResolver.cs | 2 +- .../IVariantFeatureManagerSnapshot.cs | 2 +- src/Microsoft.FeatureManagement/NameHelper.cs | 2 +- .../Targeting/ContextualTargetingFeatureVariantAllocator.cs | 2 +- .../Targeting/ContextualTargetingFilter.cs | 4 ++-- .../Targeting/TargetingEvaluator.cs | 2 +- .../Targeting/TargetingFeatureVariantAllocator.cs | 2 +- 16 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.FeatureManagement/AllocatorAliasAttribute.cs b/src/Microsoft.FeatureManagement/AllocatorAliasAttribute.cs index ae3c5584..6dd0cc14 100644 --- a/src/Microsoft.FeatureManagement/AllocatorAliasAttribute.cs +++ b/src/Microsoft.FeatureManagement/AllocatorAliasAttribute.cs @@ -29,4 +29,4 @@ public AllocatorAliasAttribute(string alias) /// public string Alias { get; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs index ee09ee3b..824c9d04 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs @@ -30,4 +30,4 @@ public ValueTask GetOptionsAsync(FeatureDefinition featureDefini return new ValueTask(_configuration.GetSection($"{variant.ConfigurationReference}")); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/ContextualFeatureFilterEvaluator.cs b/src/Microsoft.FeatureManagement/ContextualFeatureFilterEvaluator.cs index 53df9f9c..b2e68082 100644 --- a/src/Microsoft.FeatureManagement/ContextualFeatureFilterEvaluator.cs +++ b/src/Microsoft.FeatureManagement/ContextualFeatureFilterEvaluator.cs @@ -109,4 +109,4 @@ private static Func public StatusOverride StatusOverride { get; set; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/FeatureVariantAllocationContext.cs b/src/Microsoft.FeatureManagement/FeatureVariantAllocationContext.cs index 1e5fe921..ccbb126b 100644 --- a/src/Microsoft.FeatureManagement/FeatureVariantAllocationContext.cs +++ b/src/Microsoft.FeatureManagement/FeatureVariantAllocationContext.cs @@ -13,4 +13,4 @@ public class FeatureVariantAllocationContext /// public FeatureDefinition FeatureDefinition { get; set; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs index ac04bdba..45e4cbd2 100644 --- a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs @@ -22,4 +22,4 @@ public interface IContextualFeatureVariantAllocator : IFeatureVariantA /// The variant that should be allocated for a given feature. ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, TContext appContext, bool isFeatureEnabled, CancellationToken cancellationToken = default); } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs index 11765efc..6dd14834 100644 --- a/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs @@ -20,4 +20,4 @@ public interface IFeatureVariantAllocator : IFeatureVariantAllocatorMetadata /// The variant that should be allocated for a given feature. ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, bool isFeatureEnabled, CancellationToken cancellationToken = default); } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantAllocatorMetadata.cs b/src/Microsoft.FeatureManagement/IFeatureVariantAllocatorMetadata.cs index 78f956b0..7ed9a2e0 100644 --- a/src/Microsoft.FeatureManagement/IFeatureVariantAllocatorMetadata.cs +++ b/src/Microsoft.FeatureManagement/IFeatureVariantAllocatorMetadata.cs @@ -9,4 +9,4 @@ namespace Microsoft.FeatureManagement public interface IFeatureVariantAllocatorMetadata { } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs b/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs index 0de9c304..fff181ab 100644 --- a/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs +++ b/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs @@ -21,4 +21,4 @@ public interface IFeatureVariantOptionsResolver /// Typed options for a given feature definition and chosen variant. ValueTask GetOptionsAsync(FeatureDefinition featureDefinition, FeatureVariant variant, CancellationToken cancellationToken = default); } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/IVariantFeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/IVariantFeatureManagerSnapshot.cs index a54c3ce7..d2a64dbd 100644 --- a/src/Microsoft.FeatureManagement/IVariantFeatureManagerSnapshot.cs +++ b/src/Microsoft.FeatureManagement/IVariantFeatureManagerSnapshot.cs @@ -9,4 +9,4 @@ namespace Microsoft.FeatureManagement public interface IVariantFeatureManagerSnapshot : IVariantFeatureManager { } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/NameHelper.cs b/src/Microsoft.FeatureManagement/NameHelper.cs index d36b9cfa..c0647bbb 100644 --- a/src/Microsoft.FeatureManagement/NameHelper.cs +++ b/src/Microsoft.FeatureManagement/NameHelper.cs @@ -56,4 +56,4 @@ public static bool IsMatchingReference(string reference, string metadataName, st } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs index 8badd5b8..af27a207 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs @@ -135,4 +135,4 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo return new ValueTask(variant); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs index f9feb873..874f16f9 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs @@ -12,7 +12,7 @@ namespace Microsoft.FeatureManagement.FeatureFilters { /// - /// A feature filter that can be used to activate feature for targeted audiences. + /// A feature filter that can be used to activate features for targeted audiences. /// [FilterAlias(Alias)] public class ContextualTargetingFilter : IContextualFeatureFilter, IFilterParametersBinder @@ -69,4 +69,4 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext context, ITargeti return Task.FromResult(TargetingEvaluator.IsTargeted(targetingContext, settings, _options.IgnoreCase, context.FeatureName)); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs index c267a3bb..98813e0d 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs @@ -340,4 +340,4 @@ private static bool IsTargeted(string contextId, double percentage) return contextPercentage < percentage; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs index aaa41e80..8a42c6fc 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs @@ -66,4 +66,4 @@ public async ValueTask AllocateVariantAsync(FeatureVariantAlloca return await _contextualResolver.AllocateVariantAsync(variantAllocationContext, targetingContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); } } -} \ No newline at end of file +} From 7e801d906915c333399808e77ad68204aaced7f1 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 26 Jul 2023 14:56:05 -0500 Subject: [PATCH 23/80] add unit test, in progress --- .../FeatureManagement.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index a3884eef..a37c08fb 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -963,6 +963,41 @@ public async Task BindsFeatureFlagSettings() Assert.True(called); } + [Fact] + public async Task UsesVariants() + { + FeatureFilterConfiguration testFilterConfiguration = new FeatureFilterConfiguration + { + Name = "Test", + Parameters = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary() + { + { "P1", "V1" }, + }).Build() + }; + + var services = new ServiceCollection(); + + var definitionProvider = new InMemoryFeatureDefinitionProvider( + new FeatureDefinition[] + { + new FeatureDefinition + { + Name = ConditionalFeature, + EnabledFor = new List() + { + testFilterConfiguration + } + } + }); + + services.AddSingleton(definitionProvider) + .AddSingleton(new ConfigurationBuilder().Build()) + .AddFeatureManagement() + .AddFeatureFilter(); + + // TODO + } + private static void DisableEndpointRouting(MvcOptions options) { #if NET6_0 || NET5_0 || NETCOREAPP3_1 From ee3fdb0b67e015d7b5ef399f578f07f8ef3fc7d5 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 27 Jul 2023 15:39:48 -0500 Subject: [PATCH 24/80] TODOs in progress, need to restructure featurevariantassigner design --- .../ConfigurationFeatureDefinitionProvider.cs | 1 - ...figurationFeatureVariantOptionsResolver.cs | 33 ------ ...textualFeatureVariantAllocatorEvaluator.cs | 24 ++-- .../FeatureManager.cs | 110 ++++++++++++------ .../IContextualFeatureVariantAllocator.cs | 3 +- .../IFeatureVariantAllocator.cs | 3 +- .../IFeatureVariantOptionsResolver.cs | 24 ---- .../ServiceCollectionExtensions.cs | 2 - .../StatusOverride.cs | 4 + ...textualTargetingFeatureVariantAllocator.cs | 19 +-- .../Targeting/TargetingEvaluator.cs | 18 ++- .../TargetingFeatureVariantAllocator.cs | 5 +- src/Microsoft.FeatureManagement/Variant.cs | 2 +- 13 files changed, 109 insertions(+), 139 deletions(-) delete mode 100644 src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs delete mode 100644 src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index dfb9e2fc..97af20dc 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -183,7 +183,6 @@ We support } } - // fix logic for when not to set allocation, shouldn't always be not null? TODO IConfigurationSection allocationSection = configurationSection.GetSection(nameof(FeatureDefinition.Allocation)); allocation = new Allocation(); allocationSection.Bind(allocation); diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs deleted file mode 100644 index 824c9d04..00000000 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Microsoft.Extensions.Configuration; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.FeatureManagement -{ - /// - /// A feature variant options resolver that resolves options by reading configuration from the .NET Core system. - /// - sealed class ConfigurationFeatureVariantOptionsResolver : IFeatureVariantOptionsResolver - { - private readonly IConfiguration _configuration; - - public ConfigurationFeatureVariantOptionsResolver(IConfiguration configuration) - { - _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - } - - public ValueTask GetOptionsAsync(FeatureDefinition featureDefinition, FeatureVariant variant, CancellationToken cancellationToken) - { - if (variant == null) - { - throw new ArgumentNullException(nameof(variant)); - } - - return new ValueTask(_configuration.GetSection($"{variant.ConfigurationReference}")); - } - } -} diff --git a/src/Microsoft.FeatureManagement/ContextualFeatureVariantAllocatorEvaluator.cs b/src/Microsoft.FeatureManagement/ContextualFeatureVariantAllocatorEvaluator.cs index 6b6cea74..82f522d2 100644 --- a/src/Microsoft.FeatureManagement/ContextualFeatureVariantAllocatorEvaluator.cs +++ b/src/Microsoft.FeatureManagement/ContextualFeatureVariantAllocatorEvaluator.cs @@ -16,7 +16,7 @@ namespace Microsoft.FeatureManagement sealed class ContextualFeatureVariantAllocatorEvaluator : IContextualFeatureVariantAllocator { private IFeatureVariantAllocatorMetadata _allocator; - private Func> _evaluateFunc; + private Func> _evaluateFunc; public ContextualFeatureVariantAllocatorEvaluator(IFeatureVariantAllocatorMetadata allocator, Type appContextType) { @@ -44,7 +44,7 @@ public ContextualFeatureVariantAllocatorEvaluator(IFeatureVariantAllocatorMetada _allocator = allocator; } - public ValueTask AllocateVariantAsync(FeatureVariantAllocationContext allocationContext, object context, bool isFeatureEnabled, CancellationToken cancellationToken) + public ValueTask AllocateVariantAsync(FeatureVariantAllocationContext allocationContext, object context, CancellationToken cancellationToken) { if (allocationContext == null) { @@ -56,7 +56,7 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo return new ValueTask((FeatureVariant)null); } - return _evaluateFunc(_allocator, allocationContext, context, isFeatureEnabled, cancellationToken); + return _evaluateFunc(_allocator, allocationContext, context, cancellationToken); } public static bool IsContextualVariantAllocator(IFeatureVariantAllocatorMetadata allocator, Type appContextType) @@ -88,7 +88,7 @@ private static Type GetContextualAllocatorInterface(IFeatureVariantAllocatorMeta return targetInterface; } - private static Func> TypeAgnosticEvaluate(Type allocatorType, MethodInfo method) + private static Func> TypeAgnosticEvaluate(Type allocatorType, MethodInfo method) { // // Get the generic version of the evaluation helper method @@ -108,18 +108,18 @@ private static Func>)typeAgnosticDelegate; + return (Func>)typeAgnosticDelegate; } - private static Func> GenericTypeAgnosticEvaluate(MethodInfo method) + private static Func> GenericTypeAgnosticEvaluate(MethodInfo method) { - Func> func = - (Func>) - Delegate.CreateDelegate(typeof(Func>), method); + Func> func = + (Func>) + Delegate.CreateDelegate(typeof(Func>), method); - Func> genericDelegate = - (object target, FeatureVariantAllocationContext param1, object param2, bool param3, CancellationToken param4) => - func((TTarget)target, param1, (TParam2)param2, param3, param4); + Func> genericDelegate = + (object target, FeatureVariantAllocationContext param1, object param2, CancellationToken param3) => + func((TTarget)target, param1, (TParam2)param2, param3); return genericDelegate; } diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 5d66a9a0..2ef2da81 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -27,12 +27,12 @@ class FeatureManager : IFeatureManager, IDisposable, IVariantFeatureManager private readonly IEnumerable _featureFilters; private readonly IEnumerable _sessionManagers; private readonly IEnumerable _variantAllocators; + private readonly IConfiguration _configuration; private readonly ConcurrentDictionary _allocatorMetadataCache; private readonly ConcurrentDictionary _contextualFeatureVariantAllocatorCache; private readonly ILogger _logger; private readonly ConcurrentDictionary _filterMetadataCache; private readonly ConcurrentDictionary _contextualFeatureFilterCache; - private readonly IFeatureVariantOptionsResolver _variantOptionsResolver; private readonly FeatureManagementOptions _options; private readonly IMemoryCache _parametersCache; @@ -48,7 +48,7 @@ public FeatureManager( IEnumerable featureFilters, IEnumerable sessionManagers, IEnumerable variantAllocators, - IFeatureVariantOptionsResolver variantOptionsResolver, + IConfiguration configuration, ILoggerFactory loggerFactory, IOptions options) { @@ -56,7 +56,7 @@ public FeatureManager( _featureFilters = featureFilters ?? throw new ArgumentNullException(nameof(featureFilters)); _sessionManagers = sessionManagers ?? throw new ArgumentNullException(nameof(sessionManagers)); _variantAllocators = variantAllocators ?? throw new ArgumentNullException(nameof(variantAllocators)); - _variantOptionsResolver = variantOptionsResolver ?? throw new ArgumentNullException(nameof(variantOptionsResolver)); + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _allocatorMetadataCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); _contextualFeatureVariantAllocatorCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); _logger = loggerFactory.CreateLogger(); @@ -150,8 +150,9 @@ private async Task IsEnabledAsync(string feature, TContext appCo filterIndex++; // - // Handle AlwaysOn filters - if (string.Equals(featureFilterConfiguration.Name, "AlwaysOn", StringComparison.OrdinalIgnoreCase)) + // Handle AlwaysOn and On filters + if (string.Equals(featureFilterConfiguration.Name, "AlwaysOn", StringComparison.OrdinalIgnoreCase) || + string.Equals(featureFilterConfiguration.Name, "On", StringComparison.OrdinalIgnoreCase)) { if (featureDefinition.RequirementType == RequirementType.Any) { @@ -162,13 +163,6 @@ private async Task IsEnabledAsync(string feature, TContext appCo continue; } - //// - //// Handle On filters for variants - //if (string.Equals(featureFilterConfiguration.Name, "On", StringComparison.OrdinalIgnoreCase)) - //{ - // // TODO - //} - IFeatureFilterMetadata filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name); if (filter == null) @@ -223,23 +217,6 @@ await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).Con } } } - - if (!ignoreVariant && (featureDefinition.Variants?.Any() ?? false)) - { - FeatureVariant featureVariant = await GetFeatureVariantAsync(feature, featureDefinition, appContext, useAppContext, enabled, cancellationToken); - - if (featureVariant != null) - { - if (featureVariant.StatusOverride == StatusOverride.Enabled) - { - enabled = true; - } - else if (featureVariant.StatusOverride == StatusOverride.Disabled) - { - enabled = false; - } - } - } } else { @@ -255,6 +232,23 @@ await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).Con _logger.LogWarning(errorMessage); } + if (!ignoreVariant && (featureDefinition.Variants?.Any() ?? false)) + { + FeatureVariant featureVariant = await GetFeatureVariantAsync(feature, featureDefinition, appContext, useAppContext, enabled, cancellationToken); + + if (featureVariant != null) + { + if (featureVariant.StatusOverride == StatusOverride.Enabled) + { + enabled = true; + } + else if (featureVariant.StatusOverride == StatusOverride.Disabled) + { + enabled = false; + } + } + } + foreach (ISessionManager sessionManager in _sessionManagers) { await sessionManager.SetAsync(feature, enabled, cancellationToken).ConfigureAwait(false); @@ -303,28 +297,49 @@ private async ValueTask GetVariantAsync(string feature, TCont $"No variants are registered for the feature {feature}"); } + FeatureVariant featureVariant; + bool isFeatureEnabled = await IsEnabledAsync(feature, context, true, cancellationToken).ConfigureAwait(false); - FeatureVariant featureVariant = await GetFeatureVariantAsync(feature, featureDefinition, context, useContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); + featureVariant = await GetFeatureVariantAsync(feature, featureDefinition, context, useContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); if (featureVariant == null) { return null; } - // logic to resolve Configuration to one variable betwen value/reference TODO Variant returnVariant = new Variant() { Name = featureVariant.Name, - ConfigurationValue = featureVariant.ConfigurationValue, - Configuration = await _variantOptionsResolver.GetOptionsAsync(featureDefinition, featureVariant, cancellationToken).ConfigureAwait(false) + Configuration = GetVariantConfiguration(featureVariant) }; return returnVariant; } + private IConfigurationSection GetVariantConfiguration(FeatureVariant featureVariant) + { + if (featureVariant == null) + { + throw new ArgumentNullException(nameof(featureVariant)); + } + + // TODO how to return IConfigurationSection here? get to the one referenced here somehow? + //if (featureVariant.ConfigurationValue != null) + //{ + // return new ConfigurationSection(_configuration., ); + //} + + return _configuration.GetSection($"{featureVariant.ConfigurationReference}"); + } + private async ValueTask GetFeatureVariantAsync(string feature, FeatureDefinition featureDefinition, TContext context, bool useContext, bool isFeatureEnabled, CancellationToken cancellationToken) { + if (!isFeatureEnabled) + { + return ResolveDefaultFeatureVariant(featureDefinition, isFeatureEnabled); + } + FeatureVariant featureVariant; const string allocatorName = "Targeting"; @@ -343,18 +358,20 @@ private async ValueTask GetFeatureVariantAsync(string FeatureDefinition = featureDefinition }; + // TODO rename back to assigner, try to move logic from other interfaces to featuremanager where possible and build up from this new perspective + // // IFeatureVariantAllocator if (allocator is IFeatureVariantAllocator featureVariantAllocator) { - featureVariant = await featureVariantAllocator.AllocateVariantAsync(allocationContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); + featureVariant = await featureVariantAllocator.AllocateVariantAsync(allocationContext, cancellationToken).ConfigureAwait(false); } // // IContextualFeatureVariantAllocator else if (useContext && TryGetContextualFeatureVariantAllocator(allocatorName, typeof(TContext), out ContextualFeatureVariantAllocatorEvaluator contextualAllocator)) { - featureVariant = await contextualAllocator.AllocateVariantAsync(allocationContext, context, isFeatureEnabled, cancellationToken).ConfigureAwait(false); + featureVariant = await contextualAllocator.AllocateVariantAsync(allocationContext, context, cancellationToken).ConfigureAwait(false); } // // The allocator doesn't implement a feature variant allocator interface capable of performing the evaluation @@ -367,9 +384,32 @@ private async ValueTask GetFeatureVariantAsync(string $"The feature variant allocator '{allocatorName}' specified for the feature '{feature}' is not capable of evaluating the requested feature."); } + if (featureVariant == null) + { + return ResolveDefaultFeatureVariant(featureDefinition, isFeatureEnabled); + } + return featureVariant; } + private FeatureVariant ResolveDefaultFeatureVariant(FeatureDefinition featureDefinition, bool isFeatureEnabled) + { + string defaultVariantPath = isFeatureEnabled ? featureDefinition.Allocation.DefaultWhenEnabled : + defaultVariantPath = featureDefinition.Allocation.DefaultWhenDisabled; + + if (!string.IsNullOrEmpty(defaultVariantPath)) + { + FeatureVariant defaultVariant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(defaultVariantPath)); + + if (!string.IsNullOrEmpty(defaultVariant.Name)) + { + return defaultVariant; + } + } + + return null; + } + private IFeatureVariantAllocatorMetadata GetFeatureVariantAllocatorMetadata(string allocatorName) { const string allocatorSuffix = "allocator"; diff --git a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs index 45e4cbd2..bdceccac 100644 --- a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs @@ -17,9 +17,8 @@ public interface IContextualFeatureVariantAllocator : IFeatureVariantA /// /// A variant allocation context that contains information needed to allocate 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 allocating a variant of a feature. - /// A boolean indicating whether the feature the variant is being allocated to is enabled. /// The cancellation token to cancel the operation. /// The variant that should be allocated for a given feature. - ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, TContext appContext, bool isFeatureEnabled, CancellationToken cancellationToken = default); + ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, TContext appContext, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs index 6dd14834..312f4078 100644 --- a/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs @@ -15,9 +15,8 @@ public interface IFeatureVariantAllocator : IFeatureVariantAllocatorMetadata /// Allocate a variant of a feature to be used based off of customized criteria. /// /// A variant allocation context that contains information needed to allocate a variant for a feature. - /// A boolean indicating whether the feature the variant is being allocated to is enabled. /// The cancellation token to cancel the operation. /// The variant that should be allocated for a given feature. - ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, bool isFeatureEnabled, CancellationToken cancellationToken = default); + ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs b/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs deleted file mode 100644 index fff181ab..00000000 --- a/src/Microsoft.FeatureManagement/IFeatureVariantOptionsResolver.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Microsoft.Extensions.Configuration; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.FeatureManagement -{ - /// - /// Performs the resolution and binding necessary in the feature variant resolution process. - /// - public interface IFeatureVariantOptionsResolver - { - /// - /// Retrieves typed options for a given feature definition and chosen variant. - /// - /// The definition of the feature that the resolution is being performed for. - /// The chosen variant of the 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 = default); - } -} diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 61ba3bfc..8f368728 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -32,8 +32,6 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec services.TryAddSingleton(sp => sp.GetRequiredService()); - services.TryAddSingleton(); - services.AddSingleton(); services.AddSingleton(); diff --git a/src/Microsoft.FeatureManagement/StatusOverride.cs b/src/Microsoft.FeatureManagement/StatusOverride.cs index fc9f78cf..9a219e7e 100644 --- a/src/Microsoft.FeatureManagement/StatusOverride.cs +++ b/src/Microsoft.FeatureManagement/StatusOverride.cs @@ -8,6 +8,10 @@ namespace Microsoft.FeatureManagement /// public enum StatusOverride { + /// + /// + /// + None, /// /// /// diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs index af27a207..7cbe38fc 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs @@ -34,10 +34,9 @@ public ContextualTargetingFeatureVariantAllocator(IOptions /// Contextual information available for use during the allocation process. /// The targeting context used to determine which variant should be allocated. - /// A boolean indicating whether the feature the variant is being allocated to is enabled. /// The cancellation token to cancel the operation. /// - public ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, ITargetingContext targetingContext, bool isFeatureEnabled, CancellationToken cancellationToken) + public ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, ITargetingContext targetingContext, CancellationToken cancellationToken) { if (variantAllocationContext == null) { @@ -67,21 +66,6 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo FeatureVariant variant = null; - if (!isFeatureEnabled) - { - if (!string.IsNullOrEmpty(featureDefinition.Allocation.DefaultWhenDisabled)) - { - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(featureDefinition.Allocation.DefaultWhenDisabled)); - - if (!string.IsNullOrEmpty(variant.Name)) - { - return new ValueTask(variant); - } - } - - return new ValueTask((FeatureVariant)null); - } - foreach (User user in featureDefinition.Allocation.User) { if (TargetingEvaluator.IsTargeted(targetingContext, user.Users, _options.IgnoreCase)) @@ -108,7 +92,6 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo } } - // what to do if seed not specified? random int? TODO foreach (Percentile percentile in featureDefinition.Allocation.Percentile) { if (TargetingEvaluator.IsTargeted(targetingContext, percentile.From, percentile.To, featureDefinition.Allocation.Seed, _options.IgnoreCase, featureDefinition.Name)) diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs index 98813e0d..89d6b636 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs @@ -5,10 +5,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Numerics; using System.Security.Cryptography; using System.Text; -using System.Text.RegularExpressions; namespace Microsoft.FeatureManagement.Targeting { @@ -292,17 +290,25 @@ public static bool IsTargeted(ITargetingContext targetingContext, double from, d { byte[] hash; - byte[] seedBytes = BitConverter.GetBytes(seed); - string userId = ignoreCase ? targetingContext.UserId.ToLower() : targetingContext.UserId; - string contextId = $"{userId}\n{hint}"; + string contextId; + + // TODO designate int value for unset seeds (0 here) or add featurevariant property "seedSet"? + if (seed != 0) + { + contextId = $"{userId}\n{seed}"; + } + else + { + contextId = $"{userId}\n{hint}"; + } using (HashAlgorithm hashAlgorithm = SHA256.Create()) { - hash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(contextId).Concat(seedBytes).ToArray()); + hash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(contextId).ToArray()); } // diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs index 8a42c6fc..163226b7 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs @@ -40,10 +40,9 @@ public TargetingFeatureVariantAllocator(IOptions opt /// Allocates one of the variants configured for a feature based off the provided targeting context. /// /// Contextual information available for use during the allocation process. - /// A boolean indicating whether the feature the variant is being allocated to is enabled. /// The cancellation token to cancel the operation. /// - public async ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, bool isFeatureEnabled, CancellationToken cancellationToken) + public async ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, CancellationToken cancellationToken) { if (variantAllocationContext == null) { @@ -63,7 +62,7 @@ public async ValueTask AllocateVariantAsync(FeatureVariantAlloca return null; } - return await _contextualResolver.AllocateVariantAsync(variantAllocationContext, targetingContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); + return await _contextualResolver.AllocateVariantAsync(variantAllocationContext, targetingContext, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Microsoft.FeatureManagement/Variant.cs b/src/Microsoft.FeatureManagement/Variant.cs index 356925ba..11a5bc5a 100644 --- a/src/Microsoft.FeatureManagement/Variant.cs +++ b/src/Microsoft.FeatureManagement/Variant.cs @@ -18,7 +18,7 @@ public class Variant /// /// /// - public IConfiguration Configuration { get; set; } + public IConfigurationSection Configuration { get; set; } /// /// From 76ad51f6b8dd934854ae446c2c88d035a843e0f2 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 28 Jul 2023 09:49:41 -0500 Subject: [PATCH 25/80] fix seed logic --- src/Microsoft.FeatureManagement/Allocation.cs | 2 +- .../Targeting/TargetingEvaluator.cs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.FeatureManagement/Allocation.cs b/src/Microsoft.FeatureManagement/Allocation.cs index 7e23823c..e7350c04 100644 --- a/src/Microsoft.FeatureManagement/Allocation.cs +++ b/src/Microsoft.FeatureManagement/Allocation.cs @@ -38,6 +38,6 @@ public class Allocation /// /// Maps users to the same percentile across multiple feature flags. /// - public int Seed { get; set; } + public string Seed { get; set; } } } diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs index 89d6b636..dc248569 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs @@ -286,7 +286,7 @@ public static bool TryValidateSettings(TargetingFilterSettings targetingSettings /// /// Determines if a given context id should be targeted based off the percentage range given /// - public static bool IsTargeted(ITargetingContext targetingContext, double from, double to, int seed, bool ignoreCase, string hint) + public static bool IsTargeted(ITargetingContext targetingContext, double from, double to, string seed, bool ignoreCase, string hint) { byte[] hash; @@ -296,8 +296,7 @@ public static bool IsTargeted(ITargetingContext targetingContext, double from, d string contextId; - // TODO designate int value for unset seeds (0 here) or add featurevariant property "seedSet"? - if (seed != 0) + if (!string.IsNullOrEmpty(seed)) { contextId = $"{userId}\n{seed}"; } From b7a97a7f05ded482a85c4f587290f307352d0e85 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 31 Jul 2023 11:53:28 -0500 Subject: [PATCH 26/80] update comments, status logic --- src/Microsoft.FeatureManagement/FeatureDefinition.cs | 9 +++++---- src/Microsoft.FeatureManagement/FeatureManager.cs | 4 ++-- .../Targeting/TargetingEvaluator.cs | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureDefinition.cs b/src/Microsoft.FeatureManagement/FeatureDefinition.cs index 049fa906..762bca13 100644 --- a/src/Microsoft.FeatureManagement/FeatureDefinition.cs +++ b/src/Microsoft.FeatureManagement/FeatureDefinition.cs @@ -22,23 +22,24 @@ public class FeatureDefinition public IEnumerable EnabledFor { get; set; } = new List(); /// - /// Determines whether any or all registered feature filters must be enabled for the feature to be considered enabled + /// Determines whether any or all registered feature filters must be enabled for the feature to be considered enabled. /// The default value is . /// public RequirementType RequirementType { get; set; } = RequirementType.Any; /// - /// kill switch + /// When set to , this feature will always be considered disabled regardless of the rest of the feature definition. + /// The default value is . /// public Status Status { get; set; } = Status.Conditional; /// - /// + /// Describes how variants should be allocated. /// public Allocation Allocation { get; set; } /// - /// + /// A list of feature variants that specify a configuration to return when assigned. /// public IEnumerable Variants { get; set; } = Enumerable.Empty(); } diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 2ef2da81..8e48420f 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -232,7 +232,7 @@ await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).Con _logger.LogWarning(errorMessage); } - if (!ignoreVariant && (featureDefinition.Variants?.Any() ?? false)) + if (!ignoreVariant && (featureDefinition.Variants?.Any() ?? false) && featureDefinition.Status != Status.Disabled) { FeatureVariant featureVariant = await GetFeatureVariantAsync(feature, featureDefinition, appContext, useAppContext, enabled, cancellationToken); @@ -274,7 +274,7 @@ public ValueTask GetVariantAsync(string feature, TContext con throw new ArgumentNullException(nameof(feature)); } - return GetVariantAsync(feature, context, true, cancellationToken); + return GetVariantAsync(feature, context, true, cancellationToken); } private async ValueTask GetVariantAsync(string feature, TContext context, bool useContext, CancellationToken cancellationToken) diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs index dc248569..c34ae83a 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs @@ -284,7 +284,7 @@ public static bool TryValidateSettings(TargetingFilterSettings targetingSettings } /// - /// Determines if a given context id should be targeted based off the percentage range given + /// Determines if a given context id should be targeted based off the provided percentage range /// public static bool IsTargeted(ITargetingContext targetingContext, double from, double to, string seed, bool ignoreCase, string hint) { From 370228ec08d988ebf84d33c65f16228543d644dc Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 31 Jul 2023 14:37:27 -0500 Subject: [PATCH 27/80] remove unnecessary files for custom assigners, fix featuremanager methods and interfaces to match --- examples/FeatureFlagDemo/Startup.cs | 2 - .../AllocatorAliasAttribute.cs | 32 ---- ...textualFeatureVariantAllocatorEvaluator.cs | 127 -------------- .../FeatureManagementBuilder.cs | 23 --- .../FeatureManager.cs | 160 +++++------------- .../FeatureManagerSnapshot.cs | 5 +- .../FeatureVariantAllocationContext.cs | 16 -- .../IContextualFeatureVariantAllocator.cs | 10 +- .../IFeatureManagementBuilder.cs | 9 - .../IFeatureVariantAllocator.cs | 22 --- .../IFeatureVariantAllocatorMetadata.cs | 12 -- .../IVariantFeatureManager.cs | 3 +- ...textualTargetingFeatureVariantAllocator.cs | 22 +-- .../TargetingFeatureVariantAllocator.cs | 68 -------- 14 files changed, 60 insertions(+), 451 deletions(-) delete mode 100644 src/Microsoft.FeatureManagement/AllocatorAliasAttribute.cs delete mode 100644 src/Microsoft.FeatureManagement/ContextualFeatureVariantAllocatorEvaluator.cs delete mode 100644 src/Microsoft.FeatureManagement/FeatureVariantAllocationContext.cs delete mode 100644 src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs delete mode 100644 src/Microsoft.FeatureManagement/IFeatureVariantAllocatorMetadata.cs delete mode 100644 src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs diff --git a/examples/FeatureFlagDemo/Startup.cs b/examples/FeatureFlagDemo/Startup.cs index 68c726b5..65c0777c 100644 --- a/examples/FeatureFlagDemo/Startup.cs +++ b/examples/FeatureFlagDemo/Startup.cs @@ -13,7 +13,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.FeatureManagement; -using Microsoft.FeatureManagement.Allocators; using Microsoft.FeatureManagement.FeatureFilters; namespace FeatureFlagDemo @@ -60,7 +59,6 @@ public void ConfigureServices(IServiceCollection services) .AddFeatureFilter() .AddFeatureFilter() .AddFeatureFilter() - .AddFeatureVariantAllocator() .UseDisabledFeaturesHandler(new FeatureNotEnabledDisabledHandler()); services.AddMvc(o => diff --git a/src/Microsoft.FeatureManagement/AllocatorAliasAttribute.cs b/src/Microsoft.FeatureManagement/AllocatorAliasAttribute.cs deleted file mode 100644 index 6dd0cc14..00000000 --- a/src/Microsoft.FeatureManagement/AllocatorAliasAttribute.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using System; - -namespace Microsoft.FeatureManagement -{ - /// - /// Allows the name of an to be customized to relate to the name specified in configuration. - /// - public class AllocatorAliasAttribute : Attribute - { - /// - /// Creates an allocator alias using the provided alias. - /// - /// The alias of the feature variant allocator. - public AllocatorAliasAttribute(string alias) - { - if (string.IsNullOrEmpty(alias)) - { - throw new ArgumentNullException(nameof(alias)); - } - - Alias = alias; - } - - /// - /// The name that will be used to match feature feature variant allocator specified in the configuration. - /// - public string Alias { get; } - } -} diff --git a/src/Microsoft.FeatureManagement/ContextualFeatureVariantAllocatorEvaluator.cs b/src/Microsoft.FeatureManagement/ContextualFeatureVariantAllocatorEvaluator.cs deleted file mode 100644 index 82f522d2..00000000 --- a/src/Microsoft.FeatureManagement/ContextualFeatureVariantAllocatorEvaluator.cs +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.FeatureManagement -{ - /// - /// Provides a performance efficient method of evaluating without knowing what the generic type parameter is. - /// - sealed class ContextualFeatureVariantAllocatorEvaluator : IContextualFeatureVariantAllocator - { - private IFeatureVariantAllocatorMetadata _allocator; - private Func> _evaluateFunc; - - public ContextualFeatureVariantAllocatorEvaluator(IFeatureVariantAllocatorMetadata allocator, Type appContextType) - { - if (allocator == null) - { - throw new ArgumentNullException(nameof(allocator)); - } - - if (appContextType == null) - { - throw new ArgumentNullException(nameof(appContextType)); - } - - Type targetInterface = GetContextualAllocatorInterface(allocator, appContextType); - - // - // Extract IContextualFeatureVariantAllocator.AllocateVariantAsync method. - if (targetInterface != null) - { - MethodInfo evaluateMethod = targetInterface.GetMethod(nameof(IContextualFeatureVariantAllocator.AllocateVariantAsync), BindingFlags.Public | BindingFlags.Instance); - - _evaluateFunc = TypeAgnosticEvaluate(allocator.GetType(), evaluateMethod); - } - - _allocator = allocator; - } - - public ValueTask AllocateVariantAsync(FeatureVariantAllocationContext allocationContext, object context, CancellationToken cancellationToken) - { - if (allocationContext == null) - { - throw new ArgumentNullException(nameof(allocationContext)); - } - - if (_evaluateFunc == null) - { - return new ValueTask((FeatureVariant)null); - } - - return _evaluateFunc(_allocator, allocationContext, context, cancellationToken); - } - - public static bool IsContextualVariantAllocator(IFeatureVariantAllocatorMetadata allocator, Type appContextType) - { - if (allocator == null) - { - throw new ArgumentNullException(nameof(allocator)); - } - - if (appContextType == null) - { - throw new ArgumentNullException(nameof(appContextType)); - } - - return GetContextualAllocatorInterface(allocator, appContextType) != null; - } - - private static Type GetContextualAllocatorInterface(IFeatureVariantAllocatorMetadata allocator, Type appContextType) - { - IEnumerable contextualAllocatorInterfaces = allocator.GetType().GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition().IsAssignableFrom(typeof(IContextualFeatureVariantAllocator<>))); - - Type targetInterface = null; - - if (contextualAllocatorInterfaces != null) - { - targetInterface = contextualAllocatorInterfaces.FirstOrDefault(i => i.GetGenericArguments()[0].IsAssignableFrom(appContextType)); - } - - return targetInterface; - } - - private static Func> TypeAgnosticEvaluate(Type allocatorType, MethodInfo method) - { - // - // Get the generic version of the evaluation helper method - MethodInfo genericHelper = typeof(ContextualFeatureVariantAllocatorEvaluator).GetMethod(nameof(GenericTypeAgnosticEvaluate), - BindingFlags.Static | BindingFlags.NonPublic); - - // - // Create a type specific version of the evaluation helper method - MethodInfo constructedHelper = genericHelper.MakeGenericMethod - (allocatorType, - method.GetParameters()[0].ParameterType, - method.GetParameters()[1].ParameterType, - method.GetParameters()[2].ParameterType, - method.ReturnType); - - // - // Invoke the method to get the func - object typeAgnosticDelegate = constructedHelper.Invoke(null, new object[] { method }); - - return (Func>)typeAgnosticDelegate; - } - - private static Func> GenericTypeAgnosticEvaluate(MethodInfo method) - { - Func> func = - (Func>) - Delegate.CreateDelegate(typeof(Func>), method); - - Func> genericDelegate = - (object target, FeatureVariantAllocationContext param1, object param2, CancellationToken param3) => - func((TTarget)target, param1, (TParam2)param2, param3); - - return genericDelegate; - } - } -} diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs index a17e1c64..a433b2bc 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs @@ -43,29 +43,6 @@ public IFeatureManagementBuilder AddFeatureFilter() where T : IFeatureFilterM return this; } - public IFeatureManagementBuilder AddFeatureVariantAllocator() where T : IFeatureVariantAllocatorMetadata - { - Type serviceType = typeof(IFeatureVariantAllocatorMetadata); - - Type implementationType = typeof(T); - - IEnumerable featureVariantAssignerImplementations = implementationType.GetInterfaces() - .Where(i => i == typeof(IFeatureVariantAllocator) || - (i.IsGenericType && i.GetGenericTypeDefinition().IsAssignableFrom(typeof(IContextualFeatureVariantAllocator<>)))); - - if (featureVariantAssignerImplementations.Count() > 1) - { - throw new ArgumentException($"A single feature variant allocator cannot implement more than one feature variant allocator interface.", nameof(T)); - } - - if (!Services.Any(descriptor => descriptor.ServiceType == serviceType && descriptor.ImplementationType == implementationType)) - { - Services.AddSingleton(typeof(IFeatureVariantAllocatorMetadata), typeof(T)); - } - - return this; - } - public IFeatureManagementBuilder AddSessionManager() where T : ISessionManager { Services.AddSingleton(typeof(ISessionManager), typeof(T)); diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 8e48420f..a14f4417 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -5,6 +5,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.FeatureManagement.Allocators; +using Microsoft.FeatureManagement.FeatureFilters; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -26,10 +28,9 @@ class FeatureManager : IFeatureManager, IDisposable, IVariantFeatureManager private readonly IFeatureDefinitionProvider _featureDefinitionProvider; private readonly IEnumerable _featureFilters; private readonly IEnumerable _sessionManagers; - private readonly IEnumerable _variantAllocators; + private readonly IContextualFeatureVariantAllocator _contextualFeatureVariantAllocator; + private readonly ITargetingContextAccessor _contextAccessor; private readonly IConfiguration _configuration; - private readonly ConcurrentDictionary _allocatorMetadataCache; - private readonly ConcurrentDictionary _contextualFeatureVariantAllocatorCache; private readonly ILogger _logger; private readonly ConcurrentDictionary _filterMetadataCache; private readonly ConcurrentDictionary _contextualFeatureFilterCache; @@ -47,19 +48,19 @@ public FeatureManager( IFeatureDefinitionProvider featureDefinitionProvider, IEnumerable featureFilters, IEnumerable sessionManagers, - IEnumerable variantAllocators, + ITargetingContextAccessor contextAccessor, IConfiguration configuration, ILoggerFactory loggerFactory, - IOptions options) + IOptions options, + IOptions assignerOptions) { _featureDefinitionProvider = featureDefinitionProvider; _featureFilters = featureFilters ?? throw new ArgumentNullException(nameof(featureFilters)); _sessionManagers = sessionManagers ?? throw new ArgumentNullException(nameof(sessionManagers)); - _variantAllocators = variantAllocators ?? throw new ArgumentNullException(nameof(variantAllocators)); _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - _allocatorMetadataCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - _contextualFeatureVariantAllocatorCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + _contextAccessor = contextAccessor ?? throw new ArgumentNullException(nameof(contextAccessor)); _logger = loggerFactory.CreateLogger(); + _contextualFeatureVariantAllocator = new ContextualTargetingFeatureVariantAllocator(assignerOptions); _filterMetadataCache = new ConcurrentDictionary(); _contextualFeatureFilterCache = new ConcurrentDictionary(); _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); @@ -234,17 +235,20 @@ await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).Con if (!ignoreVariant && (featureDefinition.Variants?.Any() ?? false) && featureDefinition.Status != Status.Disabled) { - FeatureVariant featureVariant = await GetFeatureVariantAsync(feature, featureDefinition, appContext, useAppContext, enabled, cancellationToken); - - if (featureVariant != null) + if (appContext is TargetingContext) { - if (featureVariant.StatusOverride == StatusOverride.Enabled) - { - enabled = true; - } - else if (featureVariant.StatusOverride == StatusOverride.Disabled) + FeatureVariant featureVariant = await GetFeatureVariantAsync(featureDefinition, appContext as TargetingContext, useAppContext, enabled, cancellationToken); + + if (featureVariant != null) { - enabled = false; + if (featureVariant.StatusOverride == StatusOverride.Enabled) + { + enabled = true; + } + else if (featureVariant.StatusOverride == StatusOverride.Disabled) + { + enabled = false; + } } } } @@ -264,10 +268,10 @@ public ValueTask GetVariantAsync(string feature, CancellationToken canc throw new ArgumentNullException(nameof(feature)); } - return GetVariantAsync(feature, null, false, cancellationToken); + return GetVariantAsync(feature, null, false, cancellationToken); } - public ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken) + public ValueTask GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(feature)) { @@ -277,7 +281,7 @@ public ValueTask GetVariantAsync(string feature, TContext con return GetVariantAsync(feature, context, true, cancellationToken); } - private async ValueTask GetVariantAsync(string feature, TContext context, bool useContext, CancellationToken cancellationToken) + private async ValueTask GetVariantAsync(string feature, TargetingContext context, bool useContext, CancellationToken cancellationToken) { FeatureDefinition featureDefinition = await _featureDefinitionProvider .GetFeatureDefinitionAsync(feature, cancellationToken) @@ -301,7 +305,7 @@ private async ValueTask GetVariantAsync(string feature, TCont bool isFeatureEnabled = await IsEnabledAsync(feature, context, true, cancellationToken).ConfigureAwait(false); - featureVariant = await GetFeatureVariantAsync(feature, featureDefinition, context, useContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); + featureVariant = await GetFeatureVariantAsync(featureDefinition, context, useContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); if (featureVariant == null) { @@ -333,7 +337,7 @@ private IConfigurationSection GetVariantConfiguration(FeatureVariant featureVari return _configuration.GetSection($"{featureVariant.ConfigurationReference}"); } - private async ValueTask GetFeatureVariantAsync(string feature, FeatureDefinition featureDefinition, TContext context, bool useContext, bool isFeatureEnabled, CancellationToken cancellationToken) + private async ValueTask GetFeatureVariantAsync(FeatureDefinition featureDefinition, TargetingContext context, bool useContext, bool isFeatureEnabled, CancellationToken cancellationToken) { if (!isFeatureEnabled) { @@ -342,48 +346,26 @@ private async ValueTask GetFeatureVariantAsync(string FeatureVariant featureVariant; - const string allocatorName = "Targeting"; - - IFeatureVariantAllocatorMetadata allocator = GetFeatureVariantAllocatorMetadata(allocatorName); + // TODO rename back to assigner - if (allocator == null) + if (!useContext) { - throw new FeatureManagementException( - FeatureManagementError.MissingFeatureVariantAllocator, - $"The feature variant allocator for feature '{feature}' was not found."); - } - - var allocationContext = new FeatureVariantAllocationContext() - { - FeatureDefinition = featureDefinition - }; + // + // Acquire targeting context via accessor + context = await _contextAccessor.GetContextAsync(cancellationToken).ConfigureAwait(false); - // TODO rename back to assigner, try to move logic from other interfaces to featuremanager where possible and build up from this new perspective + // + // Ensure targeting can be performed + if (context == null) + { + _logger.LogWarning("No targeting context available for targeting evaluation."); - // - // IFeatureVariantAllocator - if (allocator is IFeatureVariantAllocator featureVariantAllocator) - { - featureVariant = await featureVariantAllocator.AllocateVariantAsync(allocationContext, cancellationToken).ConfigureAwait(false); - } - // - // IContextualFeatureVariantAllocator - else if (useContext && - TryGetContextualFeatureVariantAllocator(allocatorName, typeof(TContext), out ContextualFeatureVariantAllocatorEvaluator contextualAllocator)) - { - featureVariant = await contextualAllocator.AllocateVariantAsync(allocationContext, context, cancellationToken).ConfigureAwait(false); - } - // - // The allocator doesn't implement a feature variant allocator interface capable of performing the evaluation - else - { - throw new FeatureManagementException( - FeatureManagementError.InvalidFeatureVariantAllocator, - useContext ? - $"The feature variant allocator '{allocatorName}' specified for the feature '{feature}' is not capable of evaluating the requested feature with the provided context." : - $"The feature variant allocator '{allocatorName}' specified for the feature '{feature}' is not capable of evaluating the requested feature."); + return null; + } } + featureVariant = await _contextualFeatureVariantAllocator.AllocateVariantAsync(featureDefinition, context, cancellationToken).ConfigureAwait(false); + if (featureVariant == null) { return ResolveDefaultFeatureVariant(featureDefinition, isFeatureEnabled); @@ -394,8 +376,7 @@ private async ValueTask GetFeatureVariantAsync(string private FeatureVariant ResolveDefaultFeatureVariant(FeatureDefinition featureDefinition, bool isFeatureEnabled) { - string defaultVariantPath = isFeatureEnabled ? featureDefinition.Allocation.DefaultWhenEnabled : - defaultVariantPath = featureDefinition.Allocation.DefaultWhenDisabled; + string defaultVariantPath = isFeatureEnabled ? featureDefinition.Allocation.DefaultWhenEnabled : featureDefinition.Allocation.DefaultWhenDisabled; if (!string.IsNullOrEmpty(defaultVariantPath)) { @@ -410,65 +391,6 @@ private FeatureVariant ResolveDefaultFeatureVariant(FeatureDefinition featureDef return null; } - private IFeatureVariantAllocatorMetadata GetFeatureVariantAllocatorMetadata(string allocatorName) - { - const string allocatorSuffix = "allocator"; - - IFeatureVariantAllocatorMetadata allocator = _allocatorMetadataCache.GetOrAdd( - allocatorName, - (_) => { - - IEnumerable matchingAllocators = _variantAllocators.Where(a => - { - Type allocatorType = a.GetType(); - - string name = ((AllocatorAliasAttribute)Attribute.GetCustomAttribute(allocatorType, typeof(AllocatorAliasAttribute)))?.Alias; - - if (name == null) - { - name = allocatorType.Name; - } - - return NameHelper.IsMatchingReference( - reference: allocatorName, - metadataName: name, - suffix: allocatorSuffix); - }); - - if (matchingAllocators.Count() > 1) - { - throw new FeatureManagementException(FeatureManagementError.AmbiguousFeatureVariantAllocator, $"Multiple feature variant allocators match the configured allocator named '{allocatorName}'."); - } - - return matchingAllocators.FirstOrDefault(); - } - ); - - return allocator; - } - - private bool TryGetContextualFeatureVariantAllocator(string allocatorName, Type appContextType, out ContextualFeatureVariantAllocatorEvaluator contextualAllocator) - { - if (appContextType == null) - { - throw new ArgumentNullException(nameof(appContextType)); - } - - contextualAllocator = _contextualFeatureVariantAllocatorCache.GetOrAdd( - $"{allocatorName}{Environment.NewLine}{appContextType.FullName}", - (_) => { - - IFeatureVariantAllocatorMetadata metadata = GetFeatureVariantAllocatorMetadata(allocatorName); - - return ContextualFeatureVariantAllocatorEvaluator.IsContextualVariantAllocator(metadata, appContextType) ? - new ContextualFeatureVariantAllocatorEvaluator(metadata, appContextType) : - null; - } - ); - - return contextualAllocator != null; - } - private void BindSettings(IFeatureFilterMetadata filter, FeatureFilterEvaluationContext context, int filterIndex) { IFilterParametersBinder binder = filter as IFilterParametersBinder; diff --git a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs index 3ff2181e..6934a83b 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using Microsoft.FeatureManagement.FeatureFilters; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -77,7 +78,7 @@ public async ValueTask GetVariantAsync(string feature, CancellationToke return variant; } - public async ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken) + public async ValueTask GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken) { string cacheKey = GetVariantCacheKey(feature); @@ -88,7 +89,7 @@ public async ValueTask GetVariantAsync(string feature, TConte return _variantCache[cacheKey]; } - Variant variant = await _featureManager.GetVariantAsync(feature, context, cancellationToken).ConfigureAwait(false); + Variant variant = await _featureManager.GetVariantAsync(feature, context, cancellationToken).ConfigureAwait(false); _variantCache[cacheKey] = variant; diff --git a/src/Microsoft.FeatureManagement/FeatureVariantAllocationContext.cs b/src/Microsoft.FeatureManagement/FeatureVariantAllocationContext.cs deleted file mode 100644 index ccbb126b..00000000 --- a/src/Microsoft.FeatureManagement/FeatureVariantAllocationContext.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -namespace Microsoft.FeatureManagement -{ - /// - /// Contextual information needed during the process of feature variant allocation - /// - public class FeatureVariantAllocationContext - { - /// - /// The definition of the feature in need of an allocated variant - /// - public FeatureDefinition FeatureDefinition { get; set; } - } -} diff --git a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs index bdceccac..9e503b1e 100644 --- a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using Microsoft.FeatureManagement.FeatureFilters; using System.Threading; using System.Threading.Tasks; @@ -9,16 +10,15 @@ namespace Microsoft.FeatureManagement /// /// Provides a method to allocate a variant of a feature to be used based off of custom conditions. /// - /// A custom type that the allocator requires to perform allocation - public interface IContextualFeatureVariantAllocator : IFeatureVariantAllocatorMetadata + public interface IContextualFeatureVariantAllocator { /// /// Allocate a variant of a feature to be used based off of customized criteria. /// - /// A variant allocation context that contains information needed to allocate 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 allocating a variant of a feature. + /// Contains all of the properties defined for a feature in feature management. + /// The targeting context used to determine which variant should be allocated. /// The cancellation token to cancel the operation. /// The variant that should be allocated for a given feature. - ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, TContext appContext, CancellationToken cancellationToken = default); + ValueTask AllocateVariantAsync(FeatureDefinition featureDefinition, TargetingContext targetingContext, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureManagementBuilder.cs b/src/Microsoft.FeatureManagement/IFeatureManagementBuilder.cs index 885a0d9e..6365c098 100644 --- a/src/Microsoft.FeatureManagement/IFeatureManagementBuilder.cs +++ b/src/Microsoft.FeatureManagement/IFeatureManagementBuilder.cs @@ -24,15 +24,6 @@ public interface IFeatureManagementBuilder /// The feature management builder. IFeatureManagementBuilder AddFeatureFilter() where T : IFeatureFilterMetadata; - /// - /// Adds a given feature variant allocator to the list of feature variant allocators that will be available to allocate feature variants during runtime. - /// Possible feature variant allocator metadata types include and - /// Only one feature variant allocator interface can be implemented by a single type. - /// - /// An implementation of - /// The feature management builder. - IFeatureManagementBuilder AddFeatureVariantAllocator() where T : IFeatureVariantAllocatorMetadata; - /// /// Adds an to be used for storing feature state in a session. /// diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs deleted file mode 100644 index 312f4078..00000000 --- a/src/Microsoft.FeatureManagement/IFeatureVariantAllocator.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.FeatureManagement -{ - /// - /// Provides a method to allocate a variant of a feature to be used based off of custom conditions. - /// - public interface IFeatureVariantAllocator : IFeatureVariantAllocatorMetadata - { - /// - /// Allocate a variant of a feature to be used based off of customized criteria. - /// - /// A variant allocation context that contains information needed to allocate a variant for a feature. - /// The cancellation token to cancel the operation. - /// The variant that should be allocated for a given feature. - ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, CancellationToken cancellationToken = default); - } -} diff --git a/src/Microsoft.FeatureManagement/IFeatureVariantAllocatorMetadata.cs b/src/Microsoft.FeatureManagement/IFeatureVariantAllocatorMetadata.cs deleted file mode 100644 index 7ed9a2e0..00000000 --- a/src/Microsoft.FeatureManagement/IFeatureVariantAllocatorMetadata.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -namespace Microsoft.FeatureManagement -{ - /// - /// Marker interface for feature variant allocators used to allocate which variant should be used for a dynamic feature. - /// - public interface IFeatureVariantAllocatorMetadata - { - } -} diff --git a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs index dda96275..cd0a2ddc 100644 --- a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs @@ -3,6 +3,7 @@ // using System.Threading.Tasks; using System.Threading; +using Microsoft.FeatureManagement.FeatureFilters; namespace Microsoft.FeatureManagement { @@ -40,6 +41,6 @@ public interface IVariantFeatureManager /// A context providing information that can be used to evaluate which variant the user will be assigned. /// The cancellation token to cancel the operation. /// A variant assigned to the user based on the feature's allocation logic. - ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken = default); + ValueTask GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs index 7cbe38fc..92494238 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs @@ -14,10 +14,8 @@ namespace Microsoft.FeatureManagement.Allocators /// /// A feature variant allocator that can be used to allocate a variant based on targeted audiences. /// - [AllocatorAlias(Alias)] - public class ContextualTargetingFeatureVariantAllocator : IContextualFeatureVariantAllocator + public class ContextualTargetingFeatureVariantAllocator : IContextualFeatureVariantAllocator { - private const string Alias = "Microsoft.Targeting"; private readonly TargetingEvaluationOptions _options; /// @@ -32,15 +30,15 @@ public ContextualTargetingFeatureVariantAllocator(IOptions /// Allocates one of the variants configured for a feature based off the provided targeting context. /// - /// Contextual information available for use during the allocation process. + /// Contains all of the properties defined for a feature in feature management. /// The targeting context used to determine which variant should be allocated. /// The cancellation token to cancel the operation. /// - public ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, ITargetingContext targetingContext, CancellationToken cancellationToken) + public ValueTask AllocateVariantAsync(FeatureDefinition featureDefinition, TargetingContext targetingContext, CancellationToken cancellationToken) { - if (variantAllocationContext == null) + if (featureDefinition == null) { - throw new ArgumentNullException(nameof(variantAllocationContext)); + throw new ArgumentNullException(nameof(featureDefinition)); } if (targetingContext == null) @@ -48,20 +46,18 @@ public ValueTask AllocateVariantAsync(FeatureVariantAllocationCo throw new ArgumentNullException(nameof(targetingContext)); } - FeatureDefinition featureDefinition = variantAllocationContext.FeatureDefinition; - if (featureDefinition == null) { throw new ArgumentException( - $"{nameof(variantAllocationContext)}.{nameof(variantAllocationContext.FeatureDefinition)} cannot be null.", - nameof(variantAllocationContext)); + $"{nameof(featureDefinition)}.{nameof(featureDefinition)} cannot be null.", + nameof(featureDefinition)); } if (featureDefinition.Variants == null) { throw new ArgumentException( - $"{nameof(variantAllocationContext)}.{nameof(variantAllocationContext.FeatureDefinition)}.{nameof(featureDefinition.Variants)} cannot be null.", - nameof(variantAllocationContext)); + $"{nameof(featureDefinition)}.{nameof(featureDefinition.Variants)} cannot be null.", + nameof(featureDefinition)); } FeatureVariant variant = null; diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs deleted file mode 100644 index 163226b7..00000000 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingFeatureVariantAllocator.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.FeatureManagement.FeatureFilters; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.FeatureManagement.Allocators -{ - /// - /// A feature variant allocator that can be used to allocate a variant based on targeted audiences. - /// - [AllocatorAlias(Alias)] - public class TargetingFeatureVariantAllocator : IFeatureVariantAllocator - { - private const string Alias = "Microsoft.Targeting"; - private readonly ITargetingContextAccessor _contextAccessor; - private readonly IContextualFeatureVariantAllocator _contextualResolver; - private readonly ILogger _logger; - - /// - /// Creates a feature variant allocator that uses targeting to allocate which of a dynamic feature's registered variants should be used. - /// - /// The options controlling how targeting is performed. - /// An accessor for the targeting context required to perform a targeting evaluation. - /// A logger factory for producing logs. - public TargetingFeatureVariantAllocator(IOptions options, - ITargetingContextAccessor contextAccessor, - ILoggerFactory loggerFactory) - { - _contextAccessor = contextAccessor ?? throw new ArgumentNullException(nameof(contextAccessor)); - _contextualResolver = new ContextualTargetingFeatureVariantAllocator(options); - _logger = loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)); - } - - /// - /// Allocates one of the variants configured for a feature based off the provided targeting context. - /// - /// Contextual information available for use during the allocation process. - /// The cancellation token to cancel the operation. - /// - public async ValueTask AllocateVariantAsync(FeatureVariantAllocationContext variantAllocationContext, CancellationToken cancellationToken) - { - if (variantAllocationContext == null) - { - throw new ArgumentNullException(nameof(variantAllocationContext)); - } - - // - // Acquire targeting context via accessor - TargetingContext targetingContext = await _contextAccessor.GetContextAsync(cancellationToken).ConfigureAwait(false); - - // - // Ensure targeting can be performed - if (targetingContext == null) - { - _logger.LogWarning("No targeting context available for targeting evaluation."); - - return null; - } - - return await _contextualResolver.AllocateVariantAsync(variantAllocationContext, targetingContext, cancellationToken).ConfigureAwait(false); - } - } -} From a259e23606e3f6ac693c62c3acd1ace688c87419 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 31 Jul 2023 14:50:33 -0500 Subject: [PATCH 28/80] fix naming from allocator to assigner for classes and files --- .../FeatureManagementError.cs | 11 +---------- src/Microsoft.FeatureManagement/FeatureManager.cs | 7 +++---- ...tor.cs => IContextualFeatureVariantAssigner.cs} | 12 ++++++------ src/Microsoft.FeatureManagement/NameHelper.cs | 10 +++++----- ...> ContextualTargetingFeatureVariantAssigner.cs} | 14 +++++++------- 5 files changed, 22 insertions(+), 32 deletions(-) rename src/Microsoft.FeatureManagement/{IContextualFeatureVariantAllocator.cs => IContextualFeatureVariantAssigner.cs} (51%) rename src/Microsoft.FeatureManagement/Targeting/{ContextualTargetingFeatureVariantAllocator.cs => ContextualTargetingFeatureVariantAssigner.cs} (85%) diff --git a/src/Microsoft.FeatureManagement/FeatureManagementError.cs b/src/Microsoft.FeatureManagement/FeatureManagementError.cs index e08460cc..e254114a 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementError.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementError.cs @@ -36,15 +36,6 @@ public enum FeatureManagementError /// /// A feature does not have any feature variants registered when attempting to resolve the variant. /// - MissingFeatureVariant, - - /// - /// The feature variant allocator configured for the feature being evaluated is an ambiguous reference to multiple registered feature variant allocators. - /// - AmbiguousFeatureVariantAllocator, - - MissingFeatureVariantAllocator, - - InvalidFeatureVariantAllocator + MissingFeatureVariant } } diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index a14f4417..ff73b513 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.FeatureManagement.Allocators; using Microsoft.FeatureManagement.FeatureFilters; using System; using System.Collections.Concurrent; @@ -28,7 +27,7 @@ class FeatureManager : IFeatureManager, IDisposable, IVariantFeatureManager private readonly IFeatureDefinitionProvider _featureDefinitionProvider; private readonly IEnumerable _featureFilters; private readonly IEnumerable _sessionManagers; - private readonly IContextualFeatureVariantAllocator _contextualFeatureVariantAllocator; + private readonly IContextualFeatureVariantAssigner _contextualFeatureVariantAssigner; private readonly ITargetingContextAccessor _contextAccessor; private readonly IConfiguration _configuration; private readonly ILogger _logger; @@ -60,7 +59,7 @@ public FeatureManager( _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _contextAccessor = contextAccessor ?? throw new ArgumentNullException(nameof(contextAccessor)); _logger = loggerFactory.CreateLogger(); - _contextualFeatureVariantAllocator = new ContextualTargetingFeatureVariantAllocator(assignerOptions); + _contextualFeatureVariantAssigner = new ContextualTargetingFeatureVariantAssigner(assignerOptions); _filterMetadataCache = new ConcurrentDictionary(); _contextualFeatureFilterCache = new ConcurrentDictionary(); _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); @@ -364,7 +363,7 @@ private async ValueTask GetFeatureVariantAsync(FeatureDefinition } } - featureVariant = await _contextualFeatureVariantAllocator.AllocateVariantAsync(featureDefinition, context, cancellationToken).ConfigureAwait(false); + featureVariant = await _contextualFeatureVariantAssigner.AssignVariantAsync(featureDefinition, context, cancellationToken).ConfigureAwait(false); if (featureVariant == null) { diff --git a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAssigner.cs similarity index 51% rename from src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs rename to src/Microsoft.FeatureManagement/IContextualFeatureVariantAssigner.cs index 9e503b1e..ef92b3bd 100644 --- a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAssigner.cs @@ -8,17 +8,17 @@ namespace Microsoft.FeatureManagement { /// - /// Provides a method to allocate a variant of a feature to be used based off of custom conditions. + /// Provides a method to assign a variant of a feature to be used based off of custom conditions. /// - public interface IContextualFeatureVariantAllocator + public interface IContextualFeatureVariantAssigner { /// - /// Allocate a variant of a feature to be used based off of customized criteria. + /// Assign a variant of a feature to be used based off of customized criteria. /// /// Contains all of the properties defined for a feature in feature management. - /// The targeting context used to determine which variant should be allocated. + /// The targeting context used to determine which variant should be assigned. /// The cancellation token to cancel the operation. - /// The variant that should be allocated for a given feature. - ValueTask AllocateVariantAsync(FeatureDefinition featureDefinition, TargetingContext targetingContext, CancellationToken cancellationToken = default); + /// The variant that should be assigned for a given feature. + ValueTask AssignVariantAsync(FeatureDefinition featureDefinition, TargetingContext targetingContext, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/NameHelper.cs b/src/Microsoft.FeatureManagement/NameHelper.cs index c0647bbb..70e1f802 100644 --- a/src/Microsoft.FeatureManagement/NameHelper.cs +++ b/src/Microsoft.FeatureManagement/NameHelper.cs @@ -9,11 +9,11 @@ namespace Microsoft.FeatureManagement static class NameHelper { /// - /// Evaluates whether a feature filter or feature variant allocator reference matches a given feature filter/allocator name. + /// Evaluates whether a feature filter reference matches a given feature filter 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 allocator - /// An optional suffix that may be included when referencing the metadata type. E.g. "filter" or "allocator". + /// The name used by the feature filter. + /// An optional suffix that may be included when referencing the metadata type. E.g. "filter". /// True if the reference is a match for the metadata name. False otherwise. public static bool IsMatchingReference(string reference, string metadataName, string suffix) { @@ -28,7 +28,7 @@ public static bool IsMatchingReference(string reference, string metadataName, st } // - // Feature filters/allocator can be referenced with or without their associated suffix ('filter' or 'allocator') + // Feature filters can be referenced with or without their associated suffix ('filter') // 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)) @@ -37,7 +37,7 @@ public static bool IsMatchingReference(string reference, string metadataName, st } // - // Feature filters/allocators can have namespaces in their alias + // 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 (reference.Contains('.')) diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs similarity index 85% rename from src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs rename to src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs index 92494238..595c3d32 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAllocator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs @@ -9,12 +9,12 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.FeatureManagement.Allocators +namespace Microsoft.FeatureManagement.Assigners { /// - /// A feature variant allocator that can be used to allocate a variant based on targeted audiences. + /// A feature variant assigner that can be used to assign a variant based on targeted audiences. /// - public class ContextualTargetingFeatureVariantAllocator : IContextualFeatureVariantAllocator + public class ContextualTargetingFeatureVariantAssigner : IContextualFeatureVariantAssigner { private readonly TargetingEvaluationOptions _options; @@ -22,19 +22,19 @@ public class ContextualTargetingFeatureVariantAllocator : IContextualFeatureVari /// Creates a targeting contextual feature filter. /// /// Options controlling the behavior of the targeting evaluation performed by the filter. - public ContextualTargetingFeatureVariantAllocator(IOptions options) + public ContextualTargetingFeatureVariantAssigner(IOptions options) { _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); } /// - /// Allocates one of the variants configured for a feature based off the provided targeting context. + /// Assigns one of the variants configured for a feature based off the provided targeting context. /// /// Contains all of the properties defined for a feature in feature management. - /// The targeting context used to determine which variant should be allocated. + /// The targeting context used to determine which variant should be assigned. /// The cancellation token to cancel the operation. /// - public ValueTask AllocateVariantAsync(FeatureDefinition featureDefinition, TargetingContext targetingContext, CancellationToken cancellationToken) + public ValueTask AssignVariantAsync(FeatureDefinition featureDefinition, TargetingContext targetingContext, CancellationToken cancellationToken) { if (featureDefinition == null) { From 285042141acbc35843ecdfcdbda24927dae38f3e Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 31 Jul 2023 15:06:37 -0500 Subject: [PATCH 29/80] cleanup extra methods, todo config section logic --- .../FeatureManager.cs | 30 +++++++------------ src/Microsoft.FeatureManagement/Variant.cs | 5 ---- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index ff73b513..4926a9dc 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -311,21 +311,7 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex return null; } - Variant returnVariant = new Variant() - { - Name = featureVariant.Name, - Configuration = GetVariantConfiguration(featureVariant) - }; - - return returnVariant; - } - - private IConfigurationSection GetVariantConfiguration(FeatureVariant featureVariant) - { - if (featureVariant == null) - { - throw new ArgumentNullException(nameof(featureVariant)); - } + IConfigurationSection variantConfiguration; // TODO how to return IConfigurationSection here? get to the one referenced here somehow? //if (featureVariant.ConfigurationValue != null) @@ -333,7 +319,15 @@ private IConfigurationSection GetVariantConfiguration(FeatureVariant featureVari // return new ConfigurationSection(_configuration., ); //} - return _configuration.GetSection($"{featureVariant.ConfigurationReference}"); + variantConfiguration = _configuration.GetSection($"{featureVariant.ConfigurationReference}"); + + Variant returnVariant = new Variant() + { + Name = featureVariant.Name, + Configuration = variantConfiguration + }; + + return returnVariant; } private async ValueTask GetFeatureVariantAsync(FeatureDefinition featureDefinition, TargetingContext context, bool useContext, bool isFeatureEnabled, CancellationToken cancellationToken) @@ -345,8 +339,6 @@ private async ValueTask GetFeatureVariantAsync(FeatureDefinition FeatureVariant featureVariant; - // TODO rename back to assigner - if (!useContext) { // @@ -367,7 +359,7 @@ private async ValueTask GetFeatureVariantAsync(FeatureDefinition if (featureVariant == null) { - return ResolveDefaultFeatureVariant(featureDefinition, isFeatureEnabled); + featureVariant = ResolveDefaultFeatureVariant(featureDefinition, isFeatureEnabled); } return featureVariant; diff --git a/src/Microsoft.FeatureManagement/Variant.cs b/src/Microsoft.FeatureManagement/Variant.cs index 11a5bc5a..62f1ed2c 100644 --- a/src/Microsoft.FeatureManagement/Variant.cs +++ b/src/Microsoft.FeatureManagement/Variant.cs @@ -19,10 +19,5 @@ public class Variant /// /// public IConfigurationSection Configuration { get; set; } - - /// - /// - /// - public string ConfigurationValue { get; set; } } } From 96b636d76910695dd8530d86316708aebd532dd5 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 31 Jul 2023 16:38:49 -0500 Subject: [PATCH 30/80] in progress adding configurationsection returned when using configurationvalue --- .../Controllers/HomeController.cs | 1 + examples/FeatureFlagDemo/appsettings.json | 14 +++++- .../FeatureManagementError.cs | 7 ++- .../FeatureManager.cs | 46 +++++++++++-------- ...ntextualTargetingFeatureVariantAssigner.cs | 2 +- .../VariantConfigurationSection.cs | 15 ++++++ 6 files changed, 63 insertions(+), 22 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/VariantConfigurationSection.cs diff --git a/examples/FeatureFlagDemo/Controllers/HomeController.cs b/examples/FeatureFlagDemo/Controllers/HomeController.cs index e694900f..cd9e22d0 100644 --- a/examples/FeatureFlagDemo/Controllers/HomeController.cs +++ b/examples/FeatureFlagDemo/Controllers/HomeController.cs @@ -25,6 +25,7 @@ public HomeController(IVariantFeatureManagerSnapshot featureSnapshot) public async Task Index() { Variant test = await _featureManager.GetVariantAsync(nameof(MyFeatureFlags.Banner), CancellationToken.None); + var x = test.Configuration["Size"]; bool isEnabled = await _featureManager.IsEnabledAsync(nameof(MyFeatureFlags.Banner), CancellationToken.None); return View(); } diff --git a/examples/FeatureFlagDemo/appsettings.json b/examples/FeatureFlagDemo/appsettings.json index 1cb76654..9aee5450 100644 --- a/examples/FeatureFlagDemo/appsettings.json +++ b/examples/FeatureFlagDemo/appsettings.json @@ -5,7 +5,7 @@ } }, "AllowedHosts": "*", - + // Define feature flags in config file "FeatureManagement": { @@ -99,7 +99,7 @@ "Variants": [ { "Name": "Big", - "ConfigurationValue": "1200px", + "ConfigurationReference": "ShoppingCart:Big", "StatusOverride": "Disabled" }, { @@ -116,5 +116,15 @@ } ] } + }, + "ShoppingCart": { + "Big": { + "Size": 400, + "Color": "green" + }, + "Small": { + "Size": 150, + "Color": "gray" + } } } diff --git a/src/Microsoft.FeatureManagement/FeatureManagementError.cs b/src/Microsoft.FeatureManagement/FeatureManagementError.cs index e254114a..18006b6b 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementError.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementError.cs @@ -36,6 +36,11 @@ public enum FeatureManagementError /// /// A feature does not have any feature variants registered when attempting to resolve the variant. /// - MissingFeatureVariant + MissingFeatureVariant, + + /// + /// A feature variant specifies both a value and reference for its configuration. + /// + InvalidVariantConfiguration } } diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 4926a9dc..ff9f16c5 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -234,20 +234,17 @@ await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).Con if (!ignoreVariant && (featureDefinition.Variants?.Any() ?? false) && featureDefinition.Status != Status.Disabled) { - if (appContext is TargetingContext) - { - FeatureVariant featureVariant = await GetFeatureVariantAsync(featureDefinition, appContext as TargetingContext, useAppContext, enabled, cancellationToken); + FeatureVariant featureVariant = await GetFeatureVariantAsync(featureDefinition, appContext as TargetingContext, useAppContext, enabled, cancellationToken); - if (featureVariant != null) + if (featureVariant != null) + { + if (featureVariant.StatusOverride == StatusOverride.Enabled) { - if (featureVariant.StatusOverride == StatusOverride.Enabled) - { - enabled = true; - } - else if (featureVariant.StatusOverride == StatusOverride.Disabled) - { - enabled = false; - } + enabled = true; + } + else if (featureVariant.StatusOverride == StatusOverride.Disabled) + { + enabled = false; } } } @@ -313,13 +310,26 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex IConfigurationSection variantConfiguration; - // TODO how to return IConfigurationSection here? get to the one referenced here somehow? - //if (featureVariant.ConfigurationValue != null) - //{ - // return new ConfigurationSection(_configuration., ); - //} + if (!string.IsNullOrEmpty(featureVariant.ConfigurationValue) && !string.IsNullOrEmpty(featureVariant.ConfigurationReference)) + { + throw new FeatureManagementException( + FeatureManagementError.InvalidVariantConfiguration, + $"Both ConfigurationValue and ConfigurationReference are specified for the variant {featureVariant.Name} in feature {feature}"); + } - variantConfiguration = _configuration.GetSection($"{featureVariant.ConfigurationReference}"); + if (!string.IsNullOrEmpty(featureVariant.ConfigurationReference)) + { + variantConfiguration = _configuration.GetSection($"{featureVariant.ConfigurationReference}"); + } + else if (!string.IsNullOrEmpty(featureVariant.ConfigurationValue)) + { + variantConfiguration = null; + // TODO how to return IConfigurationSection here if it uses ConfigurationValue? get to the one referenced here somehow? + //if (featureVariant.ConfigurationValue != null) + //{ + // return new ConfigurationSection(); + //} + } Variant returnVariant = new Variant() { diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs index 595c3d32..1ff27c86 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs @@ -9,7 +9,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.FeatureManagement.Assigners +namespace Microsoft.FeatureManagement { /// /// A feature variant assigner that can be used to assign a variant based on targeted audiences. diff --git a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs new file mode 100644 index 00000000..bd11323b --- /dev/null +++ b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// + +using Microsoft.Extensions.Configuration; + +namespace Microsoft.FeatureManagement +{ + internal class VariantConfigurationSection : ConfigurationSection + { + public VariantConfigurationSection(ConfigurationRoot root, string path) : base(root, path) {} + + + } +} From 43e1b13553e8342425add30619c7cff1e1d82b98 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 31 Jul 2023 17:08:32 -0500 Subject: [PATCH 31/80] continuation of last commit --- .../Controllers/HomeController.cs | 1 + examples/FeatureFlagDemo/appsettings.json | 1 + .../FeatureManager.cs | 21 ++++++++----------- .../VariantConfigurationSection.cs | 9 ++++++++ 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/examples/FeatureFlagDemo/Controllers/HomeController.cs b/examples/FeatureFlagDemo/Controllers/HomeController.cs index cd9e22d0..edf7e6ec 100644 --- a/examples/FeatureFlagDemo/Controllers/HomeController.cs +++ b/examples/FeatureFlagDemo/Controllers/HomeController.cs @@ -26,6 +26,7 @@ public async Task Index() { Variant test = await _featureManager.GetVariantAsync(nameof(MyFeatureFlags.Banner), CancellationToken.None); var x = test.Configuration["Size"]; + var y = test.Configuration.Value; bool isEnabled = await _featureManager.IsEnabledAsync(nameof(MyFeatureFlags.Banner), CancellationToken.None); return View(); } diff --git a/examples/FeatureFlagDemo/appsettings.json b/examples/FeatureFlagDemo/appsettings.json index 9aee5450..a03a5b52 100644 --- a/examples/FeatureFlagDemo/appsettings.json +++ b/examples/FeatureFlagDemo/appsettings.json @@ -68,6 +68,7 @@ ] }, "Banner": { + "Status": "Disabled", "Allocation": { "DefaultWhenEnabled": "Small", "DefaultWhenDisabled": "Small", diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index ff9f16c5..930dabf0 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -308,27 +308,24 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex return null; } - IConfigurationSection variantConfiguration; + IConfigurationSection variantConfiguration = null; - if (!string.IsNullOrEmpty(featureVariant.ConfigurationValue) && !string.IsNullOrEmpty(featureVariant.ConfigurationReference)) + bool configValueSet = !string.IsNullOrEmpty(featureVariant.ConfigurationValue); + bool configReferenceValueSet = !string.IsNullOrEmpty(featureVariant.ConfigurationReference); + + if (configValueSet && configReferenceValueSet) { throw new FeatureManagementException( FeatureManagementError.InvalidVariantConfiguration, $"Both ConfigurationValue and ConfigurationReference are specified for the variant {featureVariant.Name} in feature {feature}"); } - - if (!string.IsNullOrEmpty(featureVariant.ConfigurationReference)) + else if (configReferenceValueSet) { - variantConfiguration = _configuration.GetSection($"{featureVariant.ConfigurationReference}"); + variantConfiguration = _configuration.GetSection(featureVariant.ConfigurationReference); } - else if (!string.IsNullOrEmpty(featureVariant.ConfigurationValue)) + else if (configValueSet) { - variantConfiguration = null; - // TODO how to return IConfigurationSection here if it uses ConfigurationValue? get to the one referenced here somehow? - //if (featureVariant.ConfigurationValue != null) - //{ - // return new ConfigurationSection(); - //} + variantConfiguration = _configuration.GetSection(); } Variant returnVariant = new Variant() diff --git a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs index bd11323b..2fb75bc5 100644 --- a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs +++ b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs @@ -10,6 +10,15 @@ internal class VariantConfigurationSection : ConfigurationSection { public VariantConfigurationSection(ConfigurationRoot root, string path) : base(root, path) {} + public VariantConfigurationSection(string configurationValue) + { + } + + public new string Value + { + get => this["Value"]; + set => this["Value"] = value; + } } } From 5f6328ecd6e5d88e0d18ee8248ce3a5955732ac0 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 1 Aug 2023 11:17:29 -0500 Subject: [PATCH 32/80] working return for configvalue --- .../Controllers/HomeController.cs | 5 ++-- examples/FeatureFlagDemo/appsettings.json | 3 +-- .../FeatureManager.cs | 8 ++++++- ...ntextualTargetingFeatureVariantAssigner.cs | 10 -------- .../VariantConfigurationSection.cs | 24 ------------------- 5 files changed, 11 insertions(+), 39 deletions(-) delete mode 100644 src/Microsoft.FeatureManagement/VariantConfigurationSection.cs diff --git a/examples/FeatureFlagDemo/Controllers/HomeController.cs b/examples/FeatureFlagDemo/Controllers/HomeController.cs index edf7e6ec..7cb95121 100644 --- a/examples/FeatureFlagDemo/Controllers/HomeController.cs +++ b/examples/FeatureFlagDemo/Controllers/HomeController.cs @@ -9,6 +9,7 @@ using Microsoft.FeatureManagement.Mvc; using System.Threading.Tasks; using System.Threading; +using Microsoft.Extensions.Configuration; namespace FeatureFlagDemo.Controllers { @@ -25,8 +26,8 @@ public HomeController(IVariantFeatureManagerSnapshot featureSnapshot) public async Task Index() { Variant test = await _featureManager.GetVariantAsync(nameof(MyFeatureFlags.Banner), CancellationToken.None); - var x = test.Configuration["Size"]; - var y = test.Configuration.Value; + string x = test.Configuration["Size"]; + string y = test.Configuration.Value; bool isEnabled = await _featureManager.IsEnabledAsync(nameof(MyFeatureFlags.Banner), CancellationToken.None); return View(); } diff --git a/examples/FeatureFlagDemo/appsettings.json b/examples/FeatureFlagDemo/appsettings.json index a03a5b52..09a2bee1 100644 --- a/examples/FeatureFlagDemo/appsettings.json +++ b/examples/FeatureFlagDemo/appsettings.json @@ -68,7 +68,6 @@ ] }, "Banner": { - "Status": "Disabled", "Allocation": { "DefaultWhenEnabled": "Small", "DefaultWhenDisabled": "Small", @@ -92,7 +91,7 @@ { "Variant": "Big", "From": 0, - "To": 100 + "To": 0 } ], "Seed": 13973240 diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 930dabf0..c63d7142 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -325,7 +325,13 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex } else if (configValueSet) { - variantConfiguration = _configuration.GetSection(); + foreach (IConfigurationSection section in _configuration.GetSection($"FeatureManagement:{feature}:Variants").GetChildren()) + { + if (section["Name"] == featureVariant.Name) + { + variantConfiguration = _configuration.GetSection($"{section.Path}:ConfigurationValue"); + } + } } Variant returnVariant = new Variant() diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs index 1ff27c86..33c7d757 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs @@ -101,16 +101,6 @@ public ValueTask AssignVariantAsync(FeatureDefinition featureDef } } - if (!string.IsNullOrEmpty(featureDefinition.Allocation.DefaultWhenEnabled)) - { - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(featureDefinition.Allocation.DefaultWhenEnabled)); - - if (!string.IsNullOrEmpty(variant.Name)) - { - return new ValueTask(variant); - } - } - return new ValueTask(variant); } } diff --git a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs deleted file mode 100644 index 2fb75bc5..00000000 --- a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// - -using Microsoft.Extensions.Configuration; - -namespace Microsoft.FeatureManagement -{ - internal class VariantConfigurationSection : ConfigurationSection - { - public VariantConfigurationSection(ConfigurationRoot root, string path) : base(root, path) {} - - public VariantConfigurationSection(string configurationValue) - { - - } - - public new string Value - { - get => this["Value"]; - set => this["Value"] = value; - } - } -} From fbc20fe84385aaf62893b7b30cc1b8a918e65495 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 1 Aug 2023 12:20:36 -0500 Subject: [PATCH 33/80] move logic to featuremanager for assigning --- .../Controllers/HomeController.cs | 1 - .../FeatureManager.cs | 77 ++++++++++++++++++- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/examples/FeatureFlagDemo/Controllers/HomeController.cs b/examples/FeatureFlagDemo/Controllers/HomeController.cs index 7cb95121..2e6a1e0b 100644 --- a/examples/FeatureFlagDemo/Controllers/HomeController.cs +++ b/examples/FeatureFlagDemo/Controllers/HomeController.cs @@ -9,7 +9,6 @@ using Microsoft.FeatureManagement.Mvc; using System.Threading.Tasks; using System.Threading; -using Microsoft.Extensions.Configuration; namespace FeatureFlagDemo.Controllers { diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index c63d7142..075733b1 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.FeatureManagement.FeatureFilters; +using Microsoft.FeatureManagement.Targeting; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -27,13 +28,13 @@ class FeatureManager : IFeatureManager, IDisposable, IVariantFeatureManager private readonly IFeatureDefinitionProvider _featureDefinitionProvider; private readonly IEnumerable _featureFilters; private readonly IEnumerable _sessionManagers; - private readonly IContextualFeatureVariantAssigner _contextualFeatureVariantAssigner; private readonly ITargetingContextAccessor _contextAccessor; private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly ConcurrentDictionary _filterMetadataCache; private readonly ConcurrentDictionary _contextualFeatureFilterCache; private readonly FeatureManagementOptions _options; + private readonly TargetingEvaluationOptions _assignerOptions; private readonly IMemoryCache _parametersCache; private class ConfigurationCacheItem @@ -59,7 +60,7 @@ public FeatureManager( _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _contextAccessor = contextAccessor ?? throw new ArgumentNullException(nameof(contextAccessor)); _logger = loggerFactory.CreateLogger(); - _contextualFeatureVariantAssigner = new ContextualTargetingFeatureVariantAssigner(assignerOptions); + _assignerOptions = assignerOptions?.Value ?? throw new ArgumentNullException(nameof(assignerOptions)); _filterMetadataCache = new ConcurrentDictionary(); _contextualFeatureFilterCache = new ConcurrentDictionary(); _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); @@ -368,7 +369,7 @@ private async ValueTask GetFeatureVariantAsync(FeatureDefinition } } - featureVariant = await _contextualFeatureVariantAssigner.AssignVariantAsync(featureDefinition, context, cancellationToken).ConfigureAwait(false); + featureVariant = await AssignVariantAsync(featureDefinition, context, cancellationToken).ConfigureAwait(false); if (featureVariant == null) { @@ -378,6 +379,76 @@ private async ValueTask GetFeatureVariantAsync(FeatureDefinition return featureVariant; } + private ValueTask AssignVariantAsync(FeatureDefinition featureDefinition, TargetingContext targetingContext, CancellationToken cancellationToken) + { + if (featureDefinition == null) + { + throw new ArgumentNullException(nameof(featureDefinition)); + } + + if (targetingContext == null) + { + throw new ArgumentNullException(nameof(targetingContext)); + } + + if (featureDefinition == null) + { + throw new ArgumentException( + $"{nameof(featureDefinition)}.{nameof(featureDefinition)} cannot be null.", + nameof(featureDefinition)); + } + + if (featureDefinition.Variants == null) + { + throw new ArgumentException( + $"{nameof(featureDefinition)}.{nameof(featureDefinition.Variants)} cannot be null.", + nameof(featureDefinition)); + } + + FeatureVariant variant = null; + + foreach (User user in featureDefinition.Allocation.User) + { + if (TargetingEvaluator.IsTargeted(targetingContext, user.Users, _assignerOptions.IgnoreCase)) + { + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(user.Variant)); + + if (!string.IsNullOrEmpty(variant.Name)) + { + return new ValueTask(variant); + } + } + } + + foreach (Group group in featureDefinition.Allocation.Group) + { + if (TargetingEvaluator.IsGroupTargeted(targetingContext, group.Groups, _assignerOptions.IgnoreCase)) + { + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(group.Variant)); + + if (!string.IsNullOrEmpty(variant.Name)) + { + return new ValueTask(variant); + } + } + } + + foreach (Percentile percentile in featureDefinition.Allocation.Percentile) + { + if (TargetingEvaluator.IsTargeted(targetingContext, percentile.From, percentile.To, featureDefinition.Allocation.Seed, _assignerOptions.IgnoreCase, featureDefinition.Name)) + { + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(percentile.Variant)); + + if (!string.IsNullOrEmpty(variant.Name)) + { + return new ValueTask(variant); + } + } + } + + return new ValueTask(variant); + } + private FeatureVariant ResolveDefaultFeatureVariant(FeatureDefinition featureDefinition, bool isFeatureEnabled) { string defaultVariantPath = isFeatureEnabled ? featureDefinition.Allocation.DefaultWhenEnabled : featureDefinition.Allocation.DefaultWhenDisabled; From 46a263e5b2a1450fdd17caf78f15f367cfe96b3e Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 1 Aug 2023 12:30:37 -0500 Subject: [PATCH 34/80] remove unused assigner classes --- .../IContextualFeatureVariantAssigner.cs | 24 ---- ...ntextualTargetingFeatureVariantAssigner.cs | 107 ------------------ 2 files changed, 131 deletions(-) delete mode 100644 src/Microsoft.FeatureManagement/IContextualFeatureVariantAssigner.cs delete mode 100644 src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs diff --git a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAssigner.cs b/src/Microsoft.FeatureManagement/IContextualFeatureVariantAssigner.cs deleted file mode 100644 index ef92b3bd..00000000 --- a/src/Microsoft.FeatureManagement/IContextualFeatureVariantAssigner.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Microsoft.FeatureManagement.FeatureFilters; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.FeatureManagement -{ - /// - /// Provides a method to assign a variant of a feature to be used based off of custom conditions. - /// - public interface IContextualFeatureVariantAssigner - { - /// - /// Assign a variant of a feature to be used based off of customized criteria. - /// - /// Contains all of the properties defined for a feature in feature management. - /// The targeting context used to determine which variant should be assigned. - /// The cancellation token to cancel the operation. - /// The variant that should be assigned for a given feature. - ValueTask AssignVariantAsync(FeatureDefinition featureDefinition, TargetingContext targetingContext, CancellationToken cancellationToken = default); - } -} diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs deleted file mode 100644 index 33c7d757..00000000 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFeatureVariantAssigner.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Microsoft.Extensions.Options; -using Microsoft.FeatureManagement.FeatureFilters; -using Microsoft.FeatureManagement.Targeting; -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.FeatureManagement -{ - /// - /// A feature variant assigner that can be used to assign a variant based on targeted audiences. - /// - public class ContextualTargetingFeatureVariantAssigner : IContextualFeatureVariantAssigner - { - private readonly TargetingEvaluationOptions _options; - - /// - /// Creates a targeting contextual feature filter. - /// - /// Options controlling the behavior of the targeting evaluation performed by the filter. - public ContextualTargetingFeatureVariantAssigner(IOptions options) - { - _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); - } - - /// - /// Assigns one of the variants configured for a feature based off the provided targeting context. - /// - /// Contains all of the properties defined for a feature in feature management. - /// The targeting context used to determine which variant should be assigned. - /// The cancellation token to cancel the operation. - /// - public ValueTask AssignVariantAsync(FeatureDefinition featureDefinition, TargetingContext targetingContext, CancellationToken cancellationToken) - { - if (featureDefinition == null) - { - throw new ArgumentNullException(nameof(featureDefinition)); - } - - if (targetingContext == null) - { - throw new ArgumentNullException(nameof(targetingContext)); - } - - if (featureDefinition == null) - { - throw new ArgumentException( - $"{nameof(featureDefinition)}.{nameof(featureDefinition)} cannot be null.", - nameof(featureDefinition)); - } - - if (featureDefinition.Variants == null) - { - throw new ArgumentException( - $"{nameof(featureDefinition)}.{nameof(featureDefinition.Variants)} cannot be null.", - nameof(featureDefinition)); - } - - FeatureVariant variant = null; - - foreach (User user in featureDefinition.Allocation.User) - { - if (TargetingEvaluator.IsTargeted(targetingContext, user.Users, _options.IgnoreCase)) - { - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(user.Variant)); - - if (!string.IsNullOrEmpty(variant.Name)) - { - return new ValueTask(variant); - } - } - } - - foreach (Group group in featureDefinition.Allocation.Group) - { - if (TargetingEvaluator.IsGroupTargeted(targetingContext, group.Groups, _options.IgnoreCase)) - { - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(group.Variant)); - - if (!string.IsNullOrEmpty(variant.Name)) - { - return new ValueTask(variant); - } - } - } - - foreach (Percentile percentile in featureDefinition.Allocation.Percentile) - { - if (TargetingEvaluator.IsTargeted(targetingContext, percentile.From, percentile.To, featureDefinition.Allocation.Seed, _options.IgnoreCase, featureDefinition.Name)) - { - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(percentile.Variant)); - - if (!string.IsNullOrEmpty(variant.Name)) - { - return new ValueTask(variant); - } - } - } - - return new ValueTask(variant); - } - } -} From baac628e434b8116b81627921beb305888913065 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 1 Aug 2023 14:46:05 -0500 Subject: [PATCH 35/80] add new configurationsection to handle return for variant --- .../FeatureManager.cs | 18 +++--- .../VariantConfigurationSection.cs | 63 +++++++++++++++++++ 2 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/VariantConfigurationSection.cs diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 075733b1..717aac29 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -326,13 +326,17 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex } else if (configValueSet) { - foreach (IConfigurationSection section in _configuration.GetSection($"FeatureManagement:{feature}:Variants").GetChildren()) - { - if (section["Name"] == featureVariant.Name) - { - variantConfiguration = _configuration.GetSection($"{section.Path}:ConfigurationValue"); - } - } + //foreach (IConfigurationSection section in _configuration.GetSection($"FeatureManagement:{feature}:Variants").GetChildren()) + //{ + // if (section["Name"] == featureVariant.Name) + // { + // variantConfiguration = _configuration.GetSection($"{section.Path}:ConfigurationValue"); + // } + //} + + VariantConfigurationSection section = new VariantConfigurationSection(featureVariant.Name, featureVariant.ConfigurationValue); + + variantConfiguration = section; } Variant returnVariant = new Variant() diff --git a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs new file mode 100644 index 00000000..d1812ce5 --- /dev/null +++ b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs @@ -0,0 +1,63 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Primitives; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Microsoft.FeatureManagement +{ + internal class VariantConfigurationSection : IConfigurationSection + { + private readonly string _path; + private string _valueForKey; + private string _key; + + public VariantConfigurationSection(string key, string value) + { + _key = key; + _path = key; + Value = value; + } + + public string this[string key] + { + get + { + if (key == _key) + { + return _valueForKey; + } + return null; + } + set + { + if (key == _key) + { + _valueForKey = value; + } + } + } + + public string Key => _key; + + public string Path => _path; + + public string Value { get; set; } + + public IEnumerable GetChildren() + { + return Enumerable.Empty(); + } + + public IChangeToken GetReloadToken() + { + return new CancellationChangeToken(CancellationToken.None); + } + + public IConfigurationSection GetSection(string key) + { + throw new NotImplementedException(); + } + } +} From 6672af6ee18ee9cf4e8d6a2ed7c14fb13d489f71 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 1 Aug 2023 15:14:19 -0500 Subject: [PATCH 36/80] null error, in progress new configurationsection class --- .../VariantConfigurationSection.cs | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs index d1812ce5..ae0eb8a1 100644 --- a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs +++ b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; using Microsoft.Extensions.Primitives; using System; using System.Collections.Generic; @@ -9,41 +10,48 @@ namespace Microsoft.FeatureManagement { internal class VariantConfigurationSection : IConfigurationSection { + private readonly ConfigurationRoot _root; private readonly string _path; - private string _valueForKey; private string _key; public VariantConfigurationSection(string key, string value) { + MemoryConfigurationSource source = new MemoryConfigurationSource(); + source.InitialData.Append(new KeyValuePair(key, value)); + + _root = new ConfigurationRoot(new List { new MemoryConfigurationProvider(source) }); _key = key; _path = key; Value = value; } - public string this[string key] - { + public string this[string key] + { get { - if (key == _key) - { - return _valueForKey; - } - return null; + return _root[ConfigurationPath.Combine(Path, key)]; } set { - if (key == _key) - { - _valueForKey = value; - } - } + _root[ConfigurationPath.Combine(Path, key)] = value; + } } public string Key => _key; public string Path => _path; - public string Value { get; set; } + public string Value + { + get + { + return _root[Path]; + } + set + { + _root[Path] = value; + } + } public IEnumerable GetChildren() { From 4d6064b85b5b480414172fda2d4959dce42e6f3d Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 1 Aug 2023 18:11:51 -0500 Subject: [PATCH 37/80] fix old bug --- src/Microsoft.FeatureManagement/VariantConfigurationSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs index ae0eb8a1..2fc4588e 100644 --- a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs +++ b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs @@ -17,7 +17,7 @@ internal class VariantConfigurationSection : IConfigurationSection public VariantConfigurationSection(string key, string value) { MemoryConfigurationSource source = new MemoryConfigurationSource(); - source.InitialData.Append(new KeyValuePair(key, value)); + source.InitialData = new List> { new KeyValuePair(key, value) }; _root = new ConfigurationRoot(new List { new MemoryConfigurationProvider(source) }); _key = key; From 6012ec3348f8085c1effb9bc80e3e2fc939b6ac9 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 2 Aug 2023 15:48:28 -0500 Subject: [PATCH 38/80] progress on unit tests --- .../FeatureManager.cs | 105 ++++++------ .../VariantConfigurationSection.cs | 4 +- .../FeatureManagement.cs | 78 ++++++--- .../Tests.FeatureManagement/appsettings.json | 150 ++++++++++++++++++ 4 files changed, 259 insertions(+), 78 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 717aac29..4ca38807 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -48,17 +48,17 @@ public FeatureManager( IFeatureDefinitionProvider featureDefinitionProvider, IEnumerable featureFilters, IEnumerable sessionManagers, - ITargetingContextAccessor contextAccessor, IConfiguration configuration, ILoggerFactory loggerFactory, IOptions options, - IOptions assignerOptions) + IOptions assignerOptions, + ITargetingContextAccessor contextAccessor = null) { _featureDefinitionProvider = featureDefinitionProvider; _featureFilters = featureFilters ?? throw new ArgumentNullException(nameof(featureFilters)); _sessionManagers = sessionManagers ?? throw new ArgumentNullException(nameof(sessionManagers)); _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - _contextAccessor = contextAccessor ?? throw new ArgumentNullException(nameof(contextAccessor)); + _contextAccessor = contextAccessor; _logger = loggerFactory.CreateLogger(); _assignerOptions = assignerOptions?.Value ?? throw new ArgumentNullException(nameof(assignerOptions)); _filterMetadataCache = new ConcurrentDictionary(); @@ -288,7 +288,7 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex { throw new FeatureManagementException( FeatureManagementError.MissingFeature, - $"The feature declaration for the dynamic feature '{feature}' was not found."); + $"The feature declaration for the feature '{feature}' was not found."); } if (featureDefinition.Variants == null) @@ -326,16 +326,7 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex } else if (configValueSet) { - //foreach (IConfigurationSection section in _configuration.GetSection($"FeatureManagement:{feature}:Variants").GetChildren()) - //{ - // if (section["Name"] == featureVariant.Name) - // { - // variantConfiguration = _configuration.GetSection($"{section.Path}:ConfigurationValue"); - // } - //} - VariantConfigurationSection section = new VariantConfigurationSection(featureVariant.Name, featureVariant.ConfigurationValue); - variantConfiguration = section; } @@ -355,25 +346,35 @@ private async ValueTask GetFeatureVariantAsync(FeatureDefinition return ResolveDefaultFeatureVariant(featureDefinition, isFeatureEnabled); } - FeatureVariant featureVariant; + FeatureVariant featureVariant = null; if (!useContext) { - // - // Acquire targeting context via accessor - context = await _contextAccessor.GetContextAsync(cancellationToken).ConfigureAwait(false); - - // - // Ensure targeting can be performed - if (context == null) + if (_contextAccessor == null) + { + _logger.LogWarning($"No instance of {nameof(ITargetingContextAccessor)} is available for targeting evaluation."); + } + else { - _logger.LogWarning("No targeting context available for targeting evaluation."); + // + // Acquire targeting context via accessor + context = await _contextAccessor.GetContextAsync(cancellationToken).ConfigureAwait(false); + + // + // Ensure targeting can be performed + if (context == null) + { + _logger.LogWarning("No targeting context available for targeting evaluation."); - return null; + return null; + } } } - featureVariant = await AssignVariantAsync(featureDefinition, context, cancellationToken).ConfigureAwait(false); + if (context != null) + { + featureVariant = await AssignVariantAsync(featureDefinition, context, cancellationToken).ConfigureAwait(false); + } if (featureVariant == null) { @@ -395,57 +396,57 @@ private ValueTask AssignVariantAsync(FeatureDefinition featureDe throw new ArgumentNullException(nameof(targetingContext)); } - if (featureDefinition == null) - { - throw new ArgumentException( - $"{nameof(featureDefinition)}.{nameof(featureDefinition)} cannot be null.", - nameof(featureDefinition)); - } - if (featureDefinition.Variants == null) { - throw new ArgumentException( - $"{nameof(featureDefinition)}.{nameof(featureDefinition.Variants)} cannot be null.", - nameof(featureDefinition)); + throw new ArgumentNullException(nameof(featureDefinition.Variants)); } FeatureVariant variant = null; - foreach (User user in featureDefinition.Allocation.User) + if (featureDefinition.Allocation.User != null) { - if (TargetingEvaluator.IsTargeted(targetingContext, user.Users, _assignerOptions.IgnoreCase)) + foreach (User user in featureDefinition.Allocation.User) { - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(user.Variant)); - - if (!string.IsNullOrEmpty(variant.Name)) + if (TargetingEvaluator.IsTargeted(targetingContext, user.Users, _assignerOptions.IgnoreCase)) { - return new ValueTask(variant); + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(user.Variant)); + + if (!string.IsNullOrEmpty(variant.Name)) + { + return new ValueTask(variant); + } } } } - foreach (Group group in featureDefinition.Allocation.Group) + if (featureDefinition.Allocation.Group != null) { - if (TargetingEvaluator.IsGroupTargeted(targetingContext, group.Groups, _assignerOptions.IgnoreCase)) + foreach (Group group in featureDefinition.Allocation.Group) { - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(group.Variant)); - - if (!string.IsNullOrEmpty(variant.Name)) + if (TargetingEvaluator.IsGroupTargeted(targetingContext, group.Groups, _assignerOptions.IgnoreCase)) { - return new ValueTask(variant); + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(group.Variant)); + + if (!string.IsNullOrEmpty(variant.Name)) + { + return new ValueTask(variant); + } } } } - foreach (Percentile percentile in featureDefinition.Allocation.Percentile) + if (featureDefinition.Allocation.Percentile != null) { - if (TargetingEvaluator.IsTargeted(targetingContext, percentile.From, percentile.To, featureDefinition.Allocation.Seed, _assignerOptions.IgnoreCase, featureDefinition.Name)) + foreach (Percentile percentile in featureDefinition.Allocation.Percentile) { - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(percentile.Variant)); - - if (!string.IsNullOrEmpty(variant.Name)) + if (TargetingEvaluator.IsTargeted(targetingContext, percentile.From, percentile.To, featureDefinition.Allocation.Seed, _assignerOptions.IgnoreCase, featureDefinition.Name)) { - return new ValueTask(variant); + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(percentile.Variant)); + + if (!string.IsNullOrEmpty(variant.Name)) + { + return new ValueTask(variant); + } } } } diff --git a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs index 2fc4588e..5f62d079 100644 --- a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs +++ b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs @@ -17,11 +17,11 @@ internal class VariantConfigurationSection : IConfigurationSection public VariantConfigurationSection(string key, string value) { MemoryConfigurationSource source = new MemoryConfigurationSource(); - source.InitialData = new List> { new KeyValuePair(key, value) }; + _path = "Root"; + source.InitialData = new List> { new KeyValuePair($"{_path}:{key}", value) }; _root = new ConfigurationRoot(new List { new MemoryConfigurationProvider(source) }); _key = key; - _path = key; Value = value; } diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index a37c08fb..97f761e6 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.FeatureFilters; using System; @@ -966,36 +967,65 @@ public async Task BindsFeatureFlagSettings() [Fact] public async Task UsesVariants() { - FeatureFilterConfiguration testFilterConfiguration = new FeatureFilterConfiguration + IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + + var services = new ServiceCollection(); + + var targetingContextAccessor = new OnDemandTargetingContextAccessor(); + services.AddSingleton(targetingContextAccessor) + .AddSingleton(config) + .AddFeatureManagement() + .AddFeatureFilter(); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + IVariantFeatureManager featureManager = serviceProvider.GetRequiredService(); + + targetingContextAccessor.Current = new TargetingContext { - Name = "Test", - Parameters = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary() - { - { "P1", "V1" }, - }).Build() + UserId = "Marsha", + Groups = new List { "Group1" } }; - var services = new ServiceCollection(); + // Test StatusOverride and Percentile with Seed + Variant variant = await featureManager.GetVariantAsync("VariantFeaturePercentileOn"); - var definitionProvider = new InMemoryFeatureDefinitionProvider( - new FeatureDefinition[] - { - new FeatureDefinition - { - Name = ConditionalFeature, - EnabledFor = new List() - { - testFilterConfiguration - } - } - }); + Assert.Equal("Big", variant.Name); + Assert.Equal("green", variant.Configuration["Color"]); + Assert.False(await featureManager.IsEnabledAsync("VariantFeaturePercentileOn")); - services.AddSingleton(definitionProvider) - .AddSingleton(new ConfigurationBuilder().Build()) - .AddFeatureManagement() - .AddFeatureFilter(); + variant = await featureManager.GetVariantAsync("VariantFeaturePercentileOff"); + + Assert.Null(variant); + Assert.True(await featureManager.IsEnabledAsync("VariantFeaturePercentileOff")); + + // Test Status = Disabled + variant = await featureManager.GetVariantAsync("VariantFeatureStatusDisabled"); + + Assert.Equal("Small", variant.Name); + Assert.Equal("300px", variant.Configuration.Value); + Assert.False(await featureManager.IsEnabledAsync("VariantFeatureStatusDisabled")); + + // Test DefaultWhenEnabled + variant = await featureManager.GetVariantAsync("VariantFeatureDefaultEnabled"); + + Assert.Equal("Medium", variant.Name); + Assert.Equal("450px", variant.Configuration.Value); + Assert.True(await featureManager.IsEnabledAsync("VariantFeatureDefaultEnabled")); + + // Test User allocation + variant = await featureManager.GetVariantAsync("VariantFeatureUser"); + + Assert.Equal("Small", variant.Name); + Assert.Equal("300px", variant.Configuration.Value); + Assert.True(await featureManager.IsEnabledAsync("VariantFeatureUser")); + + // Test Group allocation + variant = await featureManager.GetVariantAsync("VariantFeatureGroup"); - // TODO + Assert.Equal("Small", variant.Name); + Assert.Equal("300px", variant.Configuration.Value); + Assert.True(await featureManager.IsEnabledAsync("VariantFeatureGroup")); } private static void DisableEndpointRouting(MvcOptions options) diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index 2eeff873..572fdb8b 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -6,6 +6,17 @@ }, "AllowedHosts": "*", + "ShoppingCart": { + "Big": { + "Size": 600, + "Color": "green" + }, + "Small": { + "Size": 300, + "Color": "gray" + } + }, + "FeatureManagement": { "OnTestFeature": true, "OffTestFeature": false, @@ -147,6 +158,145 @@ } ] + }, + "VariantFeaturePercentileOn": { + "Allocation": { + "Percentile": [ + { + "Variant": "Big", + "From": 0, + "To": 50 + } + ], + "Seed": 1234 + }, + "Variants": [ + { + "Name": "Big", + "ConfigurationReference": "ShoppingCart:Big", + "StatusOverride": "Disabled" + } + ], + "EnabledFor": [ + { + "Name": "On" + } + ] + }, + "VariantFeaturePercentileOff": { + "Allocation": { + "Percentile": [ + { + "Variant": "Big", + "From": 0, + "To": 50 + } + ], + "Seed": 12345 + }, + "Variants": [ + { + "Name": "Big", + "ConfigurationReference": "ShoppingCart:Big", + "StatusOverride": "Disabled" + } + ], + "EnabledFor": [ + { + "Name": "On" + } + ] + }, + "VariantFeatureStatusDisabled": { + "Status": "Disabled", + "Allocation": { + "DefaultWhenDisabled": "Small" + }, + "Variants": [ + { + "Name": "Small", + "ConfigurationValue": "300px" + } + ], + "EnabledFor": [ + { + "Name": "On" + } + ] + }, + "VariantFeatureDefaultEnabled": { + "Allocation": { + "DefaultWhenEnabled": "Medium", + "User": [ + { + "Variant": "Small", + "Users": [ + "Jeff" + ] + } + ] + }, + "Variants": [ + { + "Name": "Medium", + "ConfigurationValue": "450px" + }, + { + "Name": "Small", + "ConfigurationValue": "300px" + } + ], + "EnabledFor": [ + { + "Name": "On" + } + ] + }, + "VariantFeatureUser": { + "Allocation": { + "User": [ + { + "Variant": "Small", + "Users": [ + "Marsha" + ] + } + ] + }, + "Variants": [ + { + "Name": "Small", + "ConfigurationValue": "300px" + } + ], + "EnabledFor": [ + { + "Name": "On" + } + ] + }, + "VariantFeatureGroup": { + "Allocation": { + "Group": [ + { + "Variant": "Small", + "Groups": [ + "Group1" + ] + } + ] + }, + "Variants": [ + { + "Name": "Small", + "ConfigurationValue": "300px" + } + ], + "EnabledFor": [ + { + "Name": "On" + } + ] } } } From d2543eafea20d8b9aaf8fa1e2503270027da9e68 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 3 Aug 2023 15:13:41 -0500 Subject: [PATCH 39/80] more null check changes, test fixes --- .../FeatureManagementError.cs | 2 +- .../FeatureManager.cs | 67 +++++++++---------- .../FeatureManagement.cs | 6 +- 3 files changed, 36 insertions(+), 39 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManagementError.cs b/src/Microsoft.FeatureManagement/FeatureManagementError.cs index 18006b6b..ebff2a4d 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementError.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementError.cs @@ -39,7 +39,7 @@ public enum FeatureManagementError MissingFeatureVariant, /// - /// A feature variant specifies both a value and reference for its configuration. + /// A feature variant specifies both a configuration reference and configuration value, or an invalid configuration reference. /// InvalidVariantConfiguration } diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 4ca38807..33188fe3 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -48,19 +48,19 @@ public FeatureManager( IFeatureDefinitionProvider featureDefinitionProvider, IEnumerable featureFilters, IEnumerable sessionManagers, - IConfiguration configuration, ILoggerFactory loggerFactory, IOptions options, IOptions assignerOptions, + IConfiguration configuration = null, ITargetingContextAccessor contextAccessor = null) { _featureDefinitionProvider = featureDefinitionProvider; _featureFilters = featureFilters ?? throw new ArgumentNullException(nameof(featureFilters)); _sessionManagers = sessionManagers ?? throw new ArgumentNullException(nameof(sessionManagers)); - _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - _contextAccessor = contextAccessor; _logger = loggerFactory.CreateLogger(); _assignerOptions = assignerOptions?.Value ?? throw new ArgumentNullException(nameof(assignerOptions)); + _contextAccessor = contextAccessor; + _configuration = configuration; _filterMetadataCache = new ConcurrentDictionary(); _contextualFeatureFilterCache = new ConcurrentDictionary(); _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); @@ -218,6 +218,23 @@ await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).Con } } } + + if (!ignoreVariant && (featureDefinition.Variants?.Any() ?? false) && featureDefinition.Status != Status.Disabled) + { + FeatureVariant featureVariant = await GetFeatureVariantAsync(featureDefinition, appContext as TargetingContext, useAppContext, enabled, cancellationToken); + + if (featureVariant != null) + { + if (featureVariant.StatusOverride == StatusOverride.Enabled) + { + enabled = true; + } + else if (featureVariant.StatusOverride == StatusOverride.Disabled) + { + enabled = false; + } + } + } } else { @@ -233,23 +250,6 @@ await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).Con _logger.LogWarning(errorMessage); } - if (!ignoreVariant && (featureDefinition.Variants?.Any() ?? false) && featureDefinition.Status != Status.Disabled) - { - FeatureVariant featureVariant = await GetFeatureVariantAsync(featureDefinition, appContext as TargetingContext, useAppContext, enabled, cancellationToken); - - if (featureVariant != null) - { - if (featureVariant.StatusOverride == StatusOverride.Enabled) - { - enabled = true; - } - else if (featureVariant.StatusOverride == StatusOverride.Disabled) - { - enabled = false; - } - } - } - foreach (ISessionManager sessionManager in _sessionManagers) { await sessionManager.SetAsync(feature, enabled, cancellationToken).ConfigureAwait(false); @@ -291,7 +291,7 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex $"The feature declaration for the feature '{feature}' was not found."); } - if (featureDefinition.Variants == null) + if (!featureDefinition.Variants?.Any() ?? false) { throw new FeatureManagementException( FeatureManagementError.MissingFeatureVariant, @@ -322,6 +322,13 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex } else if (configReferenceValueSet) { + if (_configuration == null) + { + throw new FeatureManagementException( + FeatureManagementError.InvalidVariantConfiguration, + $"Cannot use {nameof(featureVariant.ConfigurationReference)} if no instance of {nameof(IConfiguration)} is present."); + } + variantConfiguration = _configuration.GetSection(featureVariant.ConfigurationReference); } else if (configValueSet) @@ -352,7 +359,7 @@ private async ValueTask GetFeatureVariantAsync(FeatureDefinition { if (_contextAccessor == null) { - _logger.LogWarning($"No instance of {nameof(ITargetingContextAccessor)} is available for targeting evaluation."); + _logger.LogWarning($"No instance of {nameof(ITargetingContextAccessor)} is available for targeting evaluation. Using default variants."); } else { @@ -386,19 +393,11 @@ private async ValueTask GetFeatureVariantAsync(FeatureDefinition private ValueTask AssignVariantAsync(FeatureDefinition featureDefinition, TargetingContext targetingContext, CancellationToken cancellationToken) { - if (featureDefinition == null) + if (!featureDefinition.Variants?.Any() ?? false) { - throw new ArgumentNullException(nameof(featureDefinition)); - } - - if (targetingContext == null) - { - throw new ArgumentNullException(nameof(targetingContext)); - } - - if (featureDefinition.Variants == null) - { - throw new ArgumentNullException(nameof(featureDefinition.Variants)); + throw new FeatureManagementException( + FeatureManagementError.MissingFeatureVariant, + $"No variants are registered for the feature {featureDefinition.Name}"); } FeatureVariant variant = null; diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index 97f761e6..483ea799 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.FeatureFilters; using System; @@ -25,7 +24,7 @@ namespace Tests.FeatureManagement public class FeatureManagement { private const string OnFeature = "OnTestFeature"; - private const string OffFeature = "OffFeature"; + private const string OffFeature = "OffTestFeature"; private const string ConditionalFeature = "ConditionalFeature"; private const string ContextualFeature = "ContextualFeature"; @@ -974,8 +973,7 @@ public async Task UsesVariants() var targetingContextAccessor = new OnDemandTargetingContextAccessor(); services.AddSingleton(targetingContextAccessor) .AddSingleton(config) - .AddFeatureManagement() - .AddFeatureFilter(); + .AddFeatureManagement(); ServiceProvider serviceProvider = services.BuildServiceProvider(); From 8d5920046de36092ab85d4b9f69fcc893e4cd784 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 3 Aug 2023 15:16:47 -0500 Subject: [PATCH 40/80] reset examples changes --- .../Controllers/HomeController.cs | 10 +--- examples/FeatureFlagDemo/MyFeatureFlags.cs | 3 +- examples/FeatureFlagDemo/appsettings.json | 59 ------------------- 3 files changed, 4 insertions(+), 68 deletions(-) diff --git a/examples/FeatureFlagDemo/Controllers/HomeController.cs b/examples/FeatureFlagDemo/Controllers/HomeController.cs index 2e6a1e0b..f2643099 100644 --- a/examples/FeatureFlagDemo/Controllers/HomeController.cs +++ b/examples/FeatureFlagDemo/Controllers/HomeController.cs @@ -14,20 +14,16 @@ namespace FeatureFlagDemo.Controllers { public class HomeController : Controller { - private readonly IVariantFeatureManager _featureManager; + private readonly IFeatureManager _featureManager; - public HomeController(IVariantFeatureManagerSnapshot featureSnapshot) + public HomeController(IFeatureManagerSnapshot featureSnapshot) { _featureManager = featureSnapshot; } [FeatureGate(MyFeatureFlags.Home)] - public async Task Index() + public Task Index() { - Variant test = await _featureManager.GetVariantAsync(nameof(MyFeatureFlags.Banner), CancellationToken.None); - string x = test.Configuration["Size"]; - string y = test.Configuration.Value; - bool isEnabled = await _featureManager.IsEnabledAsync(nameof(MyFeatureFlags.Banner), CancellationToken.None); return View(); } diff --git a/examples/FeatureFlagDemo/MyFeatureFlags.cs b/examples/FeatureFlagDemo/MyFeatureFlags.cs index c648e827..9441c531 100644 --- a/examples/FeatureFlagDemo/MyFeatureFlags.cs +++ b/examples/FeatureFlagDemo/MyFeatureFlags.cs @@ -11,7 +11,6 @@ public enum MyFeatureFlags Beta, CustomViewData, ContentEnhancement, - EnhancedPipeline, - Banner + EnhancedPipeline } } diff --git a/examples/FeatureFlagDemo/appsettings.json b/examples/FeatureFlagDemo/appsettings.json index 09a2bee1..73c707a8 100644 --- a/examples/FeatureFlagDemo/appsettings.json +++ b/examples/FeatureFlagDemo/appsettings.json @@ -66,65 +66,6 @@ } } ] - }, - "Banner": { - "Allocation": { - "DefaultWhenEnabled": "Small", - "DefaultWhenDisabled": "Small", - "User": [ - { - "Variant": "Big", - "Users": [ - "Marsha" - ] - } - ], - "Group": [ - { - "Variant": "Big", - "Groups": [ - "Ring1" - ] - } - ], - "Percentile": [ - { - "Variant": "Big", - "From": 0, - "To": 0 - } - ], - "Seed": 13973240 - }, - "Variants": [ - { - "Name": "Big", - "ConfigurationReference": "ShoppingCart:Big", - "StatusOverride": "Disabled" - }, - { - "Name": "Small", - "ConfigurationValue": "300px" - } - ], - "EnabledFor": [ - { - "Name": "Microsoft.Percentage", - "Parameters": { - "Value": 100 - } - } - ] - } - }, - "ShoppingCart": { - "Big": { - "Size": 400, - "Color": "green" - }, - "Small": { - "Size": 150, - "Color": "gray" } } } From 77fc24bba9ba04bde268519e2612e60403c2062d Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 3 Aug 2023 16:12:41 -0500 Subject: [PATCH 41/80] Revert "Revert "Revert "Added default value for cancellation token in interfaces to keep existing usage possible. (#133)" (#138)"" This reverts commit d087e7bb1bc50028cf9c7835930d50bb19fffc75. --- examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs | 2 +- examples/FeatureFlagDemo/BrowserFilter.cs | 2 +- .../HttpContextTargetingContextAccessor.cs | 2 +- examples/FeatureFlagDemo/SuperUserFilter.cs | 2 +- .../ConfigurationFeatureDefinitionProvider.cs | 4 ++-- src/Microsoft.FeatureManagement/EmptySessionManager.cs | 4 ++-- .../IContextualFeatureFilter.cs | 2 +- .../IFeatureDefinitionProvider.cs | 4 ++-- src/Microsoft.FeatureManagement/IFeatureFilter.cs | 2 +- src/Microsoft.FeatureManagement/IFeatureManager.cs | 6 +++--- src/Microsoft.FeatureManagement/ISessionManager.cs | 4 ++-- .../Targeting/ITargetingContextAccessor.cs | 2 +- tests/Tests.FeatureManagement/ContextualTestFilter.cs | 2 +- .../InMemoryFeatureDefinitionProvider.cs | 4 ++-- tests/Tests.FeatureManagement/InvalidFeatureFilter.cs | 8 ++++---- .../OnDemandTargetingContextAccessor.cs | 2 +- tests/Tests.FeatureManagement/TestFilter.cs | 2 +- 17 files changed, 27 insertions(+), 27 deletions(-) diff --git a/examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs b/examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs index 193e87d4..3d236eed 100644 --- a/examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs +++ b/examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs @@ -18,7 +18,7 @@ namespace Consoto.Banking.AccountService.FeatureManagement [FilterAlias("AccountId")] class AccountIdFilter : IContextualFeatureFilter { - public Task EvaluateAsync(FeatureFilterEvaluationContext featureEvaluationContext, IAccountContext accountContext, CancellationToken cancellationToken) + public Task EvaluateAsync(FeatureFilterEvaluationContext featureEvaluationContext, IAccountContext accountContext, CancellationToken _) { if (string.IsNullOrEmpty(accountContext?.AccountId)) { diff --git a/examples/FeatureFlagDemo/BrowserFilter.cs b/examples/FeatureFlagDemo/BrowserFilter.cs index cd701078..e33f58c7 100644 --- a/examples/FeatureFlagDemo/BrowserFilter.cs +++ b/examples/FeatureFlagDemo/BrowserFilter.cs @@ -24,7 +24,7 @@ public BrowserFilter(IHttpContextAccessor httpContextAccessor) _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } - public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) { BrowserFilterSettings settings = context.Parameters.Get() ?? new BrowserFilterSettings(); diff --git a/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs b/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs index 097605b6..931be833 100644 --- a/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs +++ b/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs @@ -24,7 +24,7 @@ public HttpContextTargetingContextAccessor(IHttpContextAccessor httpContextAcces _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } - public ValueTask GetContextAsync(CancellationToken cancellationToken) + public ValueTask GetContextAsync(CancellationToken _) { HttpContext httpContext = _httpContextAccessor.HttpContext; diff --git a/examples/FeatureFlagDemo/SuperUserFilter.cs b/examples/FeatureFlagDemo/SuperUserFilter.cs index 48ab33ec..1e188704 100644 --- a/examples/FeatureFlagDemo/SuperUserFilter.cs +++ b/examples/FeatureFlagDemo/SuperUserFilter.cs @@ -9,7 +9,7 @@ namespace FeatureFlagDemo.FeatureManagement.FeatureFilters { public class SuperUserFilter : IFeatureFilter { - public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) { return Task.FromResult(false); } diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 97af20dc..f51c301c 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -45,7 +45,7 @@ public void Dispose() _changeSubscription = null; } - public Task GetFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken) + public Task GetFeatureDefinitionAsync(string featureName, CancellationToken _) { if (featureName == null) { @@ -68,7 +68,7 @@ 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 GetAllFeatureDefinitionsAsync([EnumeratorCancellation] CancellationToken _) #pragma warning restore CS1998 { if (Interlocked.Exchange(ref _stale, 0) != 0) diff --git a/src/Microsoft.FeatureManagement/EmptySessionManager.cs b/src/Microsoft.FeatureManagement/EmptySessionManager.cs index 7658a0e4..7f09b910 100644 --- a/src/Microsoft.FeatureManagement/EmptySessionManager.cs +++ b/src/Microsoft.FeatureManagement/EmptySessionManager.cs @@ -11,12 +11,12 @@ namespace Microsoft.FeatureManagement /// class EmptySessionManager : ISessionManager { - public Task SetAsync(string featureName, bool enabled, CancellationToken cancellationToken) + public Task SetAsync(string featureName, bool enabled, CancellationToken _) { return Task.CompletedTask; } - public Task GetAsync(string featureName, CancellationToken cancellationToken) + public Task GetAsync(string featureName, CancellationToken _) { return Task.FromResult((bool?)null); } diff --git a/src/Microsoft.FeatureManagement/IContextualFeatureFilter.cs b/src/Microsoft.FeatureManagement/IContextualFeatureFilter.cs index 5df783d0..84deb352 100644 --- a/src/Microsoft.FeatureManagement/IContextualFeatureFilter.cs +++ b/src/Microsoft.FeatureManagement/IContextualFeatureFilter.cs @@ -21,6 +21,6 @@ public interface IContextualFeatureFilter : IFeatureFilterMetadata /// A context defined by the application that is passed in to the feature management system to provide contextual information for evaluating a feature's state. /// The cancellation token to cancel the operation. /// True if the filter's criteria has been met, false otherwise. - Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, TContext appContext, CancellationToken cancellationToken = default); + Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, TContext appContext, CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs index c8c37494..3b5adc15 100644 --- a/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs @@ -18,13 +18,13 @@ public interface IFeatureDefinitionProvider /// 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); + Task GetFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken); /// /// 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); + IAsyncEnumerable GetAllFeatureDefinitionsAsync(CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureFilter.cs b/src/Microsoft.FeatureManagement/IFeatureFilter.cs index 416772db..865a40dc 100644 --- a/src/Microsoft.FeatureManagement/IFeatureFilter.cs +++ b/src/Microsoft.FeatureManagement/IFeatureFilter.cs @@ -17,6 +17,6 @@ public interface IFeatureFilter : IFeatureFilterMetadata /// A feature filter evaluation context that contains information that may be needed to evaluate the filter. This context includes configuration, if any, for this filter for the feature being evaluated. /// The cancellation token to cancel the operation. /// True if the filter's criteria has been met, false otherwise. - Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken = default); + Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureManager.cs b/src/Microsoft.FeatureManagement/IFeatureManager.cs index 53231f89..d9b07e6a 100644 --- a/src/Microsoft.FeatureManagement/IFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IFeatureManager.cs @@ -17,7 +17,7 @@ public interface IFeatureManager /// /// 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); + IAsyncEnumerable GetFeatureNamesAsync(CancellationToken cancellationToken); /// /// Checks whether a given feature is enabled. @@ -25,7 +25,7 @@ public interface IFeatureManager /// The name of the feature to check. /// The cancellation token to cancel the operation. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature, CancellationToken cancellationToken = default); + Task IsEnabledAsync(string feature, CancellationToken cancellationToken); /// /// Checks whether a given feature is enabled. @@ -34,6 +34,6 @@ public interface IFeatureManager /// 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. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken = default); + Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/ISessionManager.cs b/src/Microsoft.FeatureManagement/ISessionManager.cs index f6327932..1c0656f1 100644 --- a/src/Microsoft.FeatureManagement/ISessionManager.cs +++ b/src/Microsoft.FeatureManagement/ISessionManager.cs @@ -17,7 +17,7 @@ public interface ISessionManager /// The name of the feature. /// The state of the feature. /// The cancellation token to cancel the operation. - Task SetAsync(string featureName, bool enabled, CancellationToken cancellationToken = default); + Task SetAsync(string featureName, bool enabled, CancellationToken cancellationToken); /// /// Queries the session manager for the session's feature state, if any, for the given feature. @@ -25,6 +25,6 @@ public interface ISessionManager /// The name of the feature. /// The cancellation token to cancel the operation. /// The state of the feature if it is present in the session, otherwise null. - Task GetAsync(string featureName, CancellationToken cancellationToken = default); + Task GetAsync(string featureName, CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/Targeting/ITargetingContextAccessor.cs b/src/Microsoft.FeatureManagement/Targeting/ITargetingContextAccessor.cs index 04e5f709..2739e628 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ITargetingContextAccessor.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ITargetingContextAccessor.cs @@ -16,6 +16,6 @@ public interface ITargetingContextAccessor /// /// The cancellation token to cancel the operation. /// The current targeting context. - ValueTask GetContextAsync(CancellationToken cancellationToken = default); + ValueTask GetContextAsync(CancellationToken cancellationToken); } } diff --git a/tests/Tests.FeatureManagement/ContextualTestFilter.cs b/tests/Tests.FeatureManagement/ContextualTestFilter.cs index 87241891..91bd6222 100644 --- a/tests/Tests.FeatureManagement/ContextualTestFilter.cs +++ b/tests/Tests.FeatureManagement/ContextualTestFilter.cs @@ -12,7 +12,7 @@ class ContextualTestFilter : IContextualFeatureFilter { public Func ContextualCallback { get; set; } - public Task EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext, CancellationToken cancellationToken) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext, CancellationToken _) { return Task.FromResult(ContextualCallback?.Invoke(context, accountContext) ?? false); } diff --git a/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs b/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs index ae02858b..76d64a10 100644 --- a/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs +++ b/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs @@ -18,7 +18,7 @@ public InMemoryFeatureDefinitionProvider(IEnumerable featureD } #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - public async IAsyncEnumerable GetAllFeatureDefinitionsAsync([EnumeratorCancellation] CancellationToken cancellationToken) + public async IAsyncEnumerable GetAllFeatureDefinitionsAsync([EnumeratorCancellation] CancellationToken _) #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { foreach (FeatureDefinition definition in _definitions) @@ -27,7 +27,7 @@ public async IAsyncEnumerable GetAllFeatureDefinitionsAsync([ } } - public Task GetFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken) + public Task GetFeatureDefinitionAsync(string featureName, CancellationToken _) { return Task.FromResult(_definitions.FirstOrDefault(definitions => definitions.Name.Equals(featureName, StringComparison.OrdinalIgnoreCase))); } diff --git a/tests/Tests.FeatureManagement/InvalidFeatureFilter.cs b/tests/Tests.FeatureManagement/InvalidFeatureFilter.cs index cc42aaef..4f81e3e2 100644 --- a/tests/Tests.FeatureManagement/InvalidFeatureFilter.cs +++ b/tests/Tests.FeatureManagement/InvalidFeatureFilter.cs @@ -11,12 +11,12 @@ namespace Tests.FeatureManagement // Cannot implement more than one IFeatureFilter interface class InvalidFeatureFilter : IContextualFeatureFilter, IContextualFeatureFilter { - public Task EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext, CancellationToken cancellationToken) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext, CancellationToken _) { return Task.FromResult(false); } - public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, object appContext, CancellationToken cancellationToken) + public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, object appContext, CancellationToken _) { return Task.FromResult(false); } @@ -26,12 +26,12 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterCont // Cannot implement more than one IFeatureFilter interface class InvalidFeatureFilter2 : IFeatureFilter, IContextualFeatureFilter { - public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, object appContext, CancellationToken cancellationToken) + public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, object appContext, CancellationToken _) { return Task.FromResult(false); } - public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) { return Task.FromResult(false); } diff --git a/tests/Tests.FeatureManagement/OnDemandTargetingContextAccessor.cs b/tests/Tests.FeatureManagement/OnDemandTargetingContextAccessor.cs index 5ba376fb..aaff4a41 100644 --- a/tests/Tests.FeatureManagement/OnDemandTargetingContextAccessor.cs +++ b/tests/Tests.FeatureManagement/OnDemandTargetingContextAccessor.cs @@ -11,7 +11,7 @@ class OnDemandTargetingContextAccessor : ITargetingContextAccessor { public TargetingContext Current { get; set; } - public ValueTask GetContextAsync(CancellationToken cancellationToken) + public ValueTask GetContextAsync(CancellationToken _) { return new ValueTask(Current); } diff --git a/tests/Tests.FeatureManagement/TestFilter.cs b/tests/Tests.FeatureManagement/TestFilter.cs index 6a4c5c01..b2de169d 100644 --- a/tests/Tests.FeatureManagement/TestFilter.cs +++ b/tests/Tests.FeatureManagement/TestFilter.cs @@ -25,7 +25,7 @@ public object BindParameters(IConfiguration parameters) return parameters; } - public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) { return Callback?.Invoke(context) ?? Task.FromResult(false); } From a286d430b35d8c8a7be3b198722834cc0cd72ade Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 3 Aug 2023 16:21:10 -0500 Subject: [PATCH 42/80] Revert "Revert "Revert "Add cancellation token parameter to async feature management interfaces. (#131)" (#139)"" This reverts commit c1451d30fed09ad8e4c33e88ab5f0d7792940f6c. --- .../FeatureFilters/AccountIdFilter.cs | 3 +- examples/ConsoleApp/Program.cs | 3 +- examples/FeatureFlagDemo/BrowserFilter.cs | 3 +- .../Controllers/HomeController.cs | 7 ++- .../HttpContextTargetingContextAccessor.cs | 3 +- examples/FeatureFlagDemo/SuperUserFilter.cs | 3 +- examples/TargetingConsoleApp/Program.cs | 3 +- .../FeatureGateAttribute.cs | 4 +- .../FeatureGatedAsyncActionFilter.cs | 2 +- .../TagHelpers/FeatureTagHelper.cs | 5 +-- .../UseForFeatureExtensions.cs | 2 +- .../ConfigurationFeatureDefinitionProvider.cs | 5 +-- .../ContextualFeatureFilterEvaluator.cs | 28 +++++------- .../EmptySessionManager.cs | 5 +-- .../FeatureFilters/PercentageFilter.cs | 4 +- .../FeatureFilters/TimeWindowFilter.cs | 4 +- .../FeatureManager.cs | 41 ++++++++--------- .../FeatureManagerSnapshot.cs | 13 +++--- .../IContextualFeatureFilter.cs | 4 +- .../IFeatureDefinitionProvider.cs | 7 +-- .../IFeatureFilter.cs | 4 +- .../IFeatureManager.cs | 9 ++-- .../ISessionManager.cs | 7 +-- .../IVariantFeatureManager.cs | 6 +-- .../Targeting/ContextualTargetingFilter.cs | 4 +- .../Targeting/ITargetingContextAccessor.cs | 4 +- .../Targeting/TargetingFilter.cs | 8 ++-- .../ContextualTestFilter.cs | 3 +- .../CustomTargetingFilter.cs | 4 +- .../FeatureManagement.cs | 44 +++++++++---------- .../InMemoryFeatureDefinitionProvider.cs | 6 +-- .../InvalidFeatureFilter.cs | 9 ++-- .../OnDemandTargetingContextAccessor.cs | 3 +- tests/Tests.FeatureManagement/TestFilter.cs | 3 +- 34 files changed, 103 insertions(+), 160 deletions(-) diff --git a/examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs b/examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs index 3d236eed..1fd56d4a 100644 --- a/examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs +++ b/examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs @@ -6,7 +6,6 @@ using Microsoft.FeatureManagement; using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; namespace Consoto.Banking.AccountService.FeatureManagement @@ -18,7 +17,7 @@ namespace Consoto.Banking.AccountService.FeatureManagement [FilterAlias("AccountId")] class AccountIdFilter : IContextualFeatureFilter { - public Task EvaluateAsync(FeatureFilterEvaluationContext featureEvaluationContext, IAccountContext accountContext, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext featureEvaluationContext, IAccountContext accountContext) { if (string.IsNullOrEmpty(accountContext?.AccountId)) { diff --git a/examples/ConsoleApp/Program.cs b/examples/ConsoleApp/Program.cs index afe4c52f..e0ab1021 100644 --- a/examples/ConsoleApp/Program.cs +++ b/examples/ConsoleApp/Program.cs @@ -8,7 +8,6 @@ using Microsoft.FeatureManagement.FeatureFilters; using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; namespace Consoto.Banking.AccountService @@ -59,7 +58,7 @@ public static async Task Main(string[] args) AccountId = account }; - bool enabled = await featureManager.IsEnabledAsync(FeatureName, accountServiceContext, CancellationToken.None); + bool enabled = await featureManager.IsEnabledAsync(FeatureName, accountServiceContext); // // Output results diff --git a/examples/FeatureFlagDemo/BrowserFilter.cs b/examples/FeatureFlagDemo/BrowserFilter.cs index e33f58c7..efeb8e70 100644 --- a/examples/FeatureFlagDemo/BrowserFilter.cs +++ b/examples/FeatureFlagDemo/BrowserFilter.cs @@ -6,7 +6,6 @@ using Microsoft.FeatureManagement; using System; using System.Linq; -using System.Threading; using System.Threading.Tasks; namespace FeatureFlagDemo.FeatureManagement.FeatureFilters @@ -24,7 +23,7 @@ public BrowserFilter(IHttpContextAccessor httpContextAccessor) _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } - public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext context) { BrowserFilterSettings settings = context.Parameters.Get() ?? new BrowserFilterSettings(); diff --git a/examples/FeatureFlagDemo/Controllers/HomeController.cs b/examples/FeatureFlagDemo/Controllers/HomeController.cs index f2643099..c4363967 100644 --- a/examples/FeatureFlagDemo/Controllers/HomeController.cs +++ b/examples/FeatureFlagDemo/Controllers/HomeController.cs @@ -8,7 +8,6 @@ using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.Mvc; using System.Threading.Tasks; -using System.Threading; namespace FeatureFlagDemo.Controllers { @@ -22,16 +21,16 @@ public HomeController(IFeatureManagerSnapshot featureSnapshot) } [FeatureGate(MyFeatureFlags.Home)] - public Task Index() + public IActionResult Index() { return View(); } - public async Task About(CancellationToken cancellationToken) + public async Task About() { ViewData["Message"] = "Your application description page."; - if (await _featureManager.IsEnabledAsync(nameof(MyFeatureFlags.CustomViewData), cancellationToken)) + if (await _featureManager.IsEnabledAsync(nameof(MyFeatureFlags.CustomViewData))) { ViewData["Message"] = $"This is FANCY CONTENT you can see only if '{nameof(MyFeatureFlags.CustomViewData)}' is enabled."; }; diff --git a/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs b/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs index 931be833..9f9c8964 100644 --- a/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs +++ b/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Security.Claims; -using System.Threading; using System.Threading.Tasks; namespace FeatureFlagDemo @@ -24,7 +23,7 @@ public HttpContextTargetingContextAccessor(IHttpContextAccessor httpContextAcces _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } - public ValueTask GetContextAsync(CancellationToken _) + public ValueTask GetContextAsync() { HttpContext httpContext = _httpContextAccessor.HttpContext; diff --git a/examples/FeatureFlagDemo/SuperUserFilter.cs b/examples/FeatureFlagDemo/SuperUserFilter.cs index 1e188704..25dc8e5f 100644 --- a/examples/FeatureFlagDemo/SuperUserFilter.cs +++ b/examples/FeatureFlagDemo/SuperUserFilter.cs @@ -2,14 +2,13 @@ // Licensed under the MIT license. // using Microsoft.FeatureManagement; -using System.Threading; using System.Threading.Tasks; namespace FeatureFlagDemo.FeatureManagement.FeatureFilters { public class SuperUserFilter : IFeatureFilter { - public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext context) { return Task.FromResult(false); } diff --git a/examples/TargetingConsoleApp/Program.cs b/examples/TargetingConsoleApp/Program.cs index 15e35eb7..ad115215 100644 --- a/examples/TargetingConsoleApp/Program.cs +++ b/examples/TargetingConsoleApp/Program.cs @@ -9,7 +9,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; namespace Consoto.Banking.HelpDesk @@ -63,7 +62,7 @@ public static async Task Main(string[] args) Groups = user.Groups }; - bool enabled = await featureManager.IsEnabledAsync(FeatureName, targetingContext, CancellationToken.None); + bool enabled = await featureManager.IsEnabledAsync(FeatureName, targetingContext); // // Output results diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs index 835cc1bd..c2573b8c 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs @@ -107,8 +107,8 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context // // Enabled state is determined by either 'any' or 'all' features being enabled. bool enabled = RequirementType == RequirementType.All ? - await Features.All(async feature => await fm.IsEnabledAsync(feature, context.HttpContext.RequestAborted).ConfigureAwait(false)).ConfigureAwait(false) : - await Features.Any(async feature => await fm.IsEnabledAsync(feature, context.HttpContext.RequestAborted).ConfigureAwait(false)).ConfigureAwait(false); + await Features.All(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false)) : + await Features.Any(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false)); if (enabled) { diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs index 80a68a7e..594620ae 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs @@ -30,7 +30,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE { IFeatureManager featureManager = context.HttpContext.RequestServices.GetRequiredService(); - if (await featureManager.IsEnabledAsync(FeatureName, context.HttpContext.RequestAborted).ConfigureAwait(false)) + if (await featureManager.IsEnabledAsync(FeatureName).ConfigureAwait(false)) { IServiceProvider serviceProvider = context.HttpContext.RequestServices.GetRequiredService(); diff --git a/src/Microsoft.FeatureManagement.AspNetCore/TagHelpers/FeatureTagHelper.cs b/src/Microsoft.FeatureManagement.AspNetCore/TagHelpers/FeatureTagHelper.cs index 651c0068..4a0da1e0 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/TagHelpers/FeatureTagHelper.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/TagHelpers/FeatureTagHelper.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Razor.TagHelpers; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.Mvc.TagHelpers @@ -56,8 +55,8 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu IEnumerable names = Name.Split(',').Select(n => n.Trim()); enabled = Requirement == RequirementType.All ? - await names.All(async n => await _featureManager.IsEnabledAsync(n, CancellationToken.None).ConfigureAwait(false)).ConfigureAwait(false) : - await names.Any(async n => await _featureManager.IsEnabledAsync(n, CancellationToken.None).ConfigureAwait(false)).ConfigureAwait(false); + await names.All(async n => await _featureManager.IsEnabledAsync(n).ConfigureAwait(false)) : + await names.Any(async n => await _featureManager.IsEnabledAsync(n).ConfigureAwait(false)); } if (Negate) diff --git a/src/Microsoft.FeatureManagement.AspNetCore/UseForFeatureExtensions.cs b/src/Microsoft.FeatureManagement.AspNetCore/UseForFeatureExtensions.cs index c8562b7a..4774ee9b 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/UseForFeatureExtensions.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/UseForFeatureExtensions.cs @@ -57,7 +57,7 @@ public static IApplicationBuilder UseForFeature(this IApplicationBuilder app, st { IFeatureManager fm = context.RequestServices.GetRequiredService(); - if (await fm.IsEnabledAsync(featureName, context.RequestAborted).ConfigureAwait(false)) + if (await fm.IsEnabledAsync(featureName).ConfigureAwait(false)) { await branch(context).ConfigureAwait(false); } diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index f51c301c..7697dd05 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -7,7 +7,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -45,7 +44,7 @@ public void Dispose() _changeSubscription = null; } - public Task GetFeatureDefinitionAsync(string featureName, CancellationToken _) + public Task GetFeatureDefinitionAsync(string featureName) { if (featureName == null) { @@ -68,7 +67,7 @@ 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 _) + public async IAsyncEnumerable GetAllFeatureDefinitionsAsync() #pragma warning restore CS1998 { if (Interlocked.Exchange(ref _stale, 0) != 0) diff --git a/src/Microsoft.FeatureManagement/ContextualFeatureFilterEvaluator.cs b/src/Microsoft.FeatureManagement/ContextualFeatureFilterEvaluator.cs index b2e68082..baf9220a 100644 --- a/src/Microsoft.FeatureManagement/ContextualFeatureFilterEvaluator.cs +++ b/src/Microsoft.FeatureManagement/ContextualFeatureFilterEvaluator.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement @@ -16,7 +15,7 @@ namespace Microsoft.FeatureManagement class ContextualFeatureFilterEvaluator : IContextualFeatureFilter { private IFeatureFilterMetadata _filter; - private Func> _evaluateFunc; + private Func> _evaluateFunc; public ContextualFeatureFilterEvaluator(IFeatureFilterMetadata filter, Type appContextType) { @@ -44,14 +43,14 @@ public ContextualFeatureFilterEvaluator(IFeatureFilterMetadata filter, Type appC _filter = filter; } - public Task EvaluateAsync(FeatureFilterEvaluationContext evaluationContext, object context, CancellationToken cancellationToken) + public Task EvaluateAsync(FeatureFilterEvaluationContext evaluationContext, object context) { if (_evaluateFunc == null) { return Task.FromResult(false); } - return _evaluateFunc(_filter, evaluationContext, context, cancellationToken); + return _evaluateFunc(_filter, evaluationContext, context); } public static bool IsContextualFilter(IFeatureFilterMetadata filter, Type appContextType) @@ -73,7 +72,7 @@ private static Type GetContextualFilterInterface(IFeatureFilterMetadata filter, return targetInterface; } - private static Func> TypeAgnosticEvaluate(Type filterType, MethodInfo method) + private static Func> TypeAgnosticEvaluate(Type filterType, MethodInfo method) { // // Get the generic version of the evaluation helper method @@ -83,28 +82,21 @@ private static Func>)typeAgnosticDelegate; + return (Func>)typeAgnosticDelegate; } - private static Func> GenericTypeAgnosticEvaluate(MethodInfo method) + private static Func> GenericTypeAgnosticEvaluate(MethodInfo method) { - Func> func = - (Func>) - Delegate.CreateDelegate(typeof(Func>), method); + Func> func = (Func>)Delegate.CreateDelegate + (typeof(Func>), method); - Func> genericDelegate = - (object target, FeatureFilterEvaluationContext param1, object param2, CancellationToken param3) => - func((TTarget)target, param1, (TParam2)param2, param3); + Func> genericDelegate = (object target, FeatureFilterEvaluationContext param1, object param2) => func((TTarget)target, param1, (TParam2)param2); return genericDelegate; } diff --git a/src/Microsoft.FeatureManagement/EmptySessionManager.cs b/src/Microsoft.FeatureManagement/EmptySessionManager.cs index 7f09b910..fd4b3d3f 100644 --- a/src/Microsoft.FeatureManagement/EmptySessionManager.cs +++ b/src/Microsoft.FeatureManagement/EmptySessionManager.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement @@ -11,12 +10,12 @@ namespace Microsoft.FeatureManagement /// class EmptySessionManager : ISessionManager { - public Task SetAsync(string featureName, bool enabled, CancellationToken _) + public Task SetAsync(string featureName, bool enabled) { return Task.CompletedTask; } - public Task GetAsync(string featureName, CancellationToken _) + public Task GetAsync(string featureName) { return Task.FromResult((bool?)null); } diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs index 1f672861..c7ea8840 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.FeatureManagement.Utils; -using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters @@ -41,9 +40,8 @@ public object BindParameters(IConfiguration filterParameters) /// Performs a percentage based evaluation to determine whether a feature is enabled. /// /// The feature evaluation context. - /// The cancellation token to cancel the operation. /// True if the feature is enabled, false otherwise. - public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken) + public Task EvaluateAsync(FeatureFilterEvaluationContext context) { // // Check if prebound settings available, otherwise bind from parameters. diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index 48ac7b1c..884332b3 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System; -using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters @@ -41,9 +40,8 @@ public object BindParameters(IConfiguration filterParameters) /// Evaluates whether a feature is enabled based on a configurable time window. /// /// The feature evaluation context. - /// The cancellation token to cancel the operation. /// True if the feature is enabled, false otherwise. - public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken) + public Task EvaluateAsync(FeatureFilterEvaluationContext context) { // // Check if prebound settings available, otherwise bind from parameters. diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 33188fe3..748375fb 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -11,7 +11,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -67,26 +66,24 @@ public FeatureManager( _parametersCache = new MemoryCache(new MemoryCacheOptions()); } - public Task IsEnabledAsync(string feature, CancellationToken cancellationToken) + public Task IsEnabledAsync(string feature) { - return IsEnabledAsync(feature, null, false, false, cancellationToken); + return IsEnabledAsync(feature, null, false, false); } - public Task IsEnabledAsync(string feature, TContext appContext, CancellationToken cancellationToken) + public Task IsEnabledAsync(string feature, TContext appContext) { - return IsEnabledAsync(feature, appContext, true, false, cancellationToken); + return IsEnabledAsync(feature, appContext, true, false); } - public Task IsEnabledAsync(string feature, TContext appContext, bool ignoreVariant, CancellationToken cancellationToken) + public Task IsEnabledAsync(string feature, TContext appContext, bool ignoreVariant) { - return IsEnabledAsync(feature, appContext, true, ignoreVariant, cancellationToken); + return IsEnabledAsync(feature, appContext, true, ignoreVariant); } - public async IAsyncEnumerable GetFeatureNamesAsync([EnumeratorCancellation] CancellationToken cancellationToken) + public async IAsyncEnumerable GetFeatureNamesAsync() { - await foreach (FeatureDefinition featureDefintion in _featureDefinitionProvider - .GetAllFeatureDefinitionsAsync(cancellationToken) - .ConfigureAwait(false)) + await foreach (FeatureDefinition featureDefintion in _featureDefinitionProvider.GetAllFeatureDefinitionsAsync().ConfigureAwait(false)) { yield return featureDefintion.Name; } @@ -97,11 +94,11 @@ public void Dispose() _parametersCache.Dispose(); } - private async Task IsEnabledAsync(string feature, TContext appContext, bool useAppContext, bool ignoreVariant, CancellationToken cancellationToken) + private async Task IsEnabledAsync(string feature, TContext appContext, bool useAppContext, bool ignoreVariant) { foreach (ISessionManager sessionManager in _sessionManagers) { - bool? readSessionResult = await sessionManager.GetAsync(feature, cancellationToken).ConfigureAwait(false); + bool? readSessionResult = await sessionManager.GetAsync(feature).ConfigureAwait(false); if (readSessionResult.HasValue) { @@ -111,9 +108,7 @@ private async Task IsEnabledAsync(string feature, TContext appCo bool enabled; - FeatureDefinition featureDefinition = await _featureDefinitionProvider - .GetFeatureDefinitionAsync(feature, cancellationToken) - .ConfigureAwait(false); + FeatureDefinition featureDefinition = await _featureDefinitionProvider.GetFeatureDefinitionAsync(feature).ConfigureAwait(false); if (featureDefinition != null) { @@ -195,7 +190,7 @@ private async Task IsEnabledAsync(string feature, TContext appCo BindSettings(filter, context, filterIndex); if (contextualFilter != null && - await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).ConfigureAwait(false) == targetEvaluation) + await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) == targetEvaluation) { enabled = targetEvaluation; @@ -209,7 +204,7 @@ await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).Con { BindSettings(filter, context, filterIndex); - if (await featureFilter.EvaluateAsync(context, cancellationToken).ConfigureAwait(false) == targetEvaluation) + if (await featureFilter.EvaluateAsync(context).ConfigureAwait(false) == targetEvaluation) { enabled = targetEvaluation; @@ -221,7 +216,7 @@ await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).Con if (!ignoreVariant && (featureDefinition.Variants?.Any() ?? false) && featureDefinition.Status != Status.Disabled) { - FeatureVariant featureVariant = await GetFeatureVariantAsync(featureDefinition, appContext as TargetingContext, useAppContext, enabled, cancellationToken); + FeatureVariant featureVariant = await GetFeatureVariantAsync(featureDefinition, appContext as TargetingContext, useAppContext, enabled, CancellationToken.None); if (featureVariant != null) { @@ -252,7 +247,7 @@ await contextualFilter.EvaluateAsync(context, appContext, cancellationToken).Con foreach (ISessionManager sessionManager in _sessionManagers) { - await sessionManager.SetAsync(feature, enabled, cancellationToken).ConfigureAwait(false); + await sessionManager.SetAsync(feature, enabled).ConfigureAwait(false); } return enabled; @@ -281,7 +276,7 @@ public ValueTask GetVariantAsync(string feature, TargetingContext conte private async ValueTask GetVariantAsync(string feature, TargetingContext context, bool useContext, CancellationToken cancellationToken) { FeatureDefinition featureDefinition = await _featureDefinitionProvider - .GetFeatureDefinitionAsync(feature, cancellationToken) + .GetFeatureDefinitionAsync(feature) .ConfigureAwait(false); if (featureDefinition == null) @@ -300,7 +295,7 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex FeatureVariant featureVariant; - bool isFeatureEnabled = await IsEnabledAsync(feature, context, true, cancellationToken).ConfigureAwait(false); + bool isFeatureEnabled = await IsEnabledAsync(feature, context, true).ConfigureAwait(false); featureVariant = await GetFeatureVariantAsync(featureDefinition, context, useContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); @@ -365,7 +360,7 @@ private async ValueTask GetFeatureVariantAsync(FeatureDefinition { // // Acquire targeting context via accessor - context = await _contextAccessor.GetContextAsync(cancellationToken).ConfigureAwait(false); + context = await _contextAccessor.GetContextAsync().ConfigureAwait(false); // // Ensure targeting can be performed diff --git a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs index 6934a83b..1f8d7516 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -26,13 +25,13 @@ public FeatureManagerSnapshot(FeatureManager featureManager) _featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager)); } - public async IAsyncEnumerable GetFeatureNamesAsync([EnumeratorCancellation]CancellationToken cancellationToken) + public async IAsyncEnumerable GetFeatureNamesAsync() { if (_featureNames == null) { var featureNames = new List(); - await foreach (string featureName in _featureManager.GetFeatureNamesAsync(cancellationToken).ConfigureAwait(false)) + await foreach (string featureName in _featureManager.GetFeatureNamesAsync().ConfigureAwait(false)) { featureNames.Add(featureName); } @@ -46,18 +45,18 @@ public async IAsyncEnumerable GetFeatureNamesAsync([EnumeratorCancellati } } - public Task IsEnabledAsync(string feature, CancellationToken cancellationToken) + public Task IsEnabledAsync(string feature) { return _flagCache.GetOrAdd( feature, - (key) => _featureManager.IsEnabledAsync(key, cancellationToken)); + (key) => _featureManager.IsEnabledAsync(key)); } - public Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken) + public Task IsEnabledAsync(string feature, TContext context) { return _flagCache.GetOrAdd( feature, - (key) => _featureManager.IsEnabledAsync(key, context, cancellationToken)); + (key) => _featureManager.IsEnabledAsync(key, context)); } public async ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken) diff --git a/src/Microsoft.FeatureManagement/IContextualFeatureFilter.cs b/src/Microsoft.FeatureManagement/IContextualFeatureFilter.cs index 84deb352..64586334 100644 --- a/src/Microsoft.FeatureManagement/IContextualFeatureFilter.cs +++ b/src/Microsoft.FeatureManagement/IContextualFeatureFilter.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement @@ -19,8 +18,7 @@ public interface IContextualFeatureFilter : IFeatureFilterMetadata /// /// A feature filter evaluation context that contains information that may be needed to evaluate the filter. This context includes configuration, if any, for this filter for the feature being evaluated. /// A context defined by the application that is passed in to the feature management system to provide contextual information for evaluating a feature's state. - /// The cancellation token to cancel the operation. /// True if the filter's criteria has been met, false otherwise. - Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, TContext appContext, CancellationToken cancellationToken); + Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, TContext appContext); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs index 3b5adc15..bc4895b9 100644 --- a/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/IFeatureDefinitionProvider.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. // using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement @@ -16,15 +15,13 @@ 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); + Task GetFeatureDefinitionAsync(string featureName); /// /// 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); + IAsyncEnumerable GetAllFeatureDefinitionsAsync(); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureFilter.cs b/src/Microsoft.FeatureManagement/IFeatureFilter.cs index 865a40dc..e6d914df 100644 --- a/src/Microsoft.FeatureManagement/IFeatureFilter.cs +++ b/src/Microsoft.FeatureManagement/IFeatureFilter.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement @@ -15,8 +14,7 @@ public interface IFeatureFilter : IFeatureFilterMetadata /// Evaluates the feature filter to see if the filter's criteria for being enabled has been satisfied. /// /// A feature filter evaluation context that contains information that may be needed to evaluate the filter. This context includes configuration, if any, for this filter for the feature being evaluated. - /// The cancellation token to cancel the operation. /// True if the filter's criteria has been met, false otherwise. - Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken); + Task EvaluateAsync(FeatureFilterEvaluationContext context); } } diff --git a/src/Microsoft.FeatureManagement/IFeatureManager.cs b/src/Microsoft.FeatureManagement/IFeatureManager.cs index d9b07e6a..374c4548 100644 --- a/src/Microsoft.FeatureManagement/IFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IFeatureManager.cs @@ -15,25 +15,22 @@ public interface IFeatureManager /// /// Retrieves a list of feature 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); + IAsyncEnumerable GetFeatureNamesAsync(); /// /// Checks whether a given feature is enabled. /// /// The name of the feature to check. - /// The cancellation token to cancel the operation. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature, CancellationToken cancellationToken); + Task IsEnabledAsync(string feature); /// /// Checks whether a given feature 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 cancellation token to cancel the operation. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken); + Task IsEnabledAsync(string feature, TContext context); } } diff --git a/src/Microsoft.FeatureManagement/ISessionManager.cs b/src/Microsoft.FeatureManagement/ISessionManager.cs index 1c0656f1..67189375 100644 --- a/src/Microsoft.FeatureManagement/ISessionManager.cs +++ b/src/Microsoft.FeatureManagement/ISessionManager.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement @@ -16,15 +15,13 @@ public interface ISessionManager /// /// The name of the feature. /// The state of the feature. - /// The cancellation token to cancel the operation. - Task SetAsync(string featureName, bool enabled, CancellationToken cancellationToken); + Task SetAsync(string featureName, bool enabled); /// /// Queries the session manager for the session's feature state, if any, for the given feature. /// /// The name of the feature. - /// The cancellation token to cancel the operation. /// The state of the feature if it is present in the session, otherwise null. - Task GetAsync(string featureName, CancellationToken cancellationToken); + Task GetAsync(string featureName); } } diff --git a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs index cd0a2ddc..8f651c69 100644 --- a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs @@ -13,18 +13,16 @@ public interface IVariantFeatureManager /// Checks whether a given feature is enabled. /// /// The name of the feature to check. - /// The cancellation token to cancel the operation. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature, CancellationToken cancellationToken = default); + Task IsEnabledAsync(string feature); /// /// Checks whether a given feature 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 cancellation token to cancel the operation. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken = default); + Task IsEnabledAsync(string feature, TContext context); /// /// Gets the assigned variant for a specfic feature. diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs index 874f16f9..34633749 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.Options; using Microsoft.FeatureManagement.Targeting; using System; -using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters @@ -47,10 +46,9 @@ public object BindParameters(IConfiguration filterParameters) /// /// The feature evaluation context. /// The targeting context to use during targeting evaluation. - /// The cancellation token to cancel the operation. /// Thrown if either or is null. /// True if the feature is enabled, false otherwise. - public Task EvaluateAsync(FeatureFilterEvaluationContext context, ITargetingContext targetingContext, CancellationToken cancellationToken) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, ITargetingContext targetingContext) { if (context == null) { diff --git a/src/Microsoft.FeatureManagement/Targeting/ITargetingContextAccessor.cs b/src/Microsoft.FeatureManagement/Targeting/ITargetingContextAccessor.cs index 2739e628..94c1eebd 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ITargetingContextAccessor.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ITargetingContextAccessor.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters @@ -14,8 +13,7 @@ public interface ITargetingContextAccessor /// /// Retrieves the current targeting context. /// - /// The cancellation token to cancel the operation. /// The current targeting context. - ValueTask GetContextAsync(CancellationToken cancellationToken); + ValueTask GetContextAsync(); } } diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs index 4e92205a..d57e4b31 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; -using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters @@ -48,10 +47,9 @@ public object BindParameters(IConfiguration filterParameters) /// Performs a targeting evaluation using the current to determine if a feature should be enabled. /// /// The feature evaluation context. - /// The cancellation token to cancel the operation. /// Thrown if is null. /// True if the feature is enabled, false otherwise. - public async Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken) + public async Task EvaluateAsync(FeatureFilterEvaluationContext context) { if (context == null) { @@ -60,7 +58,7 @@ public async Task EvaluateAsync(FeatureFilterEvaluationContext context, Ca // // Acquire targeting context via accessor - TargetingContext targetingContext = await _contextAccessor.GetContextAsync(cancellationToken).ConfigureAwait(false); + TargetingContext targetingContext = await _contextAccessor.GetContextAsync().ConfigureAwait(false); // // Ensure targeting can be performed @@ -73,7 +71,7 @@ public async Task EvaluateAsync(FeatureFilterEvaluationContext context, Ca // // Utilize contextual filter for targeting evaluation - return await _contextualFilter.EvaluateAsync(context, targetingContext, cancellationToken).ConfigureAwait(false); + return await _contextualFilter.EvaluateAsync(context, targetingContext).ConfigureAwait(false); } } } diff --git a/tests/Tests.FeatureManagement/ContextualTestFilter.cs b/tests/Tests.FeatureManagement/ContextualTestFilter.cs index 91bd6222..4bad3010 100644 --- a/tests/Tests.FeatureManagement/ContextualTestFilter.cs +++ b/tests/Tests.FeatureManagement/ContextualTestFilter.cs @@ -3,7 +3,6 @@ // using Microsoft.FeatureManagement; using System; -using System.Threading; using System.Threading.Tasks; namespace Tests.FeatureManagement @@ -12,7 +11,7 @@ class ContextualTestFilter : IContextualFeatureFilter { public Func ContextualCallback { get; set; } - public Task EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext) { return Task.FromResult(ContextualCallback?.Invoke(context, accountContext) ?? false); } diff --git a/tests/Tests.FeatureManagement/CustomTargetingFilter.cs b/tests/Tests.FeatureManagement/CustomTargetingFilter.cs index daf970fd..8e29feb8 100644 --- a/tests/Tests.FeatureManagement/CustomTargetingFilter.cs +++ b/tests/Tests.FeatureManagement/CustomTargetingFilter.cs @@ -24,9 +24,9 @@ public CustomTargetingFilter(IOptions options, ILogg public Func> Callback { get; set; } - public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken cancellationToken) + public Task EvaluateAsync(FeatureFilterEvaluationContext context) { - return _contextualFilter.EvaluateAsync(context, new TargetingContext(){ UserId = "Jeff" }, cancellationToken); + return _contextualFilter.EvaluateAsync(context, new TargetingContext(){ UserId = "Jeff" }); } } } diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index 483ea799..d9176c23 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -15,7 +15,6 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Threading; using System.Threading.Tasks; using Xunit; @@ -44,9 +43,9 @@ public async Task ReadsConfiguration() IFeatureManager featureManager = serviceProvider.GetRequiredService(); - Assert.True(await featureManager.IsEnabledAsync(OnFeature, CancellationToken.None)); + Assert.True(await featureManager.IsEnabledAsync(OnFeature)); - Assert.False(await featureManager.IsEnabledAsync(OffFeature, CancellationToken.None)); + Assert.False(await featureManager.IsEnabledAsync(OffFeature)); IEnumerable featureFilters = serviceProvider.GetRequiredService>(); @@ -67,7 +66,7 @@ public async Task ReadsConfiguration() return Task.FromResult(true); }; - await featureManager.IsEnabledAsync(ConditionalFeature, CancellationToken.None); + await featureManager.IsEnabledAsync(ConditionalFeature); Assert.True(called); } @@ -281,10 +280,10 @@ public async Task TimeWindow() IFeatureManager featureManager = provider.GetRequiredService(); - Assert.True(await featureManager.IsEnabledAsync(feature1, CancellationToken.None)); - Assert.False(await featureManager.IsEnabledAsync(feature2, CancellationToken.None)); - Assert.True(await featureManager.IsEnabledAsync(feature3, CancellationToken.None)); - Assert.False(await featureManager.IsEnabledAsync(feature4, CancellationToken.None)); + Assert.True(await featureManager.IsEnabledAsync(feature1)); + Assert.False(await featureManager.IsEnabledAsync(feature2)); + Assert.True(await featureManager.IsEnabledAsync(feature3)); + Assert.False(await featureManager.IsEnabledAsync(feature4)); } [Fact] @@ -311,7 +310,7 @@ public async Task Percentage() for (int i = 0; i < 10; i++) { - if (await featureManager.IsEnabledAsync(feature1, CancellationToken.None)) + if (await featureManager.IsEnabledAsync(feature1)) { enabledCount++; } @@ -349,21 +348,21 @@ public async Task Targeting() Assert.True(await featureManager.IsEnabledAsync(targetingTestFeature, new TargetingContext { UserId = "Jeff" - }, CancellationToken.None)); + })); // // Not targeted by user id, but targeted by default rollout Assert.True(await featureManager.IsEnabledAsync(targetingTestFeature, new TargetingContext { UserId = "Anne" - }, CancellationToken.None)); + })); // // Not targeted by user id or default rollout Assert.False(await featureManager.IsEnabledAsync(targetingTestFeature, new TargetingContext { UserId = "Patty" - }, CancellationToken.None)); + })); // // Targeted by group rollout @@ -371,7 +370,7 @@ public async Task Targeting() { UserId = "Patty", Groups = new List() { "Ring1" } - }, CancellationToken.None)); + })); // // Not targeted by user id, default rollout or group rollout @@ -379,7 +378,7 @@ public async Task Targeting() { UserId = "Isaac", Groups = new List() { "Ring1" } - }, CancellationToken.None)); + })); } [Fact] @@ -417,7 +416,7 @@ public async Task TargetingAccessor() UserId = "Jeff" }; - Assert.True(await featureManager.IsEnabledAsync(beta, CancellationToken.None)); + Assert.True(await featureManager.IsEnabledAsync(beta)); // // Not targeted by user id or default rollout @@ -426,7 +425,7 @@ public async Task TargetingAccessor() UserId = "Patty" }; - Assert.False(await featureManager.IsEnabledAsync(beta, CancellationToken.None)); + Assert.False(await featureManager.IsEnabledAsync(beta)); } [Fact] @@ -459,11 +458,11 @@ public async Task UsesContext() context.AccountId = "NotEnabledAccount"; - Assert.False(await featureManager.IsEnabledAsync(ContextualFeature, context, CancellationToken.None)); + Assert.False(await featureManager.IsEnabledAsync(ContextualFeature, context)); context.AccountId = "abc"; - Assert.True(await featureManager.IsEnabledAsync(ContextualFeature, context, CancellationToken.None)); + Assert.True(await featureManager.IsEnabledAsync(ContextualFeature, context)); } [Fact] @@ -505,7 +504,7 @@ public async Task ListsFeatures() bool hasItems = false; - await foreach (string feature in featureManager.GetFeatureNamesAsync(CancellationToken.None)) + await foreach (string feature in featureManager.GetFeatureNamesAsync()) { hasItems = true; @@ -531,8 +530,7 @@ public async Task ThrowsExceptionForMissingFeatureFilter() IFeatureManager featureManager = serviceProvider.GetRequiredService(); - FeatureManagementException e = await Assert.ThrowsAsync( - async () => await featureManager.IsEnabledAsync(ConditionalFeature, CancellationToken.None)); + FeatureManagementException e = await Assert.ThrowsAsync(async () => await featureManager.IsEnabledAsync(ConditionalFeature)); Assert.Equal(FeatureManagementError.MissingFeatureFilter, e.Error); } @@ -558,7 +556,7 @@ public async Task SwallowsExceptionForMissingFeatureFilter() IFeatureManager featureManager = serviceProvider.GetRequiredService(); - var isEnabled = await featureManager.IsEnabledAsync(ConditionalFeature, CancellationToken.None); + var isEnabled = await featureManager.IsEnabledAsync(ConditionalFeature); Assert.False(isEnabled); } @@ -636,7 +634,7 @@ public async Task CustomFeatureDefinitionProvider() return Task.FromResult(true); }; - await featureManager.IsEnabledAsync(ConditionalFeature, CancellationToken.None); + await featureManager.IsEnabledAsync(ConditionalFeature); Assert.True(called); } diff --git a/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs b/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs index 76d64a10..fe613254 100644 --- a/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs +++ b/tests/Tests.FeatureManagement/InMemoryFeatureDefinitionProvider.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; using System.Threading.Tasks; namespace Tests.FeatureManagement @@ -18,7 +16,7 @@ public InMemoryFeatureDefinitionProvider(IEnumerable featureD } #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - public async IAsyncEnumerable GetAllFeatureDefinitionsAsync([EnumeratorCancellation] CancellationToken _) + public async IAsyncEnumerable GetAllFeatureDefinitionsAsync() #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { foreach (FeatureDefinition definition in _definitions) @@ -27,7 +25,7 @@ public async IAsyncEnumerable GetAllFeatureDefinitionsAsync([ } } - public Task GetFeatureDefinitionAsync(string featureName, CancellationToken _) + public Task GetFeatureDefinitionAsync(string featureName) { return Task.FromResult(_definitions.FirstOrDefault(definitions => definitions.Name.Equals(featureName, StringComparison.OrdinalIgnoreCase))); } diff --git a/tests/Tests.FeatureManagement/InvalidFeatureFilter.cs b/tests/Tests.FeatureManagement/InvalidFeatureFilter.cs index 4f81e3e2..0fab5063 100644 --- a/tests/Tests.FeatureManagement/InvalidFeatureFilter.cs +++ b/tests/Tests.FeatureManagement/InvalidFeatureFilter.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. // using Microsoft.FeatureManagement; -using System.Threading; using System.Threading.Tasks; namespace Tests.FeatureManagement @@ -11,12 +10,12 @@ namespace Tests.FeatureManagement // Cannot implement more than one IFeatureFilter interface class InvalidFeatureFilter : IContextualFeatureFilter, IContextualFeatureFilter { - public Task EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext) { return Task.FromResult(false); } - public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, object appContext, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, object appContext) { return Task.FromResult(false); } @@ -26,12 +25,12 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterCont // Cannot implement more than one IFeatureFilter interface class InvalidFeatureFilter2 : IFeatureFilter, IContextualFeatureFilter { - public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, object appContext, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext, object appContext) { return Task.FromResult(false); } - public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext context) { return Task.FromResult(false); } diff --git a/tests/Tests.FeatureManagement/OnDemandTargetingContextAccessor.cs b/tests/Tests.FeatureManagement/OnDemandTargetingContextAccessor.cs index aaff4a41..7eb7e971 100644 --- a/tests/Tests.FeatureManagement/OnDemandTargetingContextAccessor.cs +++ b/tests/Tests.FeatureManagement/OnDemandTargetingContextAccessor.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. // using Microsoft.FeatureManagement.FeatureFilters; -using System.Threading; using System.Threading.Tasks; namespace Tests.FeatureManagement @@ -11,7 +10,7 @@ class OnDemandTargetingContextAccessor : ITargetingContextAccessor { public TargetingContext Current { get; set; } - public ValueTask GetContextAsync(CancellationToken _) + public ValueTask GetContextAsync() { return new ValueTask(Current); } diff --git a/tests/Tests.FeatureManagement/TestFilter.cs b/tests/Tests.FeatureManagement/TestFilter.cs index b2de169d..2dbf0f71 100644 --- a/tests/Tests.FeatureManagement/TestFilter.cs +++ b/tests/Tests.FeatureManagement/TestFilter.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.FeatureManagement; using System; -using System.Threading; using System.Threading.Tasks; namespace Tests.FeatureManagement @@ -25,7 +24,7 @@ public object BindParameters(IConfiguration parameters) return parameters; } - public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) + public Task EvaluateAsync(FeatureFilterEvaluationContext context) { return Callback?.Invoke(context) ?? Task.FromResult(false); } From 9bbd115a2b69d05eb5fd3502d4c83e9695288d23 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 3 Aug 2023 17:12:25 -0500 Subject: [PATCH 43/80] add comments for new classes --- src/Microsoft.FeatureManagement/FeatureManagementError.cs | 2 +- src/Microsoft.FeatureManagement/FeatureVariant.cs | 4 ++-- src/Microsoft.FeatureManagement/Group.cs | 4 ++-- src/Microsoft.FeatureManagement/Percentile.cs | 6 +++--- src/Microsoft.FeatureManagement/Status.cs | 6 +++--- src/Microsoft.FeatureManagement/StatusOverride.cs | 8 ++++---- src/Microsoft.FeatureManagement/User.cs | 4 ++-- src/Microsoft.FeatureManagement/Variant.cs | 6 +++--- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManagementError.cs b/src/Microsoft.FeatureManagement/FeatureManagementError.cs index ebff2a4d..33502bf7 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementError.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementError.cs @@ -39,7 +39,7 @@ public enum FeatureManagementError MissingFeatureVariant, /// - /// A feature variant specifies both a configuration reference and configuration value, or an invalid configuration reference. + /// A feature variant specifies both a configuration reference and configuration value, or there is no valid configuration to reference. /// InvalidVariantConfiguration } diff --git a/src/Microsoft.FeatureManagement/FeatureVariant.cs b/src/Microsoft.FeatureManagement/FeatureVariant.cs index 736ff261..feaaa7e9 100644 --- a/src/Microsoft.FeatureManagement/FeatureVariant.cs +++ b/src/Microsoft.FeatureManagement/FeatureVariant.cs @@ -5,7 +5,7 @@ namespace Microsoft.FeatureManagement { /// - /// A variant of a feature. + /// The definition for a variant of a feature. /// public class FeatureVariant { @@ -25,7 +25,7 @@ public class FeatureVariant public string ConfigurationReference { get; set; } /// - /// + /// Overrides the state of the feature if this variant has been assigned. /// public StatusOverride StatusOverride { get; set; } } diff --git a/src/Microsoft.FeatureManagement/Group.cs b/src/Microsoft.FeatureManagement/Group.cs index c72f6135..37dc5099 100644 --- a/src/Microsoft.FeatureManagement/Group.cs +++ b/src/Microsoft.FeatureManagement/Group.cs @@ -7,7 +7,7 @@ namespace Microsoft.FeatureManagement { /// - /// + /// The definition of a group allocation. /// public class Group { @@ -17,7 +17,7 @@ public class Group public string Variant { get; set; } /// - /// + /// A list of groups that can be assigned this variant. /// public IEnumerable Groups { get; set; } } diff --git a/src/Microsoft.FeatureManagement/Percentile.cs b/src/Microsoft.FeatureManagement/Percentile.cs index fa104948..ff0652fb 100644 --- a/src/Microsoft.FeatureManagement/Percentile.cs +++ b/src/Microsoft.FeatureManagement/Percentile.cs @@ -5,7 +5,7 @@ namespace Microsoft.FeatureManagement { /// - /// + /// The definition of a percentile allocation. /// public class Percentile { @@ -15,12 +15,12 @@ public class Percentile public string Variant { get; set; } /// - /// + /// The lower bound of the percentage to which the variant will be assigned. /// public double From { get; set; } /// - /// + /// The upper bound of the percentage to which the variant will be assigned. /// public double To { get; set; } } diff --git a/src/Microsoft.FeatureManagement/Status.cs b/src/Microsoft.FeatureManagement/Status.cs index 5824b643..0a714afb 100644 --- a/src/Microsoft.FeatureManagement/Status.cs +++ b/src/Microsoft.FeatureManagement/Status.cs @@ -4,16 +4,16 @@ namespace Microsoft.FeatureManagement { /// - /// + /// Describes how a feature's state will be evaluated. /// public enum Status { /// - /// + /// The state of the feature is conditional on the rest of its definition. /// Conditional, /// - /// + /// The state of the feature is disabled. /// Disabled } diff --git a/src/Microsoft.FeatureManagement/StatusOverride.cs b/src/Microsoft.FeatureManagement/StatusOverride.cs index 9a219e7e..6df84076 100644 --- a/src/Microsoft.FeatureManagement/StatusOverride.cs +++ b/src/Microsoft.FeatureManagement/StatusOverride.cs @@ -4,20 +4,20 @@ namespace Microsoft.FeatureManagement { /// - /// + /// Overrides the feature's state with this value when a variant has been assigned. /// public enum StatusOverride { /// - /// + /// Does not affect the feature state. /// None, /// - /// + /// The feature will be considered enabled. /// Enabled, /// - /// + /// The feature will be considered disabled. /// Disabled } diff --git a/src/Microsoft.FeatureManagement/User.cs b/src/Microsoft.FeatureManagement/User.cs index bc56bf10..7947f93f 100644 --- a/src/Microsoft.FeatureManagement/User.cs +++ b/src/Microsoft.FeatureManagement/User.cs @@ -7,7 +7,7 @@ namespace Microsoft.FeatureManagement { /// - /// + /// The definition of a user allocation. /// public class User { @@ -17,7 +17,7 @@ public class User public string Variant { get; set; } /// - /// + /// A list of users that will be assigned this variant. /// public IEnumerable Users { get; set; } } diff --git a/src/Microsoft.FeatureManagement/Variant.cs b/src/Microsoft.FeatureManagement/Variant.cs index 62f1ed2c..1a7dccc1 100644 --- a/src/Microsoft.FeatureManagement/Variant.cs +++ b/src/Microsoft.FeatureManagement/Variant.cs @@ -6,17 +6,17 @@ namespace Microsoft.FeatureManagement { /// - /// + /// A variant for a feature. /// public class Variant { /// - /// + /// The name of the variant. /// public string Name { get; set; } /// - /// + /// The configuration value. /// public IConfigurationSection Configuration { get; set; } } From 3ea099b079fac4d5b1c1dbe2590d4dda4d4647e9 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 4 Aug 2023 10:54:50 -0500 Subject: [PATCH 44/80] fix comments for public classes again --- src/Microsoft.FeatureManagement/Allocation.cs | 6 +++--- src/Microsoft.FeatureManagement/FeatureVariant.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.FeatureManagement/Allocation.cs b/src/Microsoft.FeatureManagement/Allocation.cs index e7350c04..e9d9754d 100644 --- a/src/Microsoft.FeatureManagement/Allocation.cs +++ b/src/Microsoft.FeatureManagement/Allocation.cs @@ -6,17 +6,17 @@ namespace Microsoft.FeatureManagement { /// - /// + /// The definition of how variants are allocated for a feature. /// public class Allocation { /// - /// + /// The default variant used if the feature is enabled and no variant is assigned. /// public string DefaultWhenEnabled { get; set; } /// - /// + /// The default variant used if the feature is disabled and no variant is assigned. /// public string DefaultWhenDisabled { get; set; } diff --git a/src/Microsoft.FeatureManagement/FeatureVariant.cs b/src/Microsoft.FeatureManagement/FeatureVariant.cs index feaaa7e9..c764ca5f 100644 --- a/src/Microsoft.FeatureManagement/FeatureVariant.cs +++ b/src/Microsoft.FeatureManagement/FeatureVariant.cs @@ -27,6 +27,6 @@ public class FeatureVariant /// /// Overrides the state of the feature if this variant has been assigned. /// - public StatusOverride StatusOverride { get; set; } + public StatusOverride StatusOverride { get; set; } = StatusOverride.None; } } From 8306eb4653ded0d9ee1f1247bee7edc304ea1d82 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 4 Aug 2023 11:59:27 -0500 Subject: [PATCH 45/80] update comments, default values --- src/Microsoft.FeatureManagement/Allocation.cs | 4 +- .../IVariantFeatureManager.cs | 2 +- src/Microsoft.FeatureManagement/NameHelper.cs | 59 ------------------- .../Targeting/TargetingEvaluator.cs | 22 +++++++ 4 files changed, 25 insertions(+), 62 deletions(-) delete mode 100644 src/Microsoft.FeatureManagement/NameHelper.cs diff --git a/src/Microsoft.FeatureManagement/Allocation.cs b/src/Microsoft.FeatureManagement/Allocation.cs index e9d9754d..d1fcf8ae 100644 --- a/src/Microsoft.FeatureManagement/Allocation.cs +++ b/src/Microsoft.FeatureManagement/Allocation.cs @@ -21,7 +21,7 @@ public class Allocation public string DefaultWhenDisabled { get; set; } /// - /// Describes a mapping of user id to variant. + /// Describes a mapping of user ids to variants. /// public IEnumerable User { get; set; } @@ -31,7 +31,7 @@ public class Allocation public IEnumerable Group { get; set; } /// - /// Allocate a percentage of the user base to variants. + /// Allocates a percentage of the user base to chosen variants. /// public IEnumerable Percentile { get; set; } diff --git a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs index 8f651c69..ec2923e7 100644 --- a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs @@ -36,7 +36,7 @@ public interface IVariantFeatureManager /// Gets the assigned variant for a specfic feature. /// /// The name of the feature from which the variant will be assigned. - /// A context providing information that can be used to evaluate which variant the user will be assigned. + /// An instance of used to evaluate which variant the user will be assigned. /// The cancellation token to cancel the operation. /// A variant assigned to the user based on the feature's allocation logic. ValueTask GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken = default); diff --git a/src/Microsoft.FeatureManagement/NameHelper.cs b/src/Microsoft.FeatureManagement/NameHelper.cs deleted file mode 100644 index 70e1f802..00000000 --- a/src/Microsoft.FeatureManagement/NameHelper.cs +++ /dev/null @@ -1,59 +0,0 @@ -// 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 reference matches a given feature filter 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. - /// An optional suffix that may be included when referencing the metadata type. E.g. "filter". - /// 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 can be referenced with or without their associated suffix ('filter') - // 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 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/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs index c34ae83a..4ca1f27b 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Security.Cryptography; using System.Text; +using System.Threading.Tasks; namespace Microsoft.FeatureManagement.Targeting { @@ -37,6 +38,27 @@ public static bool IsTargeted(ITargetingContext targetingContext, TargetingFilte throw new ArgumentException(reason, paramName); } + if (settings.Audience.Exclusion != null) + { + // + // Check if the user is in the exclusion directly + if (targetingContext.UserId != null && + settings.Audience.Exclusion.Users != null && + settings.Audience.Exclusion.Users.Any(user => targetingContext.UserId.Equals(user, GetComparisonType(ignoreCase)))) + { + return false; + } + + // + // Check if the user is in a group within exclusion + if (targetingContext.Groups != null && + settings.Audience.Exclusion.Groups != null && + settings.Audience.Exclusion.Groups.Any(group => targetingContext.Groups.Any(g => g?.Equals(group, GetComparisonType(ignoreCase)) ?? false))) + { + return false; + } + } + // // Check if the user is being targeted directly if (settings.Audience.Users != null && From 8765c939e59bd127704599062439de9380145407 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 4 Aug 2023 13:30:53 -0500 Subject: [PATCH 46/80] fix variantconfigurationsection, comments in definitionprovider --- .../ConfigurationFeatureDefinitionProvider.cs | 4 ++-- src/Microsoft.FeatureManagement/FeatureManager.cs | 2 +- .../VariantConfigurationSection.cs | 7 +++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 7697dd05..d0226969 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -232,12 +232,12 @@ private T ParseFeatureDefinitionSectionEnum(IConfigurationSection configurati string rawValue = configurationSection[keyword]; // - // If requirement type is specified, parse it and set the requirementType variable + // If the enum is specified, parse it and set the return value if (!string.IsNullOrEmpty(rawValue) && !Enum.TryParse(rawValue, ignoreCase: true, out enumValue)) { throw new FeatureManagementException( FeatureManagementError.InvalidConfigurationSetting, - $"Invalid requirement type '{rawValue}' for feature '{configurationSection.Key}'."); + $"Invalid {typeof(T)?.Name} with value '{rawValue}' for feature '{configurationSection.Key}'."); } return enumValue; diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 748375fb..470d12aa 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -328,7 +328,7 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex } else if (configValueSet) { - VariantConfigurationSection section = new VariantConfigurationSection(featureVariant.Name, featureVariant.ConfigurationValue); + VariantConfigurationSection section = new VariantConfigurationSection(featureVariant.Name, "", featureVariant.ConfigurationValue); variantConfiguration = section; } diff --git a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs index 5f62d079..c63c9404 100644 --- a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs +++ b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs @@ -14,13 +14,12 @@ internal class VariantConfigurationSection : IConfigurationSection private readonly string _path; private string _key; - public VariantConfigurationSection(string key, string value) + public VariantConfigurationSection(string key, string path, string value) { MemoryConfigurationSource source = new MemoryConfigurationSource(); - _path = "Root"; - source.InitialData = new List> { new KeyValuePair($"{_path}:{key}", value) }; _root = new ConfigurationRoot(new List { new MemoryConfigurationProvider(source) }); + _path = path; _key = key; Value = value; } @@ -65,7 +64,7 @@ public IChangeToken GetReloadToken() public IConfigurationSection GetSection(string key) { - throw new NotImplementedException(); + return new VariantConfigurationSection(key, key, null); } } } From a3466c7d9474bfb4eba3dd7cef70c1fcc37673d9 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 4 Aug 2023 13:57:41 -0500 Subject: [PATCH 47/80] fix using statements, null checks --- .../ConfigurationFeatureDefinitionProvider.cs | 3 ++- .../FeatureManagementError.cs | 7 ++++++- .../FeatureManager.cs | 16 ++++++++-------- .../IFeatureManager.cs | 1 - .../VariantConfigurationSection.cs | 6 ++++-- .../CustomTargetingFilter.cs | 1 - 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index d0226969..ca43e25b 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -136,7 +136,7 @@ We support RequirementType requirementType = RequirementType.Any; Status status = Status.Conditional; Allocation allocation = null; - List variants = new List(); + List variants = null; var enabledFor = new List(); @@ -187,6 +187,7 @@ We support allocationSection.Bind(allocation); IEnumerable variantsSections = configurationSection.GetSection(nameof(FeatureDefinition.Variants)).GetChildren(); + variants = new List(); foreach (IConfigurationSection section in variantsSections) { diff --git a/src/Microsoft.FeatureManagement/FeatureManagementError.cs b/src/Microsoft.FeatureManagement/FeatureManagementError.cs index 33502bf7..615411c0 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementError.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementError.cs @@ -41,6 +41,11 @@ public enum FeatureManagementError /// /// A feature variant specifies both a configuration reference and configuration value, or there is no valid configuration to reference. /// - InvalidVariantConfiguration + InvalidVariantConfiguration, + + /// + /// A feature does not have allocation defined when attempting to resolve the variant. + /// + MissingAllocation } } diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 470d12aa..d9a85bd3 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -214,7 +214,7 @@ await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) } } - if (!ignoreVariant && (featureDefinition.Variants?.Any() ?? false) && featureDefinition.Status != Status.Disabled) + if (!ignoreVariant && (featureDefinition.Variants?.Any() ?? false) && featureDefinition.Allocation != null && featureDefinition.Status != Status.Disabled) { FeatureVariant featureVariant = await GetFeatureVariantAsync(featureDefinition, appContext as TargetingContext, useAppContext, enabled, CancellationToken.None); @@ -286,6 +286,13 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex $"The feature declaration for the feature '{feature}' was not found."); } + if (featureDefinition.Allocation == null) + { + throw new FeatureManagementException( + FeatureManagementError.MissingAllocation, + $"No allocation is defined for the feature {featureDefinition.Name}"); + } + if (!featureDefinition.Variants?.Any() ?? false) { throw new FeatureManagementException( @@ -388,13 +395,6 @@ private async ValueTask GetFeatureVariantAsync(FeatureDefinition private ValueTask AssignVariantAsync(FeatureDefinition featureDefinition, TargetingContext targetingContext, CancellationToken cancellationToken) { - if (!featureDefinition.Variants?.Any() ?? false) - { - throw new FeatureManagementException( - FeatureManagementError.MissingFeatureVariant, - $"No variants are registered for the feature {featureDefinition.Name}"); - } - FeatureVariant variant = null; if (featureDefinition.Allocation.User != null) diff --git a/src/Microsoft.FeatureManagement/IFeatureManager.cs b/src/Microsoft.FeatureManagement/IFeatureManager.cs index 374c4548..1b4ea0cf 100644 --- a/src/Microsoft.FeatureManagement/IFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IFeatureManager.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. // using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement diff --git a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs index c63c9404..a2008627 100644 --- a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs +++ b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs @@ -1,7 +1,9 @@ -using Microsoft.Extensions.Configuration; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Memory; using Microsoft.Extensions.Primitives; -using System; using System.Collections.Generic; using System.Linq; using System.Threading; diff --git a/tests/Tests.FeatureManagement/CustomTargetingFilter.cs b/tests/Tests.FeatureManagement/CustomTargetingFilter.cs index 8e29feb8..36579146 100644 --- a/tests/Tests.FeatureManagement/CustomTargetingFilter.cs +++ b/tests/Tests.FeatureManagement/CustomTargetingFilter.cs @@ -6,7 +6,6 @@ using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.FeatureFilters; using System; -using System.Threading; using System.Threading.Tasks; namespace Tests.FeatureManagement From eb806942689e62c5d685c2bb39a3838fddf40906 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 8 Aug 2023 12:34:08 -0500 Subject: [PATCH 48/80] fix unit test failures with servicecollectionextensions --- .../ServiceCollectionExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 8f368728..fa7f7bfd 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -34,11 +34,11 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec services.AddSingleton(); - services.AddSingleton(); + services.AddScoped(); - services.TryAddSingleton(sp => sp.GetRequiredService()); + services.TryAddScoped(sp => sp.GetRequiredService()); - services.TryAddSingleton(sp => sp.GetRequiredService()); + services.TryAddScoped(sp => sp.GetRequiredService()); return new FeatureManagementBuilder(services); } From 0b8f9e97819dfee6ae6fdfa453101941af7e1476 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 10 Aug 2023 14:24:57 -0500 Subject: [PATCH 49/80] add revisions: fix namepaces, add exceptions tests, combine percentage logic, fix comments, add cancellationtoken to new interface --- examples/TargetingConsoleApp/Program.cs | 2 +- .../ConfigurationFeatureDefinitionProvider.cs | 10 +++- .../FeatureDefinition.cs | 1 + .../FeatureManager.cs | 33 +++++++---- .../FeatureManagerSnapshot.cs | 14 +++++ .../IVariantFeatureManager.cs | 9 ++- .../Targeting/TargetingEvaluator.cs | 28 +++------- .../{ => VariantAllocation}/Allocation.cs | 6 +- .../{ => VariantAllocation}/Group.cs | 2 +- .../{ => VariantAllocation}/Percentile.cs | 2 +- .../{ => VariantAllocation}/User.cs | 2 +- .../FeatureManagement.cs | 32 +++++++++++ .../Tests.FeatureManagement/appsettings.json | 56 +++++++++++++++++++ 13 files changed, 153 insertions(+), 44 deletions(-) rename src/Microsoft.FeatureManagement/{ => VariantAllocation}/Allocation.cs (85%) rename src/Microsoft.FeatureManagement/{ => VariantAllocation}/Group.cs (90%) rename src/Microsoft.FeatureManagement/{ => VariantAllocation}/Percentile.cs (92%) rename src/Microsoft.FeatureManagement/{ => VariantAllocation}/User.cs (90%) diff --git a/examples/TargetingConsoleApp/Program.cs b/examples/TargetingConsoleApp/Program.cs index ad115215..953f609e 100644 --- a/examples/TargetingConsoleApp/Program.cs +++ b/examples/TargetingConsoleApp/Program.cs @@ -52,7 +52,7 @@ public static async Task Main(string[] args) // // Get user - AccountService.Identity.User user = await userRepository.GetUser(userId); + User user = await userRepository.GetUser(userId); // // Check if feature enabled diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index ca43e25b..81b987f2 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -3,12 +3,14 @@ // using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Primitives; +using Microsoft.FeatureManagement.VariantAllocation; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using static System.Collections.Specialized.BitVector32; namespace Microsoft.FeatureManagement { @@ -183,8 +185,12 @@ We support } IConfigurationSection allocationSection = configurationSection.GetSection(nameof(FeatureDefinition.Allocation)); - allocation = new Allocation(); - allocationSection.Bind(allocation); + + if (allocationSection.Exists()) + { + allocation = new Allocation(); + allocationSection.Bind(allocation); + } IEnumerable variantsSections = configurationSection.GetSection(nameof(FeatureDefinition.Variants)).GetChildren(); variants = new List(); diff --git a/src/Microsoft.FeatureManagement/FeatureDefinition.cs b/src/Microsoft.FeatureManagement/FeatureDefinition.cs index 762bca13..bab3b84e 100644 --- a/src/Microsoft.FeatureManagement/FeatureDefinition.cs +++ b/src/Microsoft.FeatureManagement/FeatureDefinition.cs @@ -3,6 +3,7 @@ // using System.Collections.Generic; using System.Linq; +using Microsoft.FeatureManagement.VariantAllocation; namespace Microsoft.FeatureManagement { diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index d9a85bd3..d04820ed 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Options; using Microsoft.FeatureManagement.FeatureFilters; using Microsoft.FeatureManagement.Targeting; +using Microsoft.FeatureManagement.VariantAllocation; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -68,17 +69,27 @@ public FeatureManager( public Task IsEnabledAsync(string feature) { - return IsEnabledAsync(feature, null, false, false); + return IsEnabledAsync(feature, null, false, false, CancellationToken.None); } public Task IsEnabledAsync(string feature, TContext appContext) { - return IsEnabledAsync(feature, appContext, true, false); + return IsEnabledAsync(feature, appContext, true, false, CancellationToken.None); } - public Task IsEnabledAsync(string feature, TContext appContext, bool ignoreVariant) + public Task IsEnabledAsync(string feature, CancellationToken cancellationToken) { - return IsEnabledAsync(feature, appContext, true, ignoreVariant); + return IsEnabledAsync(feature, null, false, false, cancellationToken); + } + + public Task IsEnabledAsync(string feature, TContext appContext, CancellationToken cancellationToken) + { + return IsEnabledAsync(feature, appContext, true, false, cancellationToken); + } + + private Task IsEnabledAsync(string feature, TContext appContext, bool ignoreVariant, CancellationToken cancellationToken) + { + return IsEnabledAsync(feature, appContext, true, ignoreVariant, cancellationToken); } public async IAsyncEnumerable GetFeatureNamesAsync() @@ -94,7 +105,7 @@ public void Dispose() _parametersCache.Dispose(); } - private async Task IsEnabledAsync(string feature, TContext appContext, bool useAppContext, bool ignoreVariant) + private async Task IsEnabledAsync(string feature, TContext appContext, bool useAppContext, bool ignoreVariant, CancellationToken cancellationToken) { foreach (ISessionManager sessionManager in _sessionManagers) { @@ -302,7 +313,7 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex FeatureVariant featureVariant; - bool isFeatureEnabled = await IsEnabledAsync(feature, context, true).ConfigureAwait(false); + bool isFeatureEnabled = await IsEnabledAsync(feature, context, true, cancellationToken).ConfigureAwait(false); featureVariant = await GetFeatureVariantAsync(featureDefinition, context, useContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); @@ -374,8 +385,6 @@ private async ValueTask GetFeatureVariantAsync(FeatureDefinition if (context == null) { _logger.LogWarning("No targeting context available for targeting evaluation."); - - return null; } } } @@ -403,7 +412,7 @@ private ValueTask AssignVariantAsync(FeatureDefinition featureDe { if (TargetingEvaluator.IsTargeted(targetingContext, user.Users, _assignerOptions.IgnoreCase)) { - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(user.Variant)); + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == user.Variant); if (!string.IsNullOrEmpty(variant.Name)) { @@ -419,7 +428,7 @@ private ValueTask AssignVariantAsync(FeatureDefinition featureDe { if (TargetingEvaluator.IsGroupTargeted(targetingContext, group.Groups, _assignerOptions.IgnoreCase)) { - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(group.Variant)); + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == group.Variant); if (!string.IsNullOrEmpty(variant.Name)) { @@ -435,7 +444,7 @@ private ValueTask AssignVariantAsync(FeatureDefinition featureDe { if (TargetingEvaluator.IsTargeted(targetingContext, percentile.From, percentile.To, featureDefinition.Allocation.Seed, _assignerOptions.IgnoreCase, featureDefinition.Name)) { - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(percentile.Variant)); + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == percentile.Variant); if (!string.IsNullOrEmpty(variant.Name)) { @@ -454,7 +463,7 @@ private FeatureVariant ResolveDefaultFeatureVariant(FeatureDefinition featureDef if (!string.IsNullOrEmpty(defaultVariantPath)) { - FeatureVariant defaultVariant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name.Equals(defaultVariantPath)); + FeatureVariant defaultVariant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == defaultVariantPath); if (!string.IsNullOrEmpty(defaultVariant.Name)) { diff --git a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs index 1f8d7516..886447a0 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs @@ -59,6 +59,20 @@ public Task IsEnabledAsync(string feature, TContext context) (key) => _featureManager.IsEnabledAsync(key, context)); } + public Task IsEnabledAsync(string feature, CancellationToken cancellationToken) + { + return _flagCache.GetOrAdd( + feature, + (key) => _featureManager.IsEnabledAsync(key)); + } + + public Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken) + { + return _flagCache.GetOrAdd( + feature, + (key) => _featureManager.IsEnabledAsync(key, context)); + } + public async ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken) { string cacheKey = GetVariantCacheKey(feature); diff --git a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs index ec2923e7..e6a3b5a4 100644 --- a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs @@ -7,22 +7,27 @@ namespace Microsoft.FeatureManagement { + /// + /// Used to evaluate the variant for a feature or whether a feature is enabled or disabled. + /// public interface IVariantFeatureManager { /// /// Checks whether a given feature is enabled. /// /// The name of the feature to check. + /// The cancellation token to cancel the operation. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature); + Task IsEnabledAsync(string feature, CancellationToken cancellationToken = default); /// /// Checks whether a given feature 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 cancellation token to cancel the operation. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature, TContext context); + Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken = default); /// /// Gets the assigned variant for a specfic feature. diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs index 4ca1f27b..bd6e5c2b 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Security.Cryptography; using System.Text; -using System.Threading.Tasks; namespace Microsoft.FeatureManagement.Targeting { @@ -198,7 +197,7 @@ public static bool IsTargeted( { string audienceContextId = $"{userId}\n{hint}\n{group}"; - if (IsTargeted(audienceContextId, groupRollout.RolloutPercentage)) + if (IsTargeted(audienceContextId, 0, groupRollout.RolloutPercentage)) { return true; } @@ -234,7 +233,7 @@ public static bool IsTargeted( string defaultContextId = $"{userId}\n{hint}"; - return IsTargeted(defaultContextId, defaultRolloutPercentage); + return IsTargeted(defaultContextId, 0, defaultRolloutPercentage); } /// @@ -310,8 +309,6 @@ public static bool TryValidateSettings(TargetingFilterSettings targetingSettings /// public static bool IsTargeted(ITargetingContext targetingContext, double from, double to, string seed, bool ignoreCase, string hint) { - byte[] hash; - string userId = ignoreCase ? targetingContext.UserId.ToLower() : targetingContext.UserId; @@ -327,28 +324,17 @@ public static bool IsTargeted(ITargetingContext targetingContext, double from, d contextId = $"{userId}\n{hint}"; } - using (HashAlgorithm hashAlgorithm = SHA256.Create()) - { - hash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(contextId).ToArray()); - } - - // - // Use first 4 bytes for percentage calculation - // Cryptographic hashing algorithms ensure adequate entropy across hash - uint contextMarker = BitConverter.ToUInt32(hash, 0); - - double contextPercentage = (contextMarker / (double)uint.MaxValue) * 100; - - return contextPercentage >= from && contextPercentage <= to; + return IsTargeted(contextId, from, to); } /// /// Determines if a given context id should be targeted based off the provided percentage /// /// A context identifier that determines what the percentage is applicable for - /// The total percentage of possible context identifiers that should be targeted + /// The lower bound of the percentage for which the context identifier will be targeted + /// The upper bound of the percentage for which the context identifier will be targeted /// A boolean representing if the context identifier should be targeted - private static bool IsTargeted(string contextId, double percentage) + private static bool IsTargeted(string contextId, double from, double to) { byte[] hash; @@ -364,7 +350,7 @@ private static bool IsTargeted(string contextId, double percentage) double contextPercentage = (contextMarker / (double)uint.MaxValue) * 100; - return contextPercentage < percentage; + return contextPercentage >= from && contextPercentage < to; } } } diff --git a/src/Microsoft.FeatureManagement/Allocation.cs b/src/Microsoft.FeatureManagement/VariantAllocation/Allocation.cs similarity index 85% rename from src/Microsoft.FeatureManagement/Allocation.cs rename to src/Microsoft.FeatureManagement/VariantAllocation/Allocation.cs index d1fcf8ae..54a5e5c4 100644 --- a/src/Microsoft.FeatureManagement/Allocation.cs +++ b/src/Microsoft.FeatureManagement/VariantAllocation/Allocation.cs @@ -3,7 +3,7 @@ // using System.Collections.Generic; -namespace Microsoft.FeatureManagement +namespace Microsoft.FeatureManagement.VariantAllocation { /// /// The definition of how variants are allocated for a feature. @@ -11,7 +11,7 @@ namespace Microsoft.FeatureManagement public class Allocation { /// - /// The default variant used if the feature is enabled and no variant is assigned. + /// The default variant used if the feature is disabled. /// public string DefaultWhenEnabled { get; set; } @@ -31,7 +31,7 @@ public class Allocation public IEnumerable Group { get; set; } /// - /// Allocates a percentage of the user base to chosen variants. + /// Allocates percentiles of user base to variants. /// public IEnumerable Percentile { get; set; } diff --git a/src/Microsoft.FeatureManagement/Group.cs b/src/Microsoft.FeatureManagement/VariantAllocation/Group.cs similarity index 90% rename from src/Microsoft.FeatureManagement/Group.cs rename to src/Microsoft.FeatureManagement/VariantAllocation/Group.cs index 37dc5099..558dc69d 100644 --- a/src/Microsoft.FeatureManagement/Group.cs +++ b/src/Microsoft.FeatureManagement/VariantAllocation/Group.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; -namespace Microsoft.FeatureManagement +namespace Microsoft.FeatureManagement.VariantAllocation { /// /// The definition of a group allocation. diff --git a/src/Microsoft.FeatureManagement/Percentile.cs b/src/Microsoft.FeatureManagement/VariantAllocation/Percentile.cs similarity index 92% rename from src/Microsoft.FeatureManagement/Percentile.cs rename to src/Microsoft.FeatureManagement/VariantAllocation/Percentile.cs index ff0652fb..5e238936 100644 --- a/src/Microsoft.FeatureManagement/Percentile.cs +++ b/src/Microsoft.FeatureManagement/VariantAllocation/Percentile.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. // -namespace Microsoft.FeatureManagement +namespace Microsoft.FeatureManagement.VariantAllocation { /// /// The definition of a percentile allocation. diff --git a/src/Microsoft.FeatureManagement/User.cs b/src/Microsoft.FeatureManagement/VariantAllocation/User.cs similarity index 90% rename from src/Microsoft.FeatureManagement/User.cs rename to src/Microsoft.FeatureManagement/VariantAllocation/User.cs index 7947f93f..b6723783 100644 --- a/src/Microsoft.FeatureManagement/User.cs +++ b/src/Microsoft.FeatureManagement/VariantAllocation/User.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; -namespace Microsoft.FeatureManagement +namespace Microsoft.FeatureManagement.VariantAllocation { /// /// The definition of a user allocation. diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index d9176c23..b3850560 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -1024,6 +1024,38 @@ public async Task UsesVariants() Assert.True(await featureManager.IsEnabledAsync("VariantFeatureGroup")); } + [Fact] + public async Task VariantsExceptions() + { + IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + + var services = new ServiceCollection(); + + var targetingContextAccessor = new OnDemandTargetingContextAccessor(); + services.AddSingleton(targetingContextAccessor) + .AddSingleton(config) + .AddFeatureManagement(); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + IVariantFeatureManager featureManager = serviceProvider.GetRequiredService(); + + // Test throws missing variants exception + FeatureManagementException e = await Assert.ThrowsAsync(async () => await featureManager.GetVariantAsync("VariantFeatureNoVariants")); + + Assert.Equal(FeatureManagementError.MissingFeatureVariant, e.Error); + + // Test throws invalid variant configuration + e = await Assert.ThrowsAsync(async () => await featureManager.GetVariantAsync("VariantFeatureBothConfigurations")); + + Assert.Equal(FeatureManagementError.InvalidVariantConfiguration, e.Error); + + // Test throws missing allocation + e = await Assert.ThrowsAsync(async () => await featureManager.GetVariantAsync("VariantFeatureNoAllocation")); + + Assert.Equal(FeatureManagementError.MissingAllocation, e.Error); + } + private static void DisableEndpointRouting(MvcOptions options) { #if NET6_0 || NET5_0 || NETCOREAPP3_1 diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index 572fdb8b..c79520be 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -277,6 +277,14 @@ }, "VariantFeatureGroup": { "Allocation": { + "User": [ + { + "Variant": "Small", + "Users": [ + "Jeff" + ] + } + ], "Group": [ { "Variant": "Small", @@ -297,6 +305,54 @@ "Name": "On" } ] + }, + "VariantFeatureNoVariants": { + "Allocation": { + "User": [ + { + "Variant": "Small", + "Users": [ + "Marsha" + ] + } + ] + }, + "Variants": [], + "EnabledFor": [ + { + "Name": "On" + } + ] + }, + "VariantFeatureBothConfigurations": { + "Allocation": { + "DefaultWhenEnabled": "Small" + }, + "Variants": [ + { + "Name": "Small", + "ConfigurationValue": "300px", + "ConfigurationReference": "ShoppingCart:Small" + } + ], + "EnabledFor": [ + { + "Name": "On" + } + ] + }, + "VariantFeatureNoAllocation": { + "Variants": [ + { + "Name": "Small", + "ConfigurationValue": "300px" + } + ], + "EnabledFor": [ + { + "Name": "On" + } + ] } } } From 1da4d2a569daa48e261f46463cbafb85baeb1df6 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 10 Aug 2023 14:49:37 -0500 Subject: [PATCH 50/80] change context accessor logic --- src/Microsoft.FeatureManagement/FeatureManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index d04820ed..de8ceb14 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -384,7 +384,7 @@ private async ValueTask GetFeatureVariantAsync(FeatureDefinition // Ensure targeting can be performed if (context == null) { - _logger.LogWarning("No targeting context available for targeting evaluation."); + _logger.LogWarning($"No instance of {nameof(TargetingContext)} could be found using {nameof(ITargetingContextAccessor)} for targeting evaluation."); } } } From dff60448ec27cc69d84e12995e2ef75d60bcb2e8 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 10 Aug 2023 15:32:35 -0500 Subject: [PATCH 51/80] fix comments for default variants --- .../VariantAllocation/Allocation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.FeatureManagement/VariantAllocation/Allocation.cs b/src/Microsoft.FeatureManagement/VariantAllocation/Allocation.cs index 54a5e5c4..9514073e 100644 --- a/src/Microsoft.FeatureManagement/VariantAllocation/Allocation.cs +++ b/src/Microsoft.FeatureManagement/VariantAllocation/Allocation.cs @@ -11,12 +11,12 @@ namespace Microsoft.FeatureManagement.VariantAllocation public class Allocation { /// - /// The default variant used if the feature is disabled. + /// The default variant used if the feature is enabled and no variant is assigned. /// public string DefaultWhenEnabled { get; set; } /// - /// The default variant used if the feature is disabled and no variant is assigned. + /// The default variant used if the feature is disabled. /// public string DefaultWhenDisabled { get; set; } From adbf40f6a49603262cc7d9fa0e6332e5a1ad742a Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 11 Aug 2023 16:36:00 -0500 Subject: [PATCH 52/80] PR revisions --- .../ConfigurationFeatureDefinitionProvider.cs | 1 - .../FeatureManager.cs | 2 +- .../IVariantFeatureManager.cs | 10 ++++----- .../StatusOverride.cs | 2 +- .../Targeting/TargetingEvaluator.cs | 22 +++++++++---------- src/Microsoft.FeatureManagement/Variant.cs | 2 +- 6 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 81b987f2..795b091d 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -10,7 +10,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using static System.Collections.Specialized.BitVector32; namespace Microsoft.FeatureManagement { diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index de8ceb14..2c2b2ea9 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -442,7 +442,7 @@ private ValueTask AssignVariantAsync(FeatureDefinition featureDe { foreach (Percentile percentile in featureDefinition.Allocation.Percentile) { - if (TargetingEvaluator.IsTargeted(targetingContext, percentile.From, percentile.To, featureDefinition.Allocation.Seed, _assignerOptions.IgnoreCase, featureDefinition.Name)) + if (TargetingEvaluator.IsTargeted(targetingContext, percentile.From, percentile.To, _assignerOptions.IgnoreCase, featureDefinition.Allocation.Seed)) { variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == percentile.Variant); diff --git a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs index e6a3b5a4..e4e4ac15 100644 --- a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs @@ -8,7 +8,7 @@ namespace Microsoft.FeatureManagement { /// - /// Used to evaluate the variant for a feature or whether a feature is enabled or disabled. + /// Used to evaluate the enabled state of a feature and/or get the assigned variant of a feature, if any. /// public interface IVariantFeatureManager { @@ -32,18 +32,18 @@ public interface IVariantFeatureManager /// /// Gets the assigned variant for a specfic feature. /// - /// The name of the feature from which the variant will be assigned. + /// The name of the feature to evaluate. /// The cancellation token to cancel the operation. - /// A variant assigned to the user based on the feature's allocation logic. + /// A variant assigned to the user based on the feature's configured allocation. ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken = default); /// /// Gets the assigned variant for a specfic feature. /// - /// The name of the feature from which the variant will be assigned. + /// The name of the feature to evaluate. /// An instance of used to evaluate which variant the user will be assigned. /// The cancellation token to cancel the operation. - /// A variant assigned to the user based on the feature's allocation logic. + /// A variant assigned to the user based on the feature's configured allocation. ValueTask GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/StatusOverride.cs b/src/Microsoft.FeatureManagement/StatusOverride.cs index 6df84076..ac5c16b1 100644 --- a/src/Microsoft.FeatureManagement/StatusOverride.cs +++ b/src/Microsoft.FeatureManagement/StatusOverride.cs @@ -4,7 +4,7 @@ namespace Microsoft.FeatureManagement { /// - /// Overrides the feature's state with this value when a variant has been assigned. + /// Provides the capability to override whether a feature is considered enabled or disabled when a variant is assigned. /// public enum StatusOverride { diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs index bd6e5c2b..70e62d2b 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs @@ -156,7 +156,7 @@ public static bool IsGroupTargeted( } /// - /// Determine if a targeting context is targeted by presence in a group + /// Determine if a targeting context is targeted by presence in a group and its rollout percentage /// public static bool IsTargeted( ITargetingContext targetingContext, @@ -307,22 +307,13 @@ public static bool TryValidateSettings(TargetingFilterSettings targetingSettings /// /// Determines if a given context id should be targeted based off the provided percentage range /// - public static bool IsTargeted(ITargetingContext targetingContext, double from, double to, string seed, bool ignoreCase, string hint) + public static bool IsTargeted(ITargetingContext targetingContext, double from, double to, bool ignoreCase, string hint) { string userId = ignoreCase ? targetingContext.UserId.ToLower() : targetingContext.UserId; - string contextId; - - if (!string.IsNullOrEmpty(seed)) - { - contextId = $"{userId}\n{seed}"; - } - else - { - contextId = $"{userId}\n{hint}"; - } + string contextId = $"{userId}\n{hint}"; return IsTargeted(contextId, from, to); } @@ -350,6 +341,13 @@ private static bool IsTargeted(string contextId, double from, double to) double contextPercentage = (contextMarker / (double)uint.MaxValue) * 100; + // + // Handle edge case of exact 100 bucket + if (contextPercentage == 100) + { + return to == 100; + } + return contextPercentage >= from && contextPercentage < to; } } diff --git a/src/Microsoft.FeatureManagement/Variant.cs b/src/Microsoft.FeatureManagement/Variant.cs index 1a7dccc1..f69a47ce 100644 --- a/src/Microsoft.FeatureManagement/Variant.cs +++ b/src/Microsoft.FeatureManagement/Variant.cs @@ -16,7 +16,7 @@ public class Variant public string Name { get; set; } /// - /// The configuration value. + /// The configuration of the variant. /// public IConfigurationSection Configuration { get; set; } } From 875a422926cbf055787152d87aed690eac0827ca Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 14 Aug 2023 15:39:30 -0500 Subject: [PATCH 53/80] change class names, PR fixes --- .../{VariantAllocation => }/Allocation.cs | 8 +-- .../ConfigurationFeatureDefinitionProvider.cs | 11 ++- .../FeatureDefinition.cs | 11 ++- .../FeatureManagementError.cs | 2 +- .../FeatureManager.cs | 71 +++++++++---------- .../{Status.cs => FeatureStatus.cs} | 2 +- .../Group.cs => GroupAllocation.cs} | 4 +- .../IVariantFeatureManager.cs | 8 +-- .../Percentile.cs => PercentileAllocation.cs} | 4 +- .../Targeting/TargetingEvaluator.cs | 32 +++++++-- .../User.cs => UserAllocation.cs} | 4 +- .../VariantConfigurationSection.cs | 27 +++---- ...FeatureVariant.cs => VariantDefinition.cs} | 2 +- .../FeatureManagement.cs | 35 ++++----- 14 files changed, 118 insertions(+), 103 deletions(-) rename src/Microsoft.FeatureManagement/{VariantAllocation => }/Allocation.cs (82%) rename src/Microsoft.FeatureManagement/{Status.cs => FeatureStatus.cs} (94%) rename src/Microsoft.FeatureManagement/{VariantAllocation/Group.cs => GroupAllocation.cs} (86%) rename src/Microsoft.FeatureManagement/{VariantAllocation/Percentile.cs => PercentileAllocation.cs} (88%) rename src/Microsoft.FeatureManagement/{VariantAllocation/User.cs => UserAllocation.cs} (86%) rename src/Microsoft.FeatureManagement/{FeatureVariant.cs => VariantDefinition.cs} (96%) diff --git a/src/Microsoft.FeatureManagement/VariantAllocation/Allocation.cs b/src/Microsoft.FeatureManagement/Allocation.cs similarity index 82% rename from src/Microsoft.FeatureManagement/VariantAllocation/Allocation.cs rename to src/Microsoft.FeatureManagement/Allocation.cs index 9514073e..b10ce33d 100644 --- a/src/Microsoft.FeatureManagement/VariantAllocation/Allocation.cs +++ b/src/Microsoft.FeatureManagement/Allocation.cs @@ -3,7 +3,7 @@ // using System.Collections.Generic; -namespace Microsoft.FeatureManagement.VariantAllocation +namespace Microsoft.FeatureManagement { /// /// The definition of how variants are allocated for a feature. @@ -23,17 +23,17 @@ public class Allocation /// /// Describes a mapping of user ids to variants. /// - public IEnumerable User { get; set; } + public IEnumerable User { get; set; } /// /// Describes a mapping of group names to variants. /// - public IEnumerable Group { get; set; } + public IEnumerable Group { get; set; } /// /// Allocates percentiles of user base to variants. /// - public IEnumerable Percentile { get; set; } + public IEnumerable Percentile { get; set; } /// /// Maps users to the same percentile across multiple feature flags. diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 795b091d..790403be 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -3,7 +3,6 @@ // using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Primitives; -using Microsoft.FeatureManagement.VariantAllocation; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -135,9 +134,9 @@ We support */ RequirementType requirementType = RequirementType.Any; - Status status = Status.Conditional; + FeatureStatus status = FeatureStatus.Conditional; Allocation allocation = null; - List variants = null; + List variants = null; var enabledFor = new List(); @@ -192,13 +191,13 @@ We support } IEnumerable variantsSections = configurationSection.GetSection(nameof(FeatureDefinition.Variants)).GetChildren(); - variants = new List(); + variants = new List(); foreach (IConfigurationSection section in variantsSections) { - if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[nameof(FeatureVariant.Name)])) + if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[nameof(VariantDefinition.Name)])) { - FeatureVariant variant = new FeatureVariant(); + VariantDefinition variant = new VariantDefinition(); section.Bind(variant); variants.Add(variant); } diff --git a/src/Microsoft.FeatureManagement/FeatureDefinition.cs b/src/Microsoft.FeatureManagement/FeatureDefinition.cs index bab3b84e..53819fc6 100644 --- a/src/Microsoft.FeatureManagement/FeatureDefinition.cs +++ b/src/Microsoft.FeatureManagement/FeatureDefinition.cs @@ -3,7 +3,6 @@ // using System.Collections.Generic; using System.Linq; -using Microsoft.FeatureManagement.VariantAllocation; namespace Microsoft.FeatureManagement { @@ -29,10 +28,10 @@ public class FeatureDefinition public RequirementType RequirementType { get; set; } = RequirementType.Any; /// - /// When set to , this feature will always be considered disabled regardless of the rest of the feature definition. - /// The default value is . + /// When set to , this feature will always be considered disabled regardless of the rest of the feature definition. + /// The default value is . /// - public Status Status { get; set; } = Status.Conditional; + public FeatureStatus Status { get; set; } = FeatureStatus.Conditional; /// /// Describes how variants should be allocated. @@ -40,8 +39,8 @@ public class FeatureDefinition public Allocation Allocation { get; set; } /// - /// A list of feature variants that specify a configuration to return when assigned. + /// A list of variant definitions that specify a configuration to return when assigned. /// - public IEnumerable Variants { get; set; } = Enumerable.Empty(); + public IEnumerable Variants { get; set; } = Enumerable.Empty(); } } diff --git a/src/Microsoft.FeatureManagement/FeatureManagementError.cs b/src/Microsoft.FeatureManagement/FeatureManagementError.cs index 615411c0..7b7dc4dd 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementError.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementError.cs @@ -36,7 +36,7 @@ public enum FeatureManagementError /// /// A feature does not have any feature variants registered when attempting to resolve the variant. /// - MissingFeatureVariant, + MissingVariantDefinition, /// /// A feature variant specifies both a configuration reference and configuration value, or there is no valid configuration to reference. diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 2c2b2ea9..08448081 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -7,7 +7,6 @@ using Microsoft.Extensions.Options; using Microsoft.FeatureManagement.FeatureFilters; using Microsoft.FeatureManagement.Targeting; -using Microsoft.FeatureManagement.VariantAllocation; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -132,7 +131,7 @@ private async Task IsEnabledAsync(string feature, TContext appCo // // Treat an empty list of enabled filters or if status is disabled as a disabled feature - if (featureDefinition.EnabledFor == null || !featureDefinition.EnabledFor.Any() || featureDefinition.Status == Status.Disabled) + if (featureDefinition.EnabledFor == null || !featureDefinition.EnabledFor.Any() || featureDefinition.Status == FeatureStatus.Disabled) { enabled = false; } @@ -225,17 +224,17 @@ await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) } } - if (!ignoreVariant && (featureDefinition.Variants?.Any() ?? false) && featureDefinition.Allocation != null && featureDefinition.Status != Status.Disabled) + if (!ignoreVariant && (featureDefinition.Variants?.Any() ?? false) && featureDefinition.Allocation != null && featureDefinition.Status != FeatureStatus.Disabled) { - FeatureVariant featureVariant = await GetFeatureVariantAsync(featureDefinition, appContext as TargetingContext, useAppContext, enabled, CancellationToken.None); + VariantDefinition variantDefinition = await GetVariantDefinitionAsync(featureDefinition, appContext as TargetingContext, useAppContext, enabled, CancellationToken.None); - if (featureVariant != null) + if (variantDefinition != null) { - if (featureVariant.StatusOverride == StatusOverride.Enabled) + if (variantDefinition.StatusOverride == StatusOverride.Enabled) { enabled = true; } - else if (featureVariant.StatusOverride == StatusOverride.Disabled) + else if (variantDefinition.StatusOverride == StatusOverride.Disabled) { enabled = false; } @@ -307,31 +306,31 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex if (!featureDefinition.Variants?.Any() ?? false) { throw new FeatureManagementException( - FeatureManagementError.MissingFeatureVariant, + FeatureManagementError.MissingVariantDefinition, $"No variants are registered for the feature {feature}"); } - FeatureVariant featureVariant; + VariantDefinition variantDefinition; bool isFeatureEnabled = await IsEnabledAsync(feature, context, true, cancellationToken).ConfigureAwait(false); - featureVariant = await GetFeatureVariantAsync(featureDefinition, context, useContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); + variantDefinition = await GetVariantDefinitionAsync(featureDefinition, context, useContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); - if (featureVariant == null) + if (variantDefinition == null) { return null; } IConfigurationSection variantConfiguration = null; - bool configValueSet = !string.IsNullOrEmpty(featureVariant.ConfigurationValue); - bool configReferenceValueSet = !string.IsNullOrEmpty(featureVariant.ConfigurationReference); + bool configValueSet = !string.IsNullOrEmpty(variantDefinition.ConfigurationValue); + bool configReferenceValueSet = !string.IsNullOrEmpty(variantDefinition.ConfigurationReference); if (configValueSet && configReferenceValueSet) { throw new FeatureManagementException( FeatureManagementError.InvalidVariantConfiguration, - $"Both ConfigurationValue and ConfigurationReference are specified for the variant {featureVariant.Name} in feature {feature}"); + $"Both ConfigurationValue and ConfigurationReference are specified for the variant {variantDefinition.Name} in feature {feature}"); } else if (configReferenceValueSet) { @@ -339,34 +338,34 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex { throw new FeatureManagementException( FeatureManagementError.InvalidVariantConfiguration, - $"Cannot use {nameof(featureVariant.ConfigurationReference)} if no instance of {nameof(IConfiguration)} is present."); + $"Cannot use {nameof(variantDefinition.ConfigurationReference)} if no instance of {nameof(IConfiguration)} is present."); } - variantConfiguration = _configuration.GetSection(featureVariant.ConfigurationReference); + variantConfiguration = _configuration.GetSection(variantDefinition.ConfigurationReference); } else if (configValueSet) { - VariantConfigurationSection section = new VariantConfigurationSection(featureVariant.Name, "", featureVariant.ConfigurationValue); + VariantConfigurationSection section = new VariantConfigurationSection(variantDefinition.Name, "", variantDefinition.ConfigurationValue); variantConfiguration = section; } Variant returnVariant = new Variant() { - Name = featureVariant.Name, + Name = variantDefinition.Name, Configuration = variantConfiguration }; return returnVariant; } - private async ValueTask GetFeatureVariantAsync(FeatureDefinition featureDefinition, TargetingContext context, bool useContext, bool isFeatureEnabled, CancellationToken cancellationToken) + private async ValueTask GetVariantDefinitionAsync(FeatureDefinition featureDefinition, TargetingContext context, bool useContext, bool isFeatureEnabled, CancellationToken cancellationToken) { if (!isFeatureEnabled) { - return ResolveDefaultFeatureVariant(featureDefinition, isFeatureEnabled); + return ResolveDefaultVariantDefinition(featureDefinition, isFeatureEnabled); } - FeatureVariant featureVariant = null; + VariantDefinition variantDefinition = null; if (!useContext) { @@ -391,24 +390,24 @@ private async ValueTask GetFeatureVariantAsync(FeatureDefinition if (context != null) { - featureVariant = await AssignVariantAsync(featureDefinition, context, cancellationToken).ConfigureAwait(false); + variantDefinition = await AssignVariantAsync(featureDefinition, context, cancellationToken).ConfigureAwait(false); } - if (featureVariant == null) + if (variantDefinition == null) { - featureVariant = ResolveDefaultFeatureVariant(featureDefinition, isFeatureEnabled); + variantDefinition = ResolveDefaultVariantDefinition(featureDefinition, isFeatureEnabled); } - return featureVariant; + return variantDefinition; } - private ValueTask AssignVariantAsync(FeatureDefinition featureDefinition, TargetingContext targetingContext, CancellationToken cancellationToken) + private ValueTask AssignVariantAsync(FeatureDefinition featureDefinition, TargetingContext targetingContext, CancellationToken cancellationToken) { - FeatureVariant variant = null; + VariantDefinition variant = null; if (featureDefinition.Allocation.User != null) { - foreach (User user in featureDefinition.Allocation.User) + foreach (UserAllocation user in featureDefinition.Allocation.User) { if (TargetingEvaluator.IsTargeted(targetingContext, user.Users, _assignerOptions.IgnoreCase)) { @@ -416,7 +415,7 @@ private ValueTask AssignVariantAsync(FeatureDefinition featureDe if (!string.IsNullOrEmpty(variant.Name)) { - return new ValueTask(variant); + return new ValueTask(variant); } } } @@ -424,7 +423,7 @@ private ValueTask AssignVariantAsync(FeatureDefinition featureDe if (featureDefinition.Allocation.Group != null) { - foreach (Group group in featureDefinition.Allocation.Group) + foreach (GroupAllocation group in featureDefinition.Allocation.Group) { if (TargetingEvaluator.IsGroupTargeted(targetingContext, group.Groups, _assignerOptions.IgnoreCase)) { @@ -432,7 +431,7 @@ private ValueTask AssignVariantAsync(FeatureDefinition featureDe if (!string.IsNullOrEmpty(variant.Name)) { - return new ValueTask(variant); + return new ValueTask(variant); } } } @@ -440,7 +439,7 @@ private ValueTask AssignVariantAsync(FeatureDefinition featureDe if (featureDefinition.Allocation.Percentile != null) { - foreach (Percentile percentile in featureDefinition.Allocation.Percentile) + foreach (PercentileAllocation percentile in featureDefinition.Allocation.Percentile) { if (TargetingEvaluator.IsTargeted(targetingContext, percentile.From, percentile.To, _assignerOptions.IgnoreCase, featureDefinition.Allocation.Seed)) { @@ -448,22 +447,22 @@ private ValueTask AssignVariantAsync(FeatureDefinition featureDe if (!string.IsNullOrEmpty(variant.Name)) { - return new ValueTask(variant); + return new ValueTask(variant); } } } } - return new ValueTask(variant); + return new ValueTask(variant); } - private FeatureVariant ResolveDefaultFeatureVariant(FeatureDefinition featureDefinition, bool isFeatureEnabled) + private VariantDefinition ResolveDefaultVariantDefinition(FeatureDefinition featureDefinition, bool isFeatureEnabled) { string defaultVariantPath = isFeatureEnabled ? featureDefinition.Allocation.DefaultWhenEnabled : featureDefinition.Allocation.DefaultWhenDisabled; if (!string.IsNullOrEmpty(defaultVariantPath)) { - FeatureVariant defaultVariant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == defaultVariantPath); + VariantDefinition defaultVariant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == defaultVariantPath); if (!string.IsNullOrEmpty(defaultVariant.Name)) { diff --git a/src/Microsoft.FeatureManagement/Status.cs b/src/Microsoft.FeatureManagement/FeatureStatus.cs similarity index 94% rename from src/Microsoft.FeatureManagement/Status.cs rename to src/Microsoft.FeatureManagement/FeatureStatus.cs index 0a714afb..c3c81749 100644 --- a/src/Microsoft.FeatureManagement/Status.cs +++ b/src/Microsoft.FeatureManagement/FeatureStatus.cs @@ -6,7 +6,7 @@ namespace Microsoft.FeatureManagement /// /// Describes how a feature's state will be evaluated. /// - public enum Status + public enum FeatureStatus { /// /// The state of the feature is conditional on the rest of its definition. diff --git a/src/Microsoft.FeatureManagement/VariantAllocation/Group.cs b/src/Microsoft.FeatureManagement/GroupAllocation.cs similarity index 86% rename from src/Microsoft.FeatureManagement/VariantAllocation/Group.cs rename to src/Microsoft.FeatureManagement/GroupAllocation.cs index 558dc69d..6787f8c8 100644 --- a/src/Microsoft.FeatureManagement/VariantAllocation/Group.cs +++ b/src/Microsoft.FeatureManagement/GroupAllocation.cs @@ -4,12 +4,12 @@ using System.Collections.Generic; -namespace Microsoft.FeatureManagement.VariantAllocation +namespace Microsoft.FeatureManagement { /// /// The definition of a group allocation. /// - public class Group + public class GroupAllocation { /// /// The name of the variant. diff --git a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs index e4e4ac15..10395327 100644 --- a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs @@ -18,7 +18,7 @@ public interface IVariantFeatureManager /// The name of the feature to check. /// The cancellation token to cancel the operation. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature, CancellationToken cancellationToken = default); + Task IsEnabledAsync(string feature, CancellationToken cancellationToken); /// /// Checks whether a given feature is enabled. @@ -27,7 +27,7 @@ public interface IVariantFeatureManager /// 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. /// True if the feature is enabled, otherwise false. - Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken = default); + Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken); /// /// Gets the assigned variant for a specfic feature. @@ -35,7 +35,7 @@ public interface IVariantFeatureManager /// The name of the feature to evaluate. /// The cancellation token to cancel the operation. /// A variant assigned to the user based on the feature's configured allocation. - ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken = default); + ValueTask GetVariantAsync(string feature, CancellationToken cancellationToken); /// /// Gets the assigned variant for a specfic feature. @@ -44,6 +44,6 @@ public interface IVariantFeatureManager /// An instance of used to evaluate which variant the user will be assigned. /// The cancellation token to cancel the operation. /// A variant assigned to the user based on the feature's configured allocation. - ValueTask GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken = default); + ValueTask GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken); } } diff --git a/src/Microsoft.FeatureManagement/VariantAllocation/Percentile.cs b/src/Microsoft.FeatureManagement/PercentileAllocation.cs similarity index 88% rename from src/Microsoft.FeatureManagement/VariantAllocation/Percentile.cs rename to src/Microsoft.FeatureManagement/PercentileAllocation.cs index 5e238936..44d9015c 100644 --- a/src/Microsoft.FeatureManagement/VariantAllocation/Percentile.cs +++ b/src/Microsoft.FeatureManagement/PercentileAllocation.cs @@ -2,12 +2,12 @@ // Licensed under the MIT license. // -namespace Microsoft.FeatureManagement.VariantAllocation +namespace Microsoft.FeatureManagement { /// /// The definition of a percentile allocation. /// - public class Percentile + public class PercentileAllocation { /// /// The name of the variant. diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs index 70e62d2b..8c32a9e7 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs @@ -17,6 +17,9 @@ private static StringComparison GetComparisonType(bool ignoreCase) => StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + const string OutOfRange = "The value is out of the accepted range."; + const string RequiredParameter = "Value cannot be null."; + /// /// Checks if a provided targeting context should be targeted given targeting settings. /// @@ -245,10 +248,6 @@ public static bool IsTargeted( /// True if the provided settings are valid. False if the provided settings are invalid. public static bool TryValidateSettings(TargetingFilterSettings targetingSettings, out string paramName, out string reason) { - const string OutOfRange = "The value is out of the accepted range."; - - const string RequiredParameter = "Value cannot be null."; - paramName = null; reason = null; @@ -309,6 +308,31 @@ public static bool TryValidateSettings(TargetingFilterSettings targetingSettings /// public static bool IsTargeted(ITargetingContext targetingContext, double from, double to, bool ignoreCase, string hint) { + if (targetingContext == null) + { + throw new ArgumentNullException(nameof(targetingContext)); + } + + if (string.IsNullOrEmpty(hint)) + { + throw new ArgumentNullException(nameof(hint)); + } + + if (from < 0 || from > 100) + { + throw new ArgumentException(OutOfRange, nameof(from)); + } + + if (to < 0 || to > 100) + { + throw new ArgumentException(OutOfRange, nameof(to)); + } + + if (from > to) + { + throw new ArgumentException($"Double {nameof(from)} cannot be larger than double {nameof(to)}."); + } + string userId = ignoreCase ? targetingContext.UserId.ToLower() : targetingContext.UserId; diff --git a/src/Microsoft.FeatureManagement/VariantAllocation/User.cs b/src/Microsoft.FeatureManagement/UserAllocation.cs similarity index 86% rename from src/Microsoft.FeatureManagement/VariantAllocation/User.cs rename to src/Microsoft.FeatureManagement/UserAllocation.cs index b6723783..9443de7a 100644 --- a/src/Microsoft.FeatureManagement/VariantAllocation/User.cs +++ b/src/Microsoft.FeatureManagement/UserAllocation.cs @@ -4,12 +4,12 @@ using System.Collections.Generic; -namespace Microsoft.FeatureManagement.VariantAllocation +namespace Microsoft.FeatureManagement { /// /// The definition of a user allocation. /// - public class User + public class UserAllocation { /// /// The name of the variant. diff --git a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs index a2008627..20dfa58f 100644 --- a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs +++ b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs @@ -2,8 +2,8 @@ // Licensed under the MIT license. // using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Configuration.Memory; using Microsoft.Extensions.Primitives; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -12,15 +12,16 @@ namespace Microsoft.FeatureManagement { internal class VariantConfigurationSection : IConfigurationSection { - private readonly ConfigurationRoot _root; + private readonly string _key; private readonly string _path; - private string _key; public VariantConfigurationSection(string key, string path, string value) { - MemoryConfigurationSource source = new MemoryConfigurationSource(); + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException(nameof(key)); + } - _root = new ConfigurationRoot(new List { new MemoryConfigurationProvider(source) }); _path = path; _key = key; Value = value; @@ -30,11 +31,11 @@ public string this[string key] { get { - return _root[ConfigurationPath.Combine(Path, key)]; + throw new NotImplementedException(); } set { - _root[ConfigurationPath.Combine(Path, key)] = value; + throw new NotImplementedException(); } } @@ -42,17 +43,7 @@ public string this[string key] public string Path => _path; - public string Value - { - get - { - return _root[Path]; - } - set - { - _root[Path] = value; - } - } + public string Value { get; set; } public IEnumerable GetChildren() { diff --git a/src/Microsoft.FeatureManagement/FeatureVariant.cs b/src/Microsoft.FeatureManagement/VariantDefinition.cs similarity index 96% rename from src/Microsoft.FeatureManagement/FeatureVariant.cs rename to src/Microsoft.FeatureManagement/VariantDefinition.cs index c764ca5f..4ea42fc6 100644 --- a/src/Microsoft.FeatureManagement/FeatureVariant.cs +++ b/src/Microsoft.FeatureManagement/VariantDefinition.cs @@ -7,7 +7,7 @@ namespace Microsoft.FeatureManagement /// /// The definition for a variant of a feature. /// - public class FeatureVariant + public class VariantDefinition { /// /// The name of the variant. diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index b3850560..fa10f3d6 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -15,6 +15,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -976,6 +977,7 @@ public async Task UsesVariants() ServiceProvider serviceProvider = services.BuildServiceProvider(); IVariantFeatureManager featureManager = serviceProvider.GetRequiredService(); + CancellationToken cancellationToken = CancellationToken.None; targetingContextAccessor.Current = new TargetingContext { @@ -984,44 +986,44 @@ public async Task UsesVariants() }; // Test StatusOverride and Percentile with Seed - Variant variant = await featureManager.GetVariantAsync("VariantFeaturePercentileOn"); + Variant variant = await featureManager.GetVariantAsync("VariantFeaturePercentileOn", cancellationToken); Assert.Equal("Big", variant.Name); Assert.Equal("green", variant.Configuration["Color"]); - Assert.False(await featureManager.IsEnabledAsync("VariantFeaturePercentileOn")); + Assert.False(await featureManager.IsEnabledAsync("VariantFeaturePercentileOn", cancellationToken)); - variant = await featureManager.GetVariantAsync("VariantFeaturePercentileOff"); + variant = await featureManager.GetVariantAsync("VariantFeaturePercentileOff", cancellationToken); Assert.Null(variant); - Assert.True(await featureManager.IsEnabledAsync("VariantFeaturePercentileOff")); + Assert.True(await featureManager.IsEnabledAsync("VariantFeaturePercentileOff", cancellationToken)); // Test Status = Disabled - variant = await featureManager.GetVariantAsync("VariantFeatureStatusDisabled"); + variant = await featureManager.GetVariantAsync("VariantFeatureStatusDisabled", cancellationToken); Assert.Equal("Small", variant.Name); Assert.Equal("300px", variant.Configuration.Value); - Assert.False(await featureManager.IsEnabledAsync("VariantFeatureStatusDisabled")); + Assert.False(await featureManager.IsEnabledAsync("VariantFeatureStatusDisabled", cancellationToken)); // Test DefaultWhenEnabled - variant = await featureManager.GetVariantAsync("VariantFeatureDefaultEnabled"); + variant = await featureManager.GetVariantAsync("VariantFeatureDefaultEnabled", cancellationToken); Assert.Equal("Medium", variant.Name); Assert.Equal("450px", variant.Configuration.Value); - Assert.True(await featureManager.IsEnabledAsync("VariantFeatureDefaultEnabled")); + Assert.True(await featureManager.IsEnabledAsync("VariantFeatureDefaultEnabled", cancellationToken)); // Test User allocation - variant = await featureManager.GetVariantAsync("VariantFeatureUser"); + variant = await featureManager.GetVariantAsync("VariantFeatureUser", cancellationToken); Assert.Equal("Small", variant.Name); Assert.Equal("300px", variant.Configuration.Value); - Assert.True(await featureManager.IsEnabledAsync("VariantFeatureUser")); + Assert.True(await featureManager.IsEnabledAsync("VariantFeatureUser", cancellationToken)); // Test Group allocation - variant = await featureManager.GetVariantAsync("VariantFeatureGroup"); + variant = await featureManager.GetVariantAsync("VariantFeatureGroup", cancellationToken); Assert.Equal("Small", variant.Name); Assert.Equal("300px", variant.Configuration.Value); - Assert.True(await featureManager.IsEnabledAsync("VariantFeatureGroup")); + Assert.True(await featureManager.IsEnabledAsync("VariantFeatureGroup", cancellationToken)); } [Fact] @@ -1039,19 +1041,20 @@ public async Task VariantsExceptions() ServiceProvider serviceProvider = services.BuildServiceProvider(); IVariantFeatureManager featureManager = serviceProvider.GetRequiredService(); + CancellationToken cancellationToken = CancellationToken.None; // Test throws missing variants exception - FeatureManagementException e = await Assert.ThrowsAsync(async () => await featureManager.GetVariantAsync("VariantFeatureNoVariants")); + FeatureManagementException e = await Assert.ThrowsAsync(async () => await featureManager.GetVariantAsync("VariantFeatureNoVariants", cancellationToken)); - Assert.Equal(FeatureManagementError.MissingFeatureVariant, e.Error); + Assert.Equal(FeatureManagementError.MissingVariantDefinition, e.Error); // Test throws invalid variant configuration - e = await Assert.ThrowsAsync(async () => await featureManager.GetVariantAsync("VariantFeatureBothConfigurations")); + e = await Assert.ThrowsAsync(async () => await featureManager.GetVariantAsync("VariantFeatureBothConfigurations", cancellationToken)); Assert.Equal(FeatureManagementError.InvalidVariantConfiguration, e.Error); // Test throws missing allocation - e = await Assert.ThrowsAsync(async () => await featureManager.GetVariantAsync("VariantFeatureNoAllocation")); + e = await Assert.ThrowsAsync(async () => await featureManager.GetVariantAsync("VariantFeatureNoAllocation", cancellationToken)); Assert.Equal(FeatureManagementError.MissingAllocation, e.Error); } From c50c96e66ecdff0c9aff264edca062eba8e4690d Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 14 Aug 2023 15:49:07 -0500 Subject: [PATCH 54/80] fix edge case percentage targeting --- .../Targeting/TargetingEvaluator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs index 8c32a9e7..312f5f18 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs @@ -367,9 +367,9 @@ private static bool IsTargeted(string contextId, double from, double to) // // Handle edge case of exact 100 bucket - if (contextPercentage == 100) + if (to == 100) { - return to == 100; + return contextPercentage >= from; } return contextPercentage >= from && contextPercentage < to; From 95e6459502b188ec56debf6896f50625071dc27f Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 15 Aug 2023 18:21:22 -0500 Subject: [PATCH 55/80] rename allocation classes, remove exceptions and add warning logs, prioritize inline value for variant config, more revisions --- .../{ => Allocation}/Allocation.cs | 0 .../{ => Allocation}/GroupAllocation.cs | 0 .../{ => Allocation}/PercentileAllocation.cs | 0 .../{ => Allocation}/UserAllocation.cs | 0 .../FeatureManagementError.cs | 12 +- .../FeatureManager.cs | 146 +++++++++++------- .../Microsoft.FeatureManagement.csproj | 1 + .../Targeting/TargetingEvaluator.cs | 32 ++-- .../VariantConfigurationSection.cs | 14 +- .../FeatureManagement.cs | 20 +-- .../Tests.FeatureManagement/appsettings.json | 2 +- 11 files changed, 122 insertions(+), 105 deletions(-) rename src/Microsoft.FeatureManagement/{ => Allocation}/Allocation.cs (100%) rename src/Microsoft.FeatureManagement/{ => Allocation}/GroupAllocation.cs (100%) rename src/Microsoft.FeatureManagement/{ => Allocation}/PercentileAllocation.cs (100%) rename src/Microsoft.FeatureManagement/{ => Allocation}/UserAllocation.cs (100%) diff --git a/src/Microsoft.FeatureManagement/Allocation.cs b/src/Microsoft.FeatureManagement/Allocation/Allocation.cs similarity index 100% rename from src/Microsoft.FeatureManagement/Allocation.cs rename to src/Microsoft.FeatureManagement/Allocation/Allocation.cs diff --git a/src/Microsoft.FeatureManagement/GroupAllocation.cs b/src/Microsoft.FeatureManagement/Allocation/GroupAllocation.cs similarity index 100% rename from src/Microsoft.FeatureManagement/GroupAllocation.cs rename to src/Microsoft.FeatureManagement/Allocation/GroupAllocation.cs diff --git a/src/Microsoft.FeatureManagement/PercentileAllocation.cs b/src/Microsoft.FeatureManagement/Allocation/PercentileAllocation.cs similarity index 100% rename from src/Microsoft.FeatureManagement/PercentileAllocation.cs rename to src/Microsoft.FeatureManagement/Allocation/PercentileAllocation.cs diff --git a/src/Microsoft.FeatureManagement/UserAllocation.cs b/src/Microsoft.FeatureManagement/Allocation/UserAllocation.cs similarity index 100% rename from src/Microsoft.FeatureManagement/UserAllocation.cs rename to src/Microsoft.FeatureManagement/Allocation/UserAllocation.cs diff --git a/src/Microsoft.FeatureManagement/FeatureManagementError.cs b/src/Microsoft.FeatureManagement/FeatureManagementError.cs index 7b7dc4dd..a27c71f7 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementError.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementError.cs @@ -33,19 +33,9 @@ public enum FeatureManagementError /// InvalidConfigurationSetting, - /// - /// A feature does not have any feature variants registered when attempting to resolve the variant. - /// - MissingVariantDefinition, - /// /// A feature variant specifies both a configuration reference and configuration value, or there is no valid configuration to reference. /// - InvalidVariantConfiguration, - - /// - /// A feature does not have allocation defined when attempting to resolve the variant. - /// - MissingAllocation + InvalidVariantConfiguration } } diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 08448081..9f04d1ce 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -13,6 +13,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Xunit; namespace Microsoft.FeatureManagement { @@ -68,27 +69,27 @@ public FeatureManager( public Task IsEnabledAsync(string feature) { - return IsEnabledAsync(feature, null, false, false, CancellationToken.None); + return IsEnabledAsync(feature, appContext: null, useAppContext: false, ignoreVariant: false, CancellationToken.None); } public Task IsEnabledAsync(string feature, TContext appContext) { - return IsEnabledAsync(feature, appContext, true, false, CancellationToken.None); + return IsEnabledAsync(feature, appContext, useAppContext: true, ignoreVariant: false, CancellationToken.None); } public Task IsEnabledAsync(string feature, CancellationToken cancellationToken) { - return IsEnabledAsync(feature, null, false, false, cancellationToken); + return IsEnabledAsync(feature, appContext: null, useAppContext: false, ignoreVariant: false, cancellationToken); } public Task IsEnabledAsync(string feature, TContext appContext, CancellationToken cancellationToken) { - return IsEnabledAsync(feature, appContext, true, false, cancellationToken); + return IsEnabledAsync(feature, appContext, useAppContext: true, ignoreVariant: false, cancellationToken); } private Task IsEnabledAsync(string feature, TContext appContext, bool ignoreVariant, CancellationToken cancellationToken) { - return IsEnabledAsync(feature, appContext, true, ignoreVariant, cancellationToken); + return IsEnabledAsync(feature, appContext, useAppContext: true, ignoreVariant, cancellationToken); } public async IAsyncEnumerable GetFeatureNamesAsync() @@ -226,7 +227,7 @@ await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) if (!ignoreVariant && (featureDefinition.Variants?.Any() ?? false) && featureDefinition.Allocation != null && featureDefinition.Status != FeatureStatus.Disabled) { - VariantDefinition variantDefinition = await GetVariantDefinitionAsync(featureDefinition, appContext as TargetingContext, useAppContext, enabled, CancellationToken.None); + VariantDefinition variantDefinition = await GetVariantDefinitionAsync(featureDefinition, appContext as TargetingContext, useAppContext, enabled, cancellationToken); if (variantDefinition != null) { @@ -270,7 +271,7 @@ public ValueTask GetVariantAsync(string feature, CancellationToken canc throw new ArgumentNullException(nameof(feature)); } - return GetVariantAsync(feature, null, false, cancellationToken); + return GetVariantAsync(feature, context: null, useContext: false, cancellationToken); } public ValueTask GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken) @@ -280,7 +281,12 @@ public ValueTask GetVariantAsync(string feature, TargetingContext conte throw new ArgumentNullException(nameof(feature)); } - return GetVariantAsync(feature, context, true, cancellationToken); + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return GetVariantAsync(feature, context, useContext: true, cancellationToken); } private async ValueTask GetVariantAsync(string feature, TargetingContext context, bool useContext, CancellationToken cancellationToken) @@ -291,28 +297,24 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex if (featureDefinition == null) { - throw new FeatureManagementException( - FeatureManagementError.MissingFeature, - $"The feature declaration for the feature '{feature}' was not found."); - } + string errorMessage = $"The feature declaration for the feature '{feature}' was not found."; - if (featureDefinition.Allocation == null) - { - throw new FeatureManagementException( - FeatureManagementError.MissingAllocation, - $"No allocation is defined for the feature {featureDefinition.Name}"); + if (!_options.IgnoreMissingFeatures) + { + throw new FeatureManagementException(FeatureManagementError.MissingFeature, errorMessage); + } + + _logger.LogWarning(errorMessage); } - if (!featureDefinition.Variants?.Any() ?? false) + if (featureDefinition.Allocation == null || (!featureDefinition.Variants?.Any() ?? false)) { - throw new FeatureManagementException( - FeatureManagementError.MissingVariantDefinition, - $"No variants are registered for the feature {feature}"); + return null; } VariantDefinition variantDefinition; - bool isFeatureEnabled = await IsEnabledAsync(feature, context, true, cancellationToken).ConfigureAwait(false); + bool isFeatureEnabled = await IsEnabledAsync(feature, context, ignoreVariant: true, cancellationToken).ConfigureAwait(false); variantDefinition = await GetVariantDefinitionAsync(featureDefinition, context, useContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); @@ -324,41 +326,40 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex IConfigurationSection variantConfiguration = null; bool configValueSet = !string.IsNullOrEmpty(variantDefinition.ConfigurationValue); - bool configReferenceValueSet = !string.IsNullOrEmpty(variantDefinition.ConfigurationReference); + bool configReferenceSet = !string.IsNullOrEmpty(variantDefinition.ConfigurationReference); - if (configValueSet && configReferenceValueSet) + if (configValueSet) { - throw new FeatureManagementException( - FeatureManagementError.InvalidVariantConfiguration, - $"Both ConfigurationValue and ConfigurationReference are specified for the variant {variantDefinition.Name} in feature {feature}"); + variantConfiguration = new VariantConfigurationSection(nameof(variantDefinition.ConfigurationValue), variantDefinition.ConfigurationValue); } - else if (configReferenceValueSet) + else if (configReferenceSet) { if (_configuration == null) { - throw new FeatureManagementException( - FeatureManagementError.InvalidVariantConfiguration, - $"Cannot use {nameof(variantDefinition.ConfigurationReference)} if no instance of {nameof(IConfiguration)} is present."); - } + _logger.LogWarning($"Cannot use {nameof(variantDefinition.ConfigurationReference)} as no instance of {nameof(IConfiguration)} is present."); - variantConfiguration = _configuration.GetSection(variantDefinition.ConfigurationReference); - } - else if (configValueSet) - { - VariantConfigurationSection section = new VariantConfigurationSection(variantDefinition.Name, "", variantDefinition.ConfigurationValue); - variantConfiguration = section; + return null; + } + else + { + variantConfiguration = _configuration.GetSection(variantDefinition.ConfigurationReference); + } } - Variant returnVariant = new Variant() + + return new Variant() { Name = variantDefinition.Name, Configuration = variantConfiguration }; - - return returnVariant; } - private async ValueTask GetVariantDefinitionAsync(FeatureDefinition featureDefinition, TargetingContext context, bool useContext, bool isFeatureEnabled, CancellationToken cancellationToken) + private async ValueTask GetVariantDefinitionAsync( + FeatureDefinition featureDefinition, + TargetingContext context, + bool useContext, + bool isFeatureEnabled, + CancellationToken cancellationToken) { if (!isFeatureEnabled) { @@ -409,14 +410,23 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur { foreach (UserAllocation user in featureDefinition.Allocation.User) { - if (TargetingEvaluator.IsTargeted(targetingContext, user.Users, _assignerOptions.IgnoreCase)) + if (TargetingEvaluator.IsTargeted(targetingContext.UserId, user.Users, _assignerOptions.IgnoreCase)) { - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == user.Variant); - - if (!string.IsNullOrEmpty(variant.Name)) + if (string.IsNullOrEmpty(user.Variant)) { - return new ValueTask(variant); + _logger.LogWarning($"Missing variant name for {nameof(featureDefinition.Allocation.User)} allocation in feature {featureDefinition.Name}"); + + return new ValueTask((VariantDefinition)null); } + + Assert.NotNull(featureDefinition.Variants); + + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == user.Variant); + + return new ValueTask( + featureDefinition + .Variants + .FirstOrDefault((variant) => variant.Name == user.Variant)); } } } @@ -425,14 +435,23 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur { foreach (GroupAllocation group in featureDefinition.Allocation.Group) { - if (TargetingEvaluator.IsGroupTargeted(targetingContext, group.Groups, _assignerOptions.IgnoreCase)) + if (TargetingEvaluator.IsTargeted(targetingContext.Groups, group.Groups, _assignerOptions.IgnoreCase)) { - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == group.Variant); - - if (!string.IsNullOrEmpty(variant.Name)) + if (string.IsNullOrEmpty(group.Variant)) { - return new ValueTask(variant); + _logger.LogWarning($"Missing variant name for {nameof(featureDefinition.Allocation.Group)} allocation in feature {featureDefinition.Name}"); + + return new ValueTask((VariantDefinition)null); } + + Assert.NotNull(featureDefinition.Variants); + + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == group.Variant); + + return new ValueTask( + featureDefinition + .Variants + .FirstOrDefault((variant) => variant.Name == group.Variant)); } } } @@ -443,12 +462,21 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur { if (TargetingEvaluator.IsTargeted(targetingContext, percentile.From, percentile.To, _assignerOptions.IgnoreCase, featureDefinition.Allocation.Seed)) { - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == percentile.Variant); - - if (!string.IsNullOrEmpty(variant.Name)) + if (string.IsNullOrEmpty(percentile.Variant)) { - return new ValueTask(variant); + _logger.LogWarning($"Missing variant name for {nameof(featureDefinition.Allocation.Percentile)} allocation in feature {featureDefinition.Name}"); + + return new ValueTask((VariantDefinition)null); } + + Assert.NotNull(featureDefinition.Variants); + + variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == percentile.Variant); + + return new ValueTask( + featureDefinition + .Variants + .FirstOrDefault((variant) => variant.Name == percentile.Variant)); } } } @@ -458,11 +486,11 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur private VariantDefinition ResolveDefaultVariantDefinition(FeatureDefinition featureDefinition, bool isFeatureEnabled) { - string defaultVariantPath = isFeatureEnabled ? featureDefinition.Allocation.DefaultWhenEnabled : featureDefinition.Allocation.DefaultWhenDisabled; + string defaultVariantName = isFeatureEnabled ? featureDefinition.Allocation.DefaultWhenEnabled : featureDefinition.Allocation.DefaultWhenDisabled; - if (!string.IsNullOrEmpty(defaultVariantPath)) + if (!string.IsNullOrEmpty(defaultVariantName)) { - VariantDefinition defaultVariant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == defaultVariantPath); + VariantDefinition defaultVariant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == defaultVariantName); if (!string.IsNullOrEmpty(defaultVariant.Name)) { diff --git a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj index beb3ceb4..4951bfbe 100644 --- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj +++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs index 312f5f18..4dd4cb2a 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs @@ -65,7 +65,7 @@ public static bool IsTargeted(ITargetingContext targetingContext, TargetingFilte // Check if the user is being targeted directly if (settings.Audience.Users != null && IsTargeted( - targetingContext, + targetingContext.UserId, settings.Audience.Users, ignoreCase)) { @@ -97,22 +97,17 @@ public static bool IsTargeted(ITargetingContext targetingContext, TargetingFilte /// Determines if a targeting context is targeted by presence in a list of users /// public static bool IsTargeted( - ITargetingContext targetingContext, + string userId, IEnumerable users, bool ignoreCase) { - if (targetingContext == null) - { - throw new ArgumentNullException(nameof(targetingContext)); - } - if (users == null) { throw new ArgumentNullException(nameof(users)); } - if (targetingContext.UserId != null && - users.Any(user => targetingContext.UserId.Equals(user, GetComparisonType(ignoreCase)))) + if (userId != null && + users.Any(user => userId.Equals(user, GetComparisonType(ignoreCase)))) { return true; } @@ -121,28 +116,23 @@ public static bool IsTargeted( } /// - /// Determines if a targeting context is targeted by presence in a list of groups + /// Determines if targeting context is targeted by presence in a list of groups /// - public static bool IsGroupTargeted( - ITargetingContext targetingContext, + public static bool IsTargeted( + IEnumerable targetingContextGroups, IEnumerable groups, bool ignoreCase) { - if (targetingContext == null) - { - throw new ArgumentNullException(nameof(targetingContext)); - } - if (groups == null) { throw new ArgumentNullException(nameof(groups)); } - if (targetingContext.Groups != null) + if (targetingContextGroups != null) { IEnumerable normalizedGroups = ignoreCase ? - targetingContext.Groups.Select(g => g.ToLower()) : - targetingContext.Groups; + targetingContextGroups.Select(g => g.ToLower()) : + targetingContextGroups; foreach (string group in normalizedGroups) { @@ -246,7 +236,7 @@ public static bool IsTargeted( /// The name of the invalid setting, if any. /// The reason that the setting is invalid. /// True if the provided settings are valid. False if the provided settings are invalid. - public static bool TryValidateSettings(TargetingFilterSettings targetingSettings, out string paramName, out string reason) + private static bool TryValidateSettings(TargetingFilterSettings targetingSettings, out string paramName, out string reason) { paramName = null; diff --git a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs index 20dfa58f..29de6ef2 100644 --- a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs +++ b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Primitives; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; @@ -15,18 +16,25 @@ internal class VariantConfigurationSection : IConfigurationSection private readonly string _key; private readonly string _path; - public VariantConfigurationSection(string key, string path, string value) + public VariantConfigurationSection(string key, string value) { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } - _path = path; + _path = ""; _key = key; Value = value; } + private VariantConfigurationSection(string key) + { + _path = key; + _key = key; + Value = null; + } + public string this[string key] { get @@ -57,7 +65,7 @@ public IChangeToken GetReloadToken() public IConfigurationSection GetSection(string key) { - return new VariantConfigurationSection(key, key, null); + return new VariantConfigurationSection(key); } } } diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index fa10f3d6..77bbc840 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -1027,7 +1027,7 @@ public async Task UsesVariants() } [Fact] - public async Task VariantsExceptions() + public async Task VariantsInvalidScenarios() { IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); @@ -1043,20 +1043,20 @@ public async Task VariantsExceptions() IVariantFeatureManager featureManager = serviceProvider.GetRequiredService(); CancellationToken cancellationToken = CancellationToken.None; - // Test throws missing variants exception - FeatureManagementException e = await Assert.ThrowsAsync(async () => await featureManager.GetVariantAsync("VariantFeatureNoVariants", cancellationToken)); + // Verify null variant returned if no variants are specified + Variant variant = await featureManager.GetVariantAsync("VariantFeatureNoVariants", cancellationToken); - Assert.Equal(FeatureManagementError.MissingVariantDefinition, e.Error); + Assert.Null(variant); - // Test throws invalid variant configuration - e = await Assert.ThrowsAsync(async () => await featureManager.GetVariantAsync("VariantFeatureBothConfigurations", cancellationToken)); + // Verify null variant returned if no allocation is specified + variant = await featureManager.GetVariantAsync("VariantFeatureNoAllocation", cancellationToken); - Assert.Equal(FeatureManagementError.InvalidVariantConfiguration, e.Error); + Assert.Null(variant); - // Test throws missing allocation - e = await Assert.ThrowsAsync(async () => await featureManager.GetVariantAsync("VariantFeatureNoAllocation", cancellationToken)); + // Verify that ConfigurationValue has priority over ConfigurationReference + variant = await featureManager.GetVariantAsync("VariantFeatureBothConfigurations", cancellationToken); - Assert.Equal(FeatureManagementError.MissingAllocation, e.Error); + Assert.Equal("600px", variant.Configuration.Value); } private static void DisableEndpointRouting(MvcOptions options) diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index c79520be..b4f40330 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -331,7 +331,7 @@ "Variants": [ { "Name": "Small", - "ConfigurationValue": "300px", + "ConfigurationValue": "600px", "ConfigurationReference": "ShoppingCart:Small" } ], From 744b9665c7f938bc1c28e8dc6ee665850bdc5bb0 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 16 Aug 2023 14:44:23 -0500 Subject: [PATCH 56/80] refactor isenabled to remove boolean param --- .../FeatureManager.cs | 146 +++++++++++------- .../FeatureManagement.cs | 1 - 2 files changed, 91 insertions(+), 56 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 9f04d1ce..a7768005 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -69,27 +69,72 @@ public FeatureManager( public Task IsEnabledAsync(string feature) { - return IsEnabledAsync(feature, appContext: null, useAppContext: false, ignoreVariant: false, CancellationToken.None); + return IsEnabledWithVariantsAsync(feature, appContext: null, useAppContext: false, CancellationToken.None); } public Task IsEnabledAsync(string feature, TContext appContext) { - return IsEnabledAsync(feature, appContext, useAppContext: true, ignoreVariant: false, CancellationToken.None); + return IsEnabledWithVariantsAsync(feature, appContext, useAppContext: true, CancellationToken.None); } public Task IsEnabledAsync(string feature, CancellationToken cancellationToken) { - return IsEnabledAsync(feature, appContext: null, useAppContext: false, ignoreVariant: false, cancellationToken); + return IsEnabledWithVariantsAsync(feature, appContext: null, useAppContext: false, cancellationToken); } public Task IsEnabledAsync(string feature, TContext appContext, CancellationToken cancellationToken) { - return IsEnabledAsync(feature, appContext, useAppContext: true, ignoreVariant: false, cancellationToken); + return IsEnabledWithVariantsAsync(feature, appContext, useAppContext: true, cancellationToken); } - private Task IsEnabledAsync(string feature, TContext appContext, bool ignoreVariant, CancellationToken cancellationToken) + private async Task IsEnabledWithVariantsAsync(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken) { - return IsEnabledAsync(feature, appContext, useAppContext: true, ignoreVariant, cancellationToken); + bool isFeatureEnabled = await IsEnabledPrivateAsync(feature, appContext, useAppContext, cancellationToken).ConfigureAwait(false); + + VariantDefinition variantDefinition = null; + FeatureDefinition featureDefinition = await _featureDefinitionProvider.GetFeatureDefinitionAsync(feature).ConfigureAwait(false); + TargetingContext targetingContext; + + if (featureDefinition == null || !(featureDefinition.Variants?.Any() ?? false) || featureDefinition.Status == FeatureStatus.Disabled) + { + return isFeatureEnabled; + } + + if (!isFeatureEnabled) + { + variantDefinition = ResolveDefaultVariantDefinition(featureDefinition, isFeatureEnabled: false); + } + else if (!useAppContext) + { + if (appContext is TargetingContext) + { + variantDefinition = await GetAssignedVariantAsync(featureDefinition, appContext as TargetingContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); + } + else + { + targetingContext = await ResolveContextAsync(cancellationToken).ConfigureAwait(false); + variantDefinition = await GetAssignedVariantAsync(featureDefinition, targetingContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); + } + } + + if (variantDefinition == null) + { + return isFeatureEnabled; + } + + if (variantDefinition != null) + { + if (variantDefinition.StatusOverride == StatusOverride.Enabled) + { + return true; + } + else if (variantDefinition.StatusOverride == StatusOverride.Disabled) + { + return false; + } + } + + return isFeatureEnabled; } public async IAsyncEnumerable GetFeatureNamesAsync() @@ -105,7 +150,7 @@ public void Dispose() _parametersCache.Dispose(); } - private async Task IsEnabledAsync(string feature, TContext appContext, bool useAppContext, bool ignoreVariant, CancellationToken cancellationToken) + private async Task IsEnabledPrivateAsync(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken) { foreach (ISessionManager sessionManager in _sessionManagers) { @@ -224,23 +269,6 @@ await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) } } } - - if (!ignoreVariant && (featureDefinition.Variants?.Any() ?? false) && featureDefinition.Allocation != null && featureDefinition.Status != FeatureStatus.Disabled) - { - VariantDefinition variantDefinition = await GetVariantDefinitionAsync(featureDefinition, appContext as TargetingContext, useAppContext, enabled, cancellationToken); - - if (variantDefinition != null) - { - if (variantDefinition.StatusOverride == StatusOverride.Enabled) - { - enabled = true; - } - else if (variantDefinition.StatusOverride == StatusOverride.Disabled) - { - enabled = false; - } - } - } } else { @@ -314,9 +342,19 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex VariantDefinition variantDefinition; - bool isFeatureEnabled = await IsEnabledAsync(feature, context, ignoreVariant: true, cancellationToken).ConfigureAwait(false); + bool isFeatureEnabled = await IsEnabledPrivateAsync(feature, context, useContext, cancellationToken).ConfigureAwait(false); - variantDefinition = await GetVariantDefinitionAsync(featureDefinition, context, useContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); + if (!isFeatureEnabled) + { + variantDefinition = ResolveDefaultVariantDefinition(featureDefinition, isFeatureEnabled: false); + } + + if (!useContext && context == null) + { + context = await ResolveContextAsync(cancellationToken).ConfigureAwait(false); + } + + variantDefinition = await GetAssignedVariantAsync(featureDefinition, context, isFeatureEnabled, cancellationToken).ConfigureAwait(false); if (variantDefinition == null) { @@ -346,7 +384,6 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex } } - return new Variant() { Name = variantDefinition.Name, @@ -354,41 +391,35 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex }; } - private async ValueTask GetVariantDefinitionAsync( - FeatureDefinition featureDefinition, - TargetingContext context, - bool useContext, - bool isFeatureEnabled, - CancellationToken cancellationToken) + private async ValueTask ResolveContextAsync(CancellationToken cancellationToken) { - if (!isFeatureEnabled) + TargetingContext context = null; + + if (_contextAccessor == null) { - return ResolveDefaultVariantDefinition(featureDefinition, isFeatureEnabled); + _logger.LogWarning($"No instance of {nameof(ITargetingContextAccessor)} is available for targeting evaluation. Using default variants."); } - - VariantDefinition variantDefinition = null; - - if (!useContext) + else { - if (_contextAccessor == null) - { - _logger.LogWarning($"No instance of {nameof(ITargetingContextAccessor)} is available for targeting evaluation. Using default variants."); - } - else - { - // - // Acquire targeting context via accessor - context = await _contextAccessor.GetContextAsync().ConfigureAwait(false); + // + // Acquire targeting context via accessor + context = await _contextAccessor.GetContextAsync().ConfigureAwait(false); - // - // Ensure targeting can be performed - if (context == null) - { - _logger.LogWarning($"No instance of {nameof(TargetingContext)} could be found using {nameof(ITargetingContextAccessor)} for targeting evaluation."); - } + // + // Ensure targeting can be performed + if (context == null) + { + _logger.LogWarning($"No instance of {nameof(TargetingContext)} could be found using {nameof(ITargetingContextAccessor)} for targeting evaluation."); } } + return context; + } + + private async ValueTask GetAssignedVariantAsync(FeatureDefinition featureDefinition, TargetingContext context, bool isFeatureEnabled, CancellationToken cancellationToken) + { + VariantDefinition variantDefinition = null; + if (context != null) { variantDefinition = await AssignVariantAsync(featureDefinition, context, cancellationToken).ConfigureAwait(false); @@ -460,7 +491,12 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur { foreach (PercentileAllocation percentile in featureDefinition.Allocation.Percentile) { - if (TargetingEvaluator.IsTargeted(targetingContext, percentile.From, percentile.To, _assignerOptions.IgnoreCase, featureDefinition.Allocation.Seed)) + if (TargetingEvaluator.IsTargeted( + targetingContext, + percentile.From, + percentile.To, + _assignerOptions.IgnoreCase, + featureDefinition.Allocation.Seed)) { if (string.IsNullOrEmpty(percentile.Variant)) { diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index 77bbc840..243d264c 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -814,7 +814,6 @@ public async Task UsesRequirementType() // Set filters to all return true testFeatureFilter.Callback = _ => Task.FromResult(true); - Assert.True(await featureManager.IsEnabledAsync(anyFilterFeature)); Assert.True(await featureManager.IsEnabledAsync(allFilterFeature)); From 02f6e55ad0a6b74f825e37f8cd924b569160e928 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 16 Aug 2023 16:22:13 -0500 Subject: [PATCH 57/80] change configurationvalue to IConfigurationSection instead of string --- .../FeatureManager.cs | 4 +- .../VariantConfigurationSection.cs | 71 ------------------- .../VariantDefinition.cs | 4 +- .../FeatureManagement.cs | 4 +- .../Tests.FeatureManagement/appsettings.json | 5 +- 5 files changed, 11 insertions(+), 77 deletions(-) delete mode 100644 src/Microsoft.FeatureManagement/VariantConfigurationSection.cs diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index a7768005..a5095af0 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -363,12 +363,12 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex IConfigurationSection variantConfiguration = null; - bool configValueSet = !string.IsNullOrEmpty(variantDefinition.ConfigurationValue); + bool configValueSet = variantDefinition.ConfigurationValue.Exists(); bool configReferenceSet = !string.IsNullOrEmpty(variantDefinition.ConfigurationReference); if (configValueSet) { - variantConfiguration = new VariantConfigurationSection(nameof(variantDefinition.ConfigurationValue), variantDefinition.ConfigurationValue); + variantConfiguration = variantDefinition.ConfigurationValue; } else if (configReferenceSet) { diff --git a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs b/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs deleted file mode 100644 index 29de6ef2..00000000 --- a/src/Microsoft.FeatureManagement/VariantConfigurationSection.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Primitives; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; - -namespace Microsoft.FeatureManagement -{ - internal class VariantConfigurationSection : IConfigurationSection - { - private readonly string _key; - private readonly string _path; - - public VariantConfigurationSection(string key, string value) - { - if (string.IsNullOrEmpty(key)) - { - throw new ArgumentNullException(nameof(key)); - } - - _path = ""; - _key = key; - Value = value; - } - - private VariantConfigurationSection(string key) - { - _path = key; - _key = key; - Value = null; - } - - public string this[string key] - { - get - { - throw new NotImplementedException(); - } - set - { - throw new NotImplementedException(); - } - } - - public string Key => _key; - - public string Path => _path; - - public string Value { get; set; } - - public IEnumerable GetChildren() - { - return Enumerable.Empty(); - } - - public IChangeToken GetReloadToken() - { - return new CancellationChangeToken(CancellationToken.None); - } - - public IConfigurationSection GetSection(string key) - { - return new VariantConfigurationSection(key); - } - } -} diff --git a/src/Microsoft.FeatureManagement/VariantDefinition.cs b/src/Microsoft.FeatureManagement/VariantDefinition.cs index 4ea42fc6..77bd6d7d 100644 --- a/src/Microsoft.FeatureManagement/VariantDefinition.cs +++ b/src/Microsoft.FeatureManagement/VariantDefinition.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. // +using Microsoft.Extensions.Configuration; + namespace Microsoft.FeatureManagement { /// @@ -17,7 +19,7 @@ public class VariantDefinition /// /// The value of the configuration for this variant of the feature. /// - public string ConfigurationValue { get; set; } + public IConfigurationSection ConfigurationValue { get; set; } /// /// A reference pointing to the configuration for this variant of the feature. diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index 243d264c..cb1c3036 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -1003,11 +1003,11 @@ public async Task UsesVariants() Assert.Equal("300px", variant.Configuration.Value); Assert.False(await featureManager.IsEnabledAsync("VariantFeatureStatusDisabled", cancellationToken)); - // Test DefaultWhenEnabled + // Test DefaultWhenEnabled and ConfigurationValue with inline IConfigurationSection variant = await featureManager.GetVariantAsync("VariantFeatureDefaultEnabled", cancellationToken); Assert.Equal("Medium", variant.Name); - Assert.Equal("450px", variant.Configuration.Value); + Assert.Equal("450px", variant.Configuration["Size"]); Assert.True(await featureManager.IsEnabledAsync("VariantFeatureDefaultEnabled", cancellationToken)); // Test User allocation diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index b4f40330..3c754bbe 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -239,7 +239,10 @@ "Variants": [ { "Name": "Medium", - "ConfigurationValue": "450px" + "ConfigurationValue": { + "Size": "450px", + "Color": "Purple" + } }, { "Name": "Small", From efbab1761d3c6b0d8f1b90254c60994aa6747aa6 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 16 Aug 2023 16:27:41 -0500 Subject: [PATCH 58/80] fix enabledwithvariants logic --- .../FeatureManager.cs | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index a5095af0..216475fc 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -91,7 +91,7 @@ private async Task IsEnabledWithVariantsAsync(string feature, TC { bool isFeatureEnabled = await IsEnabledPrivateAsync(feature, appContext, useAppContext, cancellationToken).ConfigureAwait(false); - VariantDefinition variantDefinition = null; + VariantDefinition variantDefinition; FeatureDefinition featureDefinition = await _featureDefinitionProvider.GetFeatureDefinitionAsync(feature).ConfigureAwait(false); TargetingContext targetingContext; @@ -104,34 +104,27 @@ private async Task IsEnabledWithVariantsAsync(string feature, TC { variantDefinition = ResolveDefaultVariantDefinition(featureDefinition, isFeatureEnabled: false); } - else if (!useAppContext) + else if (useAppContext && appContext is TargetingContext) { - if (appContext is TargetingContext) - { - variantDefinition = await GetAssignedVariantAsync(featureDefinition, appContext as TargetingContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); - } - else - { - targetingContext = await ResolveContextAsync(cancellationToken).ConfigureAwait(false); - variantDefinition = await GetAssignedVariantAsync(featureDefinition, targetingContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); - } + variantDefinition = await GetAssignedVariantAsync(featureDefinition, appContext as TargetingContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); + } + else + { + targetingContext = await ResolveContextAsync(cancellationToken).ConfigureAwait(false); + variantDefinition = await GetAssignedVariantAsync(featureDefinition, targetingContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); } if (variantDefinition == null) { return isFeatureEnabled; } - - if (variantDefinition != null) + else if (variantDefinition.StatusOverride == StatusOverride.Enabled) { - if (variantDefinition.StatusOverride == StatusOverride.Enabled) - { - return true; - } - else if (variantDefinition.StatusOverride == StatusOverride.Disabled) - { - return false; - } + return true; + } + else if (variantDefinition.StatusOverride == StatusOverride.Disabled) + { + return false; } return isFeatureEnabled; From e4cddae9d9eec9cd28cb2e140a96810484270661 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 17 Aug 2023 11:50:21 -0500 Subject: [PATCH 59/80] PR revisions, fix logic in new methods from last commit --- .../FeatureManagementError.cs | 7 +- .../FeatureManager.cs | 92 ++++++++++--------- .../Targeting/TargetingEvaluator.cs | 16 ++-- 3 files changed, 60 insertions(+), 55 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManagementError.cs b/src/Microsoft.FeatureManagement/FeatureManagementError.cs index a27c71f7..d67c0b16 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementError.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementError.cs @@ -31,11 +31,6 @@ public enum FeatureManagementError /// /// The given configuration setting was invalid. /// - InvalidConfigurationSetting, - - /// - /// A feature variant specifies both a configuration reference and configuration value, or there is no valid configuration to reference. - /// - InvalidVariantConfiguration + InvalidConfigurationSetting } } diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 216475fc..7e01d39d 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -89,29 +89,45 @@ public Task IsEnabledAsync(string feature, TContext appContext, private async Task IsEnabledWithVariantsAsync(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken) { - bool isFeatureEnabled = await IsEnabledPrivateAsync(feature, appContext, useAppContext, cancellationToken).ConfigureAwait(false); + bool isFeatureEnabled = await IsEnabled(feature, appContext, useAppContext, cancellationToken).ConfigureAwait(false); - VariantDefinition variantDefinition; FeatureDefinition featureDefinition = await _featureDefinitionProvider.GetFeatureDefinitionAsync(feature).ConfigureAwait(false); - TargetingContext targetingContext; - if (featureDefinition == null || !(featureDefinition.Variants?.Any() ?? false) || featureDefinition.Status == FeatureStatus.Disabled) + if (featureDefinition == null || featureDefinition.Status == FeatureStatus.Disabled) + { + return false; + } + + if (!(featureDefinition.Variants?.Any() ?? false) || featureDefinition?.Allocation == null) { return isFeatureEnabled; } + VariantDefinition variantDefinition; + if (!isFeatureEnabled) { variantDefinition = ResolveDefaultVariantDefinition(featureDefinition, isFeatureEnabled: false); } - else if (useAppContext && appContext is TargetingContext) - { - variantDefinition = await GetAssignedVariantAsync(featureDefinition, appContext as TargetingContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); - } else { - targetingContext = await ResolveContextAsync(cancellationToken).ConfigureAwait(false); - variantDefinition = await GetAssignedVariantAsync(featureDefinition, targetingContext, isFeatureEnabled, cancellationToken).ConfigureAwait(false); + TargetingContext targetingContext; + + if (useAppContext) + { + targetingContext = appContext as TargetingContext; + } + else + { + targetingContext = await ResolveTargetingContextAsync(cancellationToken).ConfigureAwait(false); + } + + variantDefinition = await GetAssignedVariantAsync( + featureDefinition, + targetingContext, + isFeatureEnabled, + cancellationToken) + .ConfigureAwait(false); } if (variantDefinition == null) @@ -143,7 +159,7 @@ public void Dispose() _parametersCache.Dispose(); } - private async Task IsEnabledPrivateAsync(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken) + private async Task IsEnabled(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken) { foreach (ISessionManager sessionManager in _sessionManagers) { @@ -328,26 +344,28 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex _logger.LogWarning(errorMessage); } - if (featureDefinition.Allocation == null || (!featureDefinition.Variants?.Any() ?? false)) + if (featureDefinition?.Allocation == null || (!featureDefinition.Variants?.Any() ?? false)) { return null; } - VariantDefinition variantDefinition; + VariantDefinition variantDefinition = null; - bool isFeatureEnabled = await IsEnabledPrivateAsync(feature, context, useContext, cancellationToken).ConfigureAwait(false); + bool isFeatureEnabled = await IsEnabled(feature, context, useContext, cancellationToken).ConfigureAwait(false); if (!isFeatureEnabled) { variantDefinition = ResolveDefaultVariantDefinition(featureDefinition, isFeatureEnabled: false); } - - if (!useContext && context == null) + else { - context = await ResolveContextAsync(cancellationToken).ConfigureAwait(false); - } + if (!useContext) + { + context = await ResolveTargetingContextAsync(cancellationToken).ConfigureAwait(false); + } - variantDefinition = await GetAssignedVariantAsync(featureDefinition, context, isFeatureEnabled, cancellationToken).ConfigureAwait(false); + variantDefinition = await GetAssignedVariantAsync(featureDefinition, context, isFeatureEnabled, cancellationToken).ConfigureAwait(false); + } if (variantDefinition == null) { @@ -384,26 +402,24 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex }; } - private async ValueTask ResolveContextAsync(CancellationToken cancellationToken) + private async ValueTask ResolveTargetingContextAsync(CancellationToken cancellationToken) { - TargetingContext context = null; - if (_contextAccessor == null) { - _logger.LogWarning($"No instance of {nameof(ITargetingContextAccessor)} is available for targeting evaluation. Using default variants."); + _logger.LogWarning($"No instance of {nameof(ITargetingContextAccessor)} is available for targeting evaluation."); + + return null; } - else - { - // - // Acquire targeting context via accessor - context = await _contextAccessor.GetContextAsync().ConfigureAwait(false); - // - // Ensure targeting can be performed - if (context == null) - { - _logger.LogWarning($"No instance of {nameof(TargetingContext)} could be found using {nameof(ITargetingContextAccessor)} for targeting evaluation."); - } + // + // Acquire targeting context via accessor + TargetingContext context = await _contextAccessor.GetContextAsync().ConfigureAwait(false); + + // + // Ensure targeting can be performed + if (context == null) + { + _logger.LogWarning($"No instance of {nameof(TargetingContext)} could be found using {nameof(ITargetingContextAccessor)} for targeting evaluation."); } return context; @@ -420,7 +436,7 @@ private async ValueTask GetAssignedVariantAsync(FeatureDefini if (variantDefinition == null) { - variantDefinition = ResolveDefaultVariantDefinition(featureDefinition, isFeatureEnabled); + variantDefinition = ResolveDefaultVariantDefinition(featureDefinition, isFeatureEnabled: true); } return variantDefinition; @@ -445,8 +461,6 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur Assert.NotNull(featureDefinition.Variants); - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == user.Variant); - return new ValueTask( featureDefinition .Variants @@ -470,8 +484,6 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur Assert.NotNull(featureDefinition.Variants); - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == group.Variant); - return new ValueTask( featureDefinition .Variants @@ -500,8 +512,6 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur Assert.NotNull(featureDefinition.Variants); - variant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == percentile.Variant); - return new ValueTask( featureDefinition .Variants diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs index 4dd4cb2a..998380f2 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs @@ -119,24 +119,24 @@ public static bool IsTargeted( /// Determines if targeting context is targeted by presence in a list of groups /// public static bool IsTargeted( - IEnumerable targetingContextGroups, - IEnumerable groups, + IEnumerable sourceGroups, + IEnumerable targetedGroups, bool ignoreCase) { - if (groups == null) + if (targetedGroups == null) { - throw new ArgumentNullException(nameof(groups)); + throw new ArgumentNullException(nameof(targetedGroups)); } - if (targetingContextGroups != null) + if (sourceGroups != null) { IEnumerable normalizedGroups = ignoreCase ? - targetingContextGroups.Select(g => g.ToLower()) : - targetingContextGroups; + sourceGroups.Select(g => g.ToLower()) : + sourceGroups; foreach (string group in normalizedGroups) { - string allocationGroup = groups.FirstOrDefault(g => g.Equals(group, GetComparisonType(ignoreCase))); + string allocationGroup = targetedGroups.FirstOrDefault(g => g.Equals(group, GetComparisonType(ignoreCase))); if (allocationGroup != null) { From e8a640b0599c9963be0c61f3ae6d04b49fcdd6e6 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 21 Aug 2023 12:57:02 -0500 Subject: [PATCH 60/80] set session managers last in flow --- .../FeatureManager.cs | 81 +++++++++---------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 7e01d39d..bb36b54c 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -93,54 +93,56 @@ private async Task IsEnabledWithVariantsAsync(string feature, TC FeatureDefinition featureDefinition = await _featureDefinitionProvider.GetFeatureDefinitionAsync(feature).ConfigureAwait(false); - if (featureDefinition == null || featureDefinition.Status == FeatureStatus.Disabled) + // If featureDefinition is null or FeatureStatus is Disabled, return isFeatureEnabled, which is false. + // If there are no variants or no allocation defined, also return isFeatureEnabled, true or false depending on IsEnabled. + if (featureDefinition != null && + featureDefinition.Status != FeatureStatus.Disabled && + (featureDefinition.Variants?.Any() ?? false) && + featureDefinition.Allocation != null) { - return false; - } - - if (!(featureDefinition.Variants?.Any() ?? false) || featureDefinition?.Allocation == null) - { - return isFeatureEnabled; - } - - VariantDefinition variantDefinition; - - if (!isFeatureEnabled) - { - variantDefinition = ResolveDefaultVariantDefinition(featureDefinition, isFeatureEnabled: false); - } - else - { - TargetingContext targetingContext; + VariantDefinition variantDefinition; - if (useAppContext) + if (!isFeatureEnabled) { - targetingContext = appContext as TargetingContext; + variantDefinition = ResolveDefaultVariantDefinition(featureDefinition, isFeatureEnabled: false); } else { - targetingContext = await ResolveTargetingContextAsync(cancellationToken).ConfigureAwait(false); + TargetingContext targetingContext; + + if (useAppContext) + { + targetingContext = appContext as TargetingContext; + } + else + { + targetingContext = await ResolveTargetingContextAsync(cancellationToken).ConfigureAwait(false); + } + + variantDefinition = await GetAssignedVariantAsync( + featureDefinition, + targetingContext, + isFeatureEnabled, + cancellationToken) + .ConfigureAwait(false); } - variantDefinition = await GetAssignedVariantAsync( - featureDefinition, - targetingContext, - isFeatureEnabled, - cancellationToken) - .ConfigureAwait(false); + if (variantDefinition != null) + { + if (variantDefinition.StatusOverride == StatusOverride.Enabled) + { + isFeatureEnabled = true; + } + else if (variantDefinition.StatusOverride == StatusOverride.Disabled) + { + isFeatureEnabled = false; + } + } } - if (variantDefinition == null) - { - return isFeatureEnabled; - } - else if (variantDefinition.StatusOverride == StatusOverride.Enabled) - { - return true; - } - else if (variantDefinition.StatusOverride == StatusOverride.Disabled) + foreach (ISessionManager sessionManager in _sessionManagers) { - return false; + await sessionManager.SetAsync(feature, isFeatureEnabled).ConfigureAwait(false); } return isFeatureEnabled; @@ -293,11 +295,6 @@ await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) _logger.LogWarning(errorMessage); } - foreach (ISessionManager sessionManager in _sessionManagers) - { - await sessionManager.SetAsync(feature, enabled).ConfigureAwait(false); - } - return enabled; } From dc49a2f3f653ac70e4053d53b3629da362de7053 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 21 Aug 2023 13:30:06 -0500 Subject: [PATCH 61/80] make false explicit for status disabled or missing definition --- src/Microsoft.FeatureManagement/FeatureManager.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index bb36b54c..a16913f3 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -93,12 +93,11 @@ private async Task IsEnabledWithVariantsAsync(string feature, TC FeatureDefinition featureDefinition = await _featureDefinitionProvider.GetFeatureDefinitionAsync(feature).ConfigureAwait(false); - // If featureDefinition is null or FeatureStatus is Disabled, return isFeatureEnabled, which is false. - // If there are no variants or no allocation defined, also return isFeatureEnabled, true or false depending on IsEnabled. - if (featureDefinition != null && - featureDefinition.Status != FeatureStatus.Disabled && - (featureDefinition.Variants?.Any() ?? false) && - featureDefinition.Allocation != null) + if (featureDefinition == null || featureDefinition.Status == FeatureStatus.Disabled) + { + isFeatureEnabled = false; + } + else if ((featureDefinition.Variants?.Any() ?? false) && featureDefinition.Allocation != null) { VariantDefinition variantDefinition; From d184882903a8cbb74b8b64f890f38e1b6c079a59 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 22 Aug 2023 13:33:22 -0500 Subject: [PATCH 62/80] fix constructor default params, move session managers logic, pr revisions --- ...rosoft.FeatureManagement.AspNetCore.csproj | 2 +- .../FeatureManager.cs | 28 +++++++++--------- .../Microsoft.FeatureManagement.csproj | 4 +-- .../ServiceCollectionExtensions.cs | 29 ++++++++++++++++++- .../Tests.FeatureManagement.csproj | 4 +-- 5 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj index 612eecfc..62bbc180 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj +++ b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj @@ -11,7 +11,7 @@ - netstandard2.0;netcoreapp3.1;net5.0;net6.0 + netstandard2.0;net5.0;net6.0 true false ..\..\build\Microsoft.FeatureManagement.snk diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index a16913f3..b2f51a46 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -28,15 +28,13 @@ class FeatureManager : IFeatureManager, IDisposable, IVariantFeatureManager private readonly IFeatureDefinitionProvider _featureDefinitionProvider; private readonly IEnumerable _featureFilters; private readonly IEnumerable _sessionManagers; - private readonly ITargetingContextAccessor _contextAccessor; - private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly ConcurrentDictionary _filterMetadataCache; private readonly ConcurrentDictionary _contextualFeatureFilterCache; private readonly FeatureManagementOptions _options; private readonly TargetingEvaluationOptions _assignerOptions; private readonly IMemoryCache _parametersCache; - + private class ConfigurationCacheItem { public IConfiguration Parameters { get; set; } @@ -50,23 +48,23 @@ public FeatureManager( IEnumerable sessionManagers, ILoggerFactory loggerFactory, IOptions options, - IOptions assignerOptions, - IConfiguration configuration = null, - ITargetingContextAccessor contextAccessor = null) + IOptions assignerOptions) { _featureDefinitionProvider = featureDefinitionProvider; _featureFilters = featureFilters ?? throw new ArgumentNullException(nameof(featureFilters)); _sessionManagers = sessionManagers ?? throw new ArgumentNullException(nameof(sessionManagers)); _logger = loggerFactory.CreateLogger(); _assignerOptions = assignerOptions?.Value ?? throw new ArgumentNullException(nameof(assignerOptions)); - _contextAccessor = contextAccessor; - _configuration = configuration; _filterMetadataCache = new ConcurrentDictionary(); _contextualFeatureFilterCache = new ConcurrentDictionary(); _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); _parametersCache = new MemoryCache(new MemoryCacheOptions()); } + public IConfiguration Configuration { get; init; } + + public ITargetingContextAccessor TargetingContextAccessor { get; init; } + public Task IsEnabledAsync(string feature) { return IsEnabledWithVariantsAsync(feature, appContext: null, useAppContext: false, CancellationToken.None); @@ -89,7 +87,7 @@ public Task IsEnabledAsync(string feature, TContext appContext, private async Task IsEnabledWithVariantsAsync(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken) { - bool isFeatureEnabled = await IsEnabled(feature, appContext, useAppContext, cancellationToken).ConfigureAwait(false); + bool isFeatureEnabled = await IsEnabledAsync(feature, appContext, useAppContext, cancellationToken).ConfigureAwait(false); FeatureDefinition featureDefinition = await _featureDefinitionProvider.GetFeatureDefinitionAsync(feature).ConfigureAwait(false); @@ -160,7 +158,7 @@ public void Dispose() _parametersCache.Dispose(); } - private async Task IsEnabled(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken) + private async Task IsEnabledAsync(string feature, TContext appContext, bool useAppContext, CancellationToken cancellationToken) { foreach (ISessionManager sessionManager in _sessionManagers) { @@ -347,7 +345,7 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex VariantDefinition variantDefinition = null; - bool isFeatureEnabled = await IsEnabled(feature, context, useContext, cancellationToken).ConfigureAwait(false); + bool isFeatureEnabled = await IsEnabledAsync(feature, context, useContext, cancellationToken).ConfigureAwait(false); if (!isFeatureEnabled) { @@ -379,7 +377,7 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex } else if (configReferenceSet) { - if (_configuration == null) + if (Configuration == null) { _logger.LogWarning($"Cannot use {nameof(variantDefinition.ConfigurationReference)} as no instance of {nameof(IConfiguration)} is present."); @@ -387,7 +385,7 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex } else { - variantConfiguration = _configuration.GetSection(variantDefinition.ConfigurationReference); + variantConfiguration = Configuration.GetSection(variantDefinition.ConfigurationReference); } } @@ -400,7 +398,7 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex private async ValueTask ResolveTargetingContextAsync(CancellationToken cancellationToken) { - if (_contextAccessor == null) + if (TargetingContextAccessor == null) { _logger.LogWarning($"No instance of {nameof(ITargetingContextAccessor)} is available for targeting evaluation."); @@ -409,7 +407,7 @@ private async ValueTask ResolveTargetingContextAsync(Cancellat // // Acquire targeting context via accessor - TargetingContext context = await _contextAccessor.GetContextAsync().ConfigureAwait(false); + TargetingContext context = await TargetingContextAccessor.GetContextAsync().ConfigureAwait(false); // // Ensure targeting can be performed diff --git a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj index 4951bfbe..30818ff7 100644 --- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj +++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj @@ -11,11 +11,11 @@ - netstandard2.0;netcoreapp3.1;net5.0;net6.0 + netstandard2.0;net5.0;net6.0 true false ..\..\build\Microsoft.FeatureManagement.snk - 8.0 + 9.0 diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index fa7f7bfd..8fffd225 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -4,7 +4,11 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.FeatureManagement.FeatureFilters; using System; +using System.Collections.Generic; namespace Microsoft.FeatureManagement { @@ -30,8 +34,31 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec services.TryAddSingleton(sp => sp.GetRequiredService()); - services.TryAddSingleton(sp => sp.GetRequiredService()); + services.TryAddSingleton(sp => + new FeatureManager( + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService>()) + { + Configuration = sp.GetService(), // May or may not exist in DI + TargetingContextAccessor = sp.GetService() + }); + services.TryAddSingleton(sp => + new FeatureManager( + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService>()) + { + Configuration = sp.GetService(), // May or may not exist in DI + TargetingContextAccessor = sp.GetService() + }); services.AddSingleton(); services.AddScoped(); diff --git a/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj b/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj index 7501c259..2d4d91f6 100644 --- a/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj +++ b/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj @@ -1,9 +1,9 @@  - netcoreapp2.1;netcoreapp3.1;net5.0;net6.0 + netcoreapp2.1;net5.0;net6.0 false - 8.0 + 9.0 True ..\..\build\Microsoft.FeatureManagement.snk From 49aa2fbd2d5fbc710f24a01545477c94eab2db94 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 22 Aug 2023 13:36:06 -0500 Subject: [PATCH 63/80] fix comment --- .../ServiceCollectionExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 8fffd225..da5c6d6d 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -43,7 +43,7 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec sp.GetRequiredService>(), sp.GetRequiredService>()) { - Configuration = sp.GetService(), // May or may not exist in DI + Configuration = sp.GetService(), TargetingContextAccessor = sp.GetService() }); @@ -56,7 +56,7 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec sp.GetRequiredService>(), sp.GetRequiredService>()) { - Configuration = sp.GetService(), // May or may not exist in DI + Configuration = sp.GetService(), TargetingContextAccessor = sp.GetService() }); services.AddSingleton(); From a0a787a02b42585998dab2be9653ba4709203865 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 22 Aug 2023 16:04:38 -0500 Subject: [PATCH 64/80] fix resolvedefaultvariant, isexternalinit error --- .../FeatureManager.cs | 12 +--------- .../IsExternalInit.cs | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/IsExternalInit.cs diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index b2f51a46..319f57ff 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -521,17 +521,7 @@ private VariantDefinition ResolveDefaultVariantDefinition(FeatureDefinition feat { string defaultVariantName = isFeatureEnabled ? featureDefinition.Allocation.DefaultWhenEnabled : featureDefinition.Allocation.DefaultWhenDisabled; - if (!string.IsNullOrEmpty(defaultVariantName)) - { - VariantDefinition defaultVariant = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == defaultVariantName); - - if (!string.IsNullOrEmpty(defaultVariant.Name)) - { - return defaultVariant; - } - } - - return null; + return featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == defaultVariantName); } private void BindSettings(IFeatureFilterMetadata filter, FeatureFilterEvaluationContext context, int filterIndex) diff --git a/src/Microsoft.FeatureManagement/IsExternalInit.cs b/src/Microsoft.FeatureManagement/IsExternalInit.cs new file mode 100644 index 00000000..3e722842 --- /dev/null +++ b/src/Microsoft.FeatureManagement/IsExternalInit.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + +using System.ComponentModel; + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit + { + } +} + +#endif \ No newline at end of file From 6ef1fce8abcfe2aaf731ee54694a448aaabfec44 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Tue, 22 Aug 2023 16:11:37 -0500 Subject: [PATCH 65/80] add back 3.1 --- .../Microsoft.FeatureManagement.AspNetCore.csproj | 2 +- .../Microsoft.FeatureManagement.csproj | 2 +- tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj index 62bbc180..612eecfc 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj +++ b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj @@ -11,7 +11,7 @@ - netstandard2.0;net5.0;net6.0 + netstandard2.0;netcoreapp3.1;net5.0;net6.0 true false ..\..\build\Microsoft.FeatureManagement.snk diff --git a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj index 30818ff7..383956c9 100644 --- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj +++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj @@ -11,7 +11,7 @@ - netstandard2.0;net5.0;net6.0 + netstandard2.0;netcoreapp3.1;net5.0;net6.0 true false ..\..\build\Microsoft.FeatureManagement.snk diff --git a/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj b/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj index 2d4d91f6..cbeed357 100644 --- a/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj +++ b/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1;net5.0;net6.0 + netcoreapp2.1;netcoreapp3.1;net5.0;net6.0 false 9.0 True From 2067d274f0728285eea17f226fc26bd92c75bdfb Mon Sep 17 00:00:00 2001 From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com> Date: Wed, 23 Aug 2023 14:51:30 -0500 Subject: [PATCH 66/80] Apply suggestions from code review Co-authored-by: Jimmy Campbell --- src/Microsoft.FeatureManagement/FeatureManager.cs | 4 ++-- src/Microsoft.FeatureManagement/FeatureStatus.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 319f57ff..a64f0f92 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -400,7 +400,7 @@ private async ValueTask ResolveTargetingContextAsync(Cancellat { if (TargetingContextAccessor == null) { - _logger.LogWarning($"No instance of {nameof(ITargetingContextAccessor)} is available for targeting evaluation."); + _logger.LogWarning($"No instance of {nameof(ITargetingContextAccessor)} is available for variant assignment."); return null; } @@ -413,7 +413,7 @@ private async ValueTask ResolveTargetingContextAsync(Cancellat // Ensure targeting can be performed if (context == null) { - _logger.LogWarning($"No instance of {nameof(TargetingContext)} could be found using {nameof(ITargetingContextAccessor)} for targeting evaluation."); + _logger.LogWarning($"No instance of {nameof(TargetingContext)} could be found using {nameof(ITargetingContextAccessor)} for variant assignment."); } return context; diff --git a/src/Microsoft.FeatureManagement/FeatureStatus.cs b/src/Microsoft.FeatureManagement/FeatureStatus.cs index c3c81749..3c1b9089 100644 --- a/src/Microsoft.FeatureManagement/FeatureStatus.cs +++ b/src/Microsoft.FeatureManagement/FeatureStatus.cs @@ -9,11 +9,11 @@ namespace Microsoft.FeatureManagement public enum FeatureStatus { /// - /// The state of the feature is conditional on the rest of its definition. + /// The state of the feature is conditional upon the feature evaluation pipeline. /// Conditional, /// - /// The state of the feature is disabled. + /// The state of the feature is always disabled. /// Disabled } From 3fa5f53c9d823f96e2451379c613dd72c5a27a3b Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 23 Aug 2023 15:04:20 -0500 Subject: [PATCH 67/80] isexternalinit comments, remove resolvedefault helper --- .../FeatureManager.cs | 13 +++---------- .../FeatureManagerSnapshot.cs | 2 +- .../IsExternalInit.cs | 17 ++++++++--------- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 319f57ff..2457b097 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -101,7 +101,7 @@ private async Task IsEnabledWithVariantsAsync(string feature, TC if (!isFeatureEnabled) { - variantDefinition = ResolveDefaultVariantDefinition(featureDefinition, isFeatureEnabled: false); + variantDefinition = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == featureDefinition.Allocation.DefaultWhenDisabled); } else { @@ -349,7 +349,7 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex if (!isFeatureEnabled) { - variantDefinition = ResolveDefaultVariantDefinition(featureDefinition, isFeatureEnabled: false); + variantDefinition = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == featureDefinition.Allocation.DefaultWhenDisabled); } else { @@ -430,7 +430,7 @@ private async ValueTask GetAssignedVariantAsync(FeatureDefini if (variantDefinition == null) { - variantDefinition = ResolveDefaultVariantDefinition(featureDefinition, isFeatureEnabled: true); + variantDefinition = featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == featureDefinition.Allocation.DefaultWhenEnabled); } return variantDefinition; @@ -517,13 +517,6 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur return new ValueTask(variant); } - private VariantDefinition ResolveDefaultVariantDefinition(FeatureDefinition featureDefinition, bool isFeatureEnabled) - { - string defaultVariantName = isFeatureEnabled ? featureDefinition.Allocation.DefaultWhenEnabled : featureDefinition.Allocation.DefaultWhenDisabled; - - return featureDefinition.Variants.FirstOrDefault((variant) => variant.Name == defaultVariantName); - } - private void BindSettings(IFeatureFilterMetadata filter, FeatureFilterEvaluationContext context, int filterIndex) { IFilterParametersBinder binder = filter as IFilterParametersBinder; diff --git a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs index 886447a0..9e20e8c6 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs @@ -17,7 +17,7 @@ class FeatureManagerSnapshot : IFeatureManagerSnapshot, IVariantFeatureManagerSn { private readonly FeatureManager _featureManager; private readonly ConcurrentDictionary> _flagCache = new ConcurrentDictionary>(); - private readonly IDictionary _variantCache = new Dictionary(); + private readonly ConcurrentDictionary _variantCache = new ConcurrentDictionary(); private IEnumerable _featureNames; public FeatureManagerSnapshot(FeatureManager featureManager) diff --git a/src/Microsoft.FeatureManagement/IsExternalInit.cs b/src/Microsoft.FeatureManagement/IsExternalInit.cs index 3e722842..279b080b 100644 --- a/src/Microsoft.FeatureManagement/IsExternalInit.cs +++ b/src/Microsoft.FeatureManagement/IsExternalInit.cs @@ -1,18 +1,17 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// -#if NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 +// The init accessor for properties is supported in C# 9.0 and later. +// This class is used to compile .NET frameworks that don't support C# 9.0 or later while still using the init accessor for a property. +// The code referenced for this file can be found here: https://github.com/dotnet/roslyn/issues/45510#issuecomment-725091019 + +#if NETSTANDARD2_0 || NETCOREAPP2_1 || NETCOREAPP3_1 using System.ComponentModel; -// ReSharper disable once CheckNamespace namespace System.Runtime.CompilerServices { - /// - /// Reserved to be used by the compiler for tracking metadata. - /// This class should not be used by developers in source code. - /// [EditorBrowsable(EditorBrowsableState.Never)] internal static class IsExternalInit { From c745f59de37f7630867789a35f71aee944d4eb46 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 28 Aug 2023 10:56:01 -0700 Subject: [PATCH 68/80] remove binding, fix featuredefinitionprovider issues --- .../ConfigurationFeatureDefinitionProvider.cs | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 790403be..b04ffa33 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -22,6 +22,8 @@ sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionProvider // provider to be marked for caching as well. private const string FeatureFiltersSectionName = "EnabledFor"; + private const string RequirementTypeKeyword = "RequirementType"; + private const string FeatureStatusKeyword = "Status"; private readonly IConfiguration _configuration; private readonly ConcurrentDictionary _definitions; private IDisposable _changeSubscription; @@ -162,8 +164,8 @@ We support } else { - requirementType = ParseFeatureDefinitionSectionEnum(configurationSection, nameof(RequirementType), requirementType); - status = ParseFeatureDefinitionSectionEnum(configurationSection, nameof(FeatureDefinition.Status), status); + requirementType = ParseFeatureDefinitionSectionEnum(configurationSection.Key, configurationSection[RequirementTypeKeyword], requirementType); + status = ParseFeatureDefinitionSectionEnum(configurationSection.Key, configurationSection[FeatureStatusKeyword], status); IEnumerable filterSections = configurationSection.GetSection(FeatureFiltersSectionName).GetChildren(); @@ -186,8 +188,37 @@ We support if (allocationSection.Exists()) { - allocation = new Allocation(); - allocationSection.Bind(allocation); + allocation = new Allocation() + { + DefaultWhenDisabled = allocationSection[nameof(Allocation.DefaultWhenDisabled)], + DefaultWhenEnabled = allocationSection[nameof(Allocation.DefaultWhenEnabled)], + User = allocationSection.GetSection(nameof(Allocation.User)).GetChildren().Select(userAllocation => + { + return new UserAllocation() + { + Variant = userAllocation[nameof(UserAllocation.Variant)], + Users = userAllocation.GetSection(nameof(UserAllocation.Users)).Get>() + }; + }), + Group = allocationSection.GetSection(nameof(Allocation.Group)).GetChildren().Select(groupAllocation => + { + return new GroupAllocation() + { + Variant = groupAllocation[nameof(GroupAllocation.Variant)], + Groups = groupAllocation.GetSection(nameof(GroupAllocation.Groups)).Get>() + }; + }), + Percentile = allocationSection.GetSection(nameof(Allocation.Percentile)).GetChildren().Select(percentileAllocation => + { + return new PercentileAllocation() + { + Variant = percentileAllocation[nameof(PercentileAllocation.Variant)], + From = percentileAllocation.GetValue(nameof(PercentileAllocation.From)), + To = percentileAllocation.GetValue(nameof(PercentileAllocation.To)) + }; + }), + Seed = allocationSection[nameof(Allocation.Seed)] + }; } IEnumerable variantsSections = configurationSection.GetSection(nameof(FeatureDefinition.Variants)).GetChildren(); @@ -197,8 +228,13 @@ We support { if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[nameof(VariantDefinition.Name)])) { - VariantDefinition variant = new VariantDefinition(); - section.Bind(variant); + VariantDefinition variant = new VariantDefinition() + { + Name = section[nameof(VariantDefinition.Name)], + ConfigurationValue = section.GetSection(nameof(VariantDefinition.ConfigurationValue)), + ConfigurationReference = section[nameof(VariantDefinition.ConfigurationReference)], + StatusOverride = section.GetValue(nameof(VariantDefinition.StatusOverride)) + }; variants.Add(variant); } } @@ -231,18 +267,16 @@ private IEnumerable GetFeatureDefinitionSections() } } - private T ParseFeatureDefinitionSectionEnum(IConfigurationSection configurationSection, string keyword, T enumValue) + private T ParseFeatureDefinitionSectionEnum(string feature, string value, T enumValue) where T : struct, Enum { - string rawValue = configurationSection[keyword]; - // // If the enum is specified, parse it and set the return value - if (!string.IsNullOrEmpty(rawValue) && !Enum.TryParse(rawValue, ignoreCase: true, out enumValue)) + if (!string.IsNullOrEmpty(value) && !Enum.TryParse(value, ignoreCase: true, out enumValue)) { throw new FeatureManagementException( FeatureManagementError.InvalidConfigurationSetting, - $"Invalid {typeof(T)?.Name} with value '{rawValue}' for feature '{configurationSection.Key}'."); + $"Invalid {typeof(T).Name} with value '{value}' for feature '{feature}'."); } return enumValue; From 29bce048af4dcd562eb945eebf83029052367a03 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 28 Aug 2023 11:02:54 -0700 Subject: [PATCH 69/80] change to Debug.Assert from Assert --- src/Microsoft.FeatureManagement/FeatureManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index fd2e9d2b..7ed3eea2 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -10,10 +10,10 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Xunit; namespace Microsoft.FeatureManagement { @@ -453,7 +453,7 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur return new ValueTask((VariantDefinition)null); } - Assert.NotNull(featureDefinition.Variants); + Debug.Assert(featureDefinition.Variants != null); return new ValueTask( featureDefinition @@ -476,7 +476,7 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur return new ValueTask((VariantDefinition)null); } - Assert.NotNull(featureDefinition.Variants); + Debug.Assert(featureDefinition.Variants != null); return new ValueTask( featureDefinition @@ -504,7 +504,7 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur return new ValueTask((VariantDefinition)null); } - Assert.NotNull(featureDefinition.Variants); + Debug.Assert(featureDefinition.Variants != null); return new ValueTask( featureDefinition From 9c7765dfc730b616b93e35daafdf37b619e32c71 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 28 Aug 2023 14:15:05 -0700 Subject: [PATCH 70/80] update method name --- .../ConfigurationFeatureDefinitionProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index b04ffa33..45b4f490 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -164,8 +164,8 @@ We support } else { - requirementType = ParseFeatureDefinitionSectionEnum(configurationSection.Key, configurationSection[RequirementTypeKeyword], requirementType); - status = ParseFeatureDefinitionSectionEnum(configurationSection.Key, configurationSection[FeatureStatusKeyword], status); + requirementType = ParseEnum(configurationSection.Key, configurationSection[RequirementTypeKeyword], requirementType); + status = ParseEnum(configurationSection.Key, configurationSection[FeatureStatusKeyword], status); IEnumerable filterSections = configurationSection.GetSection(FeatureFiltersSectionName).GetChildren(); @@ -267,7 +267,7 @@ private IEnumerable GetFeatureDefinitionSections() } } - private T ParseFeatureDefinitionSectionEnum(string feature, string value, T enumValue) + private T ParseEnum(string feature, string value, T enumValue) where T : struct, Enum { // From 1fb1c6760da32cc70a1435e70095ed3e0b27441c Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 31 Aug 2023 16:33:51 -0700 Subject: [PATCH 71/80] remove parseenum, add ConfigurationFields class --- .../ConfigurationFeatureDefinitionProvider.cs | 104 +++++++++--------- .../ConfigurationFields.cs | 41 +++++++ 2 files changed, 94 insertions(+), 51 deletions(-) create mode 100644 src/Microsoft.FeatureManagement/ConfigurationFields.cs diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 45b4f490..385ff336 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -21,9 +21,6 @@ sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionProvider // IFeatureDefinitionProviderCacheable interface is only used to mark this provider as cacheable. This allows our test suite's // provider to be marked for caching as well. - private const string FeatureFiltersSectionName = "EnabledFor"; - private const string RequirementTypeKeyword = "RequirementType"; - private const string FeatureStatusKeyword = "Status"; private readonly IConfiguration _configuration; private readonly ConcurrentDictionary _definitions; private IDisposable _changeSubscription; @@ -136,8 +133,11 @@ We support */ RequirementType requirementType = RequirementType.Any; - FeatureStatus status = FeatureStatus.Conditional; + + FeatureStatus featureStatus = FeatureStatus.Conditional; + Allocation allocation = null; + List variants = null; var enabledFor = new List(); @@ -146,7 +146,7 @@ We support if (string.IsNullOrEmpty(val)) { - val = configurationSection[FeatureFiltersSectionName]; + val = configurationSection[ConfigurationFields.FeatureFiltersSectionName]; } if (!string.IsNullOrEmpty(val) && bool.TryParse(val, out bool result) && result) @@ -164,76 +164,95 @@ We support } else { - requirementType = ParseEnum(configurationSection.Key, configurationSection[RequirementTypeKeyword], requirementType); - status = ParseEnum(configurationSection.Key, configurationSection[FeatureStatusKeyword], status); + string rawRequirementType = configurationSection[ConfigurationFields.RequirementType]; + + string rawFeatureStatus = configurationSection[ConfigurationFields.FeatureStatus]; + + string parseEnumErrorString = "Invalid {0} with value '{1}' for feature '{2}'."; + + // + // If the enum is specified, parse it and set the return value + if (!string.IsNullOrEmpty(rawRequirementType) && !Enum.TryParse(rawRequirementType, ignoreCase: true, out requirementType)) + { + throw new FeatureManagementException( + FeatureManagementError.InvalidConfigurationSetting, + string.Format(parseEnumErrorString, nameof(RequirementType), rawRequirementType, configurationSection.Key)); + } + + if (!string.IsNullOrEmpty(rawFeatureStatus) && !Enum.TryParse(rawFeatureStatus, ignoreCase: true, out featureStatus)) + { + throw new FeatureManagementException( + FeatureManagementError.InvalidConfigurationSetting, + string.Format(parseEnumErrorString, nameof(FeatureStatus), rawFeatureStatus, configurationSection.Key)); + } - IEnumerable filterSections = configurationSection.GetSection(FeatureFiltersSectionName).GetChildren(); + IEnumerable filterSections = configurationSection.GetSection(ConfigurationFields.FeatureFiltersSectionName).GetChildren(); foreach (IConfigurationSection section in filterSections) { // // 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 _) && !string.IsNullOrEmpty(section[nameof(FeatureFilterConfiguration.Name)])) + if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[ConfigurationFields.NameKeyword])) { enabledFor.Add(new FeatureFilterConfiguration() { - Name = section[nameof(FeatureFilterConfiguration.Name)], - Parameters = new ConfigurationWrapper(section.GetSection(nameof(FeatureFilterConfiguration.Parameters))) + Name = section[ConfigurationFields.NameKeyword], + Parameters = new ConfigurationWrapper(section.GetSection(ConfigurationFields.FeatureFilterConfigurationParameters)) }); } } - IConfigurationSection allocationSection = configurationSection.GetSection(nameof(FeatureDefinition.Allocation)); + IConfigurationSection allocationSection = configurationSection.GetSection(ConfigurationFields.AllocationSectionName); if (allocationSection.Exists()) { allocation = new Allocation() { - DefaultWhenDisabled = allocationSection[nameof(Allocation.DefaultWhenDisabled)], - DefaultWhenEnabled = allocationSection[nameof(Allocation.DefaultWhenEnabled)], - User = allocationSection.GetSection(nameof(Allocation.User)).GetChildren().Select(userAllocation => + DefaultWhenDisabled = allocationSection[ConfigurationFields.AllocationDefaultWhenDisabled], + DefaultWhenEnabled = allocationSection[ConfigurationFields.AllocationDefaultWhenEnabled], + User = allocationSection.GetSection(ConfigurationFields.UserAllocationSectionName).GetChildren().Select(userAllocation => { return new UserAllocation() { - Variant = userAllocation[nameof(UserAllocation.Variant)], - Users = userAllocation.GetSection(nameof(UserAllocation.Users)).Get>() + Variant = userAllocation[ConfigurationFields.AllocationVariantKeyword], + Users = userAllocation.GetSection(ConfigurationFields.UserAllocationUsers).Get>() }; }), - Group = allocationSection.GetSection(nameof(Allocation.Group)).GetChildren().Select(groupAllocation => + Group = allocationSection.GetSection(ConfigurationFields.GroupAllocationSectionName).GetChildren().Select(groupAllocation => { return new GroupAllocation() { - Variant = groupAllocation[nameof(GroupAllocation.Variant)], - Groups = groupAllocation.GetSection(nameof(GroupAllocation.Groups)).Get>() + Variant = groupAllocation[ConfigurationFields.AllocationVariantKeyword], + Groups = groupAllocation.GetSection(ConfigurationFields.GroupAllocationGroups).Get>() }; }), - Percentile = allocationSection.GetSection(nameof(Allocation.Percentile)).GetChildren().Select(percentileAllocation => + Percentile = allocationSection.GetSection(ConfigurationFields.PercentileAllocationSectionName).GetChildren().Select(percentileAllocation => { return new PercentileAllocation() { - Variant = percentileAllocation[nameof(PercentileAllocation.Variant)], - From = percentileAllocation.GetValue(nameof(PercentileAllocation.From)), - To = percentileAllocation.GetValue(nameof(PercentileAllocation.To)) + Variant = percentileAllocation[ConfigurationFields.AllocationVariantKeyword], + From = percentileAllocation.GetValue(ConfigurationFields.PercentileAllocationFrom), + To = percentileAllocation.GetValue(ConfigurationFields.PercentileAllocationTo) }; }), - Seed = allocationSection[nameof(Allocation.Seed)] + Seed = allocationSection[ConfigurationFields.AllocationSeed] }; } - IEnumerable variantsSections = configurationSection.GetSection(nameof(FeatureDefinition.Variants)).GetChildren(); + IEnumerable variantsSections = configurationSection.GetSection(ConfigurationFields.VariantsSectionName).GetChildren(); variants = new List(); foreach (IConfigurationSection section in variantsSections) { - if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[nameof(VariantDefinition.Name)])) + if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[ConfigurationFields.NameKeyword])) { VariantDefinition variant = new VariantDefinition() { - Name = section[nameof(VariantDefinition.Name)], - ConfigurationValue = section.GetSection(nameof(VariantDefinition.ConfigurationValue)), - ConfigurationReference = section[nameof(VariantDefinition.ConfigurationReference)], - StatusOverride = section.GetValue(nameof(VariantDefinition.StatusOverride)) + Name = section[ConfigurationFields.NameKeyword], + ConfigurationValue = section.GetSection(ConfigurationFields.VariantDefinitionConfigurationValue), + ConfigurationReference = section[ConfigurationFields.VariantDefinitionConfigurationReference], + StatusOverride = section.GetValue(ConfigurationFields.VariantDefinitionStatusOverride) }; variants.Add(variant); } @@ -245,7 +264,7 @@ We support Name = configurationSection.Key, EnabledFor = enabledFor, RequirementType = requirementType, - Status = status, + Status = featureStatus, Allocation = allocation, Variants = variants }; @@ -253,33 +272,16 @@ We support private IEnumerable GetFeatureDefinitionSections() { - const string FeatureManagementSectionName = "FeatureManagement"; - - if (_configuration.GetChildren().Any(s => s.Key.Equals(FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase))) + if (_configuration.GetChildren().Any(s => s.Key.Equals(ConfigurationFields.FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase))) { // // Look for feature definitions under the "FeatureManagement" section - return _configuration.GetSection(FeatureManagementSectionName).GetChildren(); + return _configuration.GetSection(ConfigurationFields.FeatureManagementSectionName).GetChildren(); } else { return _configuration.GetChildren(); } } - - private T ParseEnum(string feature, string value, T enumValue) - where T : struct, Enum - { - // - // If the enum is specified, parse it and set the return value - if (!string.IsNullOrEmpty(value) && !Enum.TryParse(value, ignoreCase: true, out enumValue)) - { - throw new FeatureManagementException( - FeatureManagementError.InvalidConfigurationSetting, - $"Invalid {typeof(T).Name} with value '{value}' for feature '{feature}'."); - } - - return enumValue; - } } } diff --git a/src/Microsoft.FeatureManagement/ConfigurationFields.cs b/src/Microsoft.FeatureManagement/ConfigurationFields.cs new file mode 100644 index 00000000..3642da21 --- /dev/null +++ b/src/Microsoft.FeatureManagement/ConfigurationFields.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// + +namespace Microsoft.FeatureManagement +{ + internal static class ConfigurationFields + { + // Enum keywords + public const string RequirementType = "RequirementType"; + public const string FeatureStatus = "Status"; + + // Feature filters keywords + public const string FeatureFiltersSectionName = "EnabledFor"; + public const string FeatureFilterConfigurationParameters = "Parameters"; + + // Allocation keywords + public const string AllocationSectionName = "Allocation"; + public const string AllocationDefaultWhenDisabled = "DefaultWhenDisabled"; + public const string AllocationDefaultWhenEnabled = "DefaultWhenEnabled"; + public const string UserAllocationSectionName = "User"; + public const string AllocationVariantKeyword = "Variant"; + public const string UserAllocationUsers = "Users"; + public const string GroupAllocationSectionName = "Group"; + public const string GroupAllocationGroups = "Groups"; + public const string PercentileAllocationSectionName = "Percentile"; + public const string PercentileAllocationFrom = "From"; + public const string PercentileAllocationTo = "To"; + public const string AllocationSeed = "Seed"; + + // Variants keywords + public const string VariantsSectionName = "Variants"; + public const string VariantDefinitionConfigurationValue = "ConfigurationValue"; + public const string VariantDefinitionConfigurationReference = "ConfigurationReference"; + public const string VariantDefinitionStatusOverride = "StatusOverride"; + + // Other keywords + public const string NameKeyword = "Name"; + public const string FeatureManagementSectionName = "FeatureManagement"; + } +} From 6dfb3ec4007d3af5f59778c952872a78c7db639a Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Wed, 6 Sep 2023 17:13:06 -0700 Subject: [PATCH 72/80] test failing, fixed PR revisions --- .../ConfigurationFeatureDefinitionProvider.cs | 83 +++++++++++++++---- .../FeatureManagement.cs | 18 ++++ .../Tests.FeatureManagement/appsettings.json | 39 +++++++++ 3 files changed, 125 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 385ff336..5c74c8a7 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -6,6 +6,8 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -26,6 +28,8 @@ sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionProvider private IDisposable _changeSubscription; private int _stale = 0; + const string ParseValueErrorString = "Invalid {0} with value '{1}' for feature '{2}'."; + public ConfigurationFeatureDefinitionProvider(IConfiguration configuration) { _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); @@ -168,22 +172,14 @@ We support string rawFeatureStatus = configurationSection[ConfigurationFields.FeatureStatus]; - string parseEnumErrorString = "Invalid {0} with value '{1}' for feature '{2}'."; - - // - // If the enum is specified, parse it and set the return value - if (!string.IsNullOrEmpty(rawRequirementType) && !Enum.TryParse(rawRequirementType, ignoreCase: true, out requirementType)) + if (!string.IsNullOrEmpty(rawRequirementType)) { - throw new FeatureManagementException( - FeatureManagementError.InvalidConfigurationSetting, - string.Format(parseEnumErrorString, nameof(RequirementType), rawRequirementType, configurationSection.Key)); + requirementType = ParseEnum(configurationSection.Key, rawRequirementType, ConfigurationFields.RequirementType); } - if (!string.IsNullOrEmpty(rawFeatureStatus) && !Enum.TryParse(rawFeatureStatus, ignoreCase: true, out featureStatus)) + if (!string.IsNullOrEmpty(rawFeatureStatus)) { - throw new FeatureManagementException( - FeatureManagementError.InvalidConfigurationSetting, - string.Format(parseEnumErrorString, nameof(FeatureStatus), rawFeatureStatus, configurationSection.Key)); + featureStatus = ParseEnum(configurationSection.Key, rawFeatureStatus, ConfigurationFields.FeatureStatus); } IEnumerable filterSections = configurationSection.GetSection(ConfigurationFields.FeatureFiltersSectionName).GetChildren(); @@ -229,11 +225,29 @@ We support }), Percentile = allocationSection.GetSection(ConfigurationFields.PercentileAllocationSectionName).GetChildren().Select(percentileAllocation => { + double from = 0; + + double to = 0; + + string rawFrom = percentileAllocation[ConfigurationFields.PercentileAllocationFrom]; + + string rawTo = percentileAllocation[ConfigurationFields.PercentileAllocationTo]; + + if (!string.IsNullOrEmpty(rawFrom)) + { + from = ParseDouble(configurationSection.Key, rawFrom, ConfigurationFields.PercentileAllocationFrom); + } + + if (!string.IsNullOrEmpty(rawTo)) + { + to = ParseDouble(configurationSection.Key, rawTo, ConfigurationFields.PercentileAllocationTo); + } + return new PercentileAllocation() { Variant = percentileAllocation[ConfigurationFields.AllocationVariantKeyword], - From = percentileAllocation.GetValue(ConfigurationFields.PercentileAllocationFrom), - To = percentileAllocation.GetValue(ConfigurationFields.PercentileAllocationTo) + From = from, + To = to }; }), Seed = allocationSection[ConfigurationFields.AllocationSeed] @@ -247,13 +261,23 @@ We support { if (int.TryParse(section.Key, out int _) && !string.IsNullOrEmpty(section[ConfigurationFields.NameKeyword])) { + StatusOverride statusOverride = StatusOverride.None; + + string rawStatusOverride = section[ConfigurationFields.VariantDefinitionStatusOverride]; + + if (!string.IsNullOrEmpty(rawStatusOverride)) + { + statusOverride = ParseEnum(configurationSection.Key, rawStatusOverride, ConfigurationFields.VariantDefinitionStatusOverride); + } + VariantDefinition variant = new VariantDefinition() { Name = section[ConfigurationFields.NameKeyword], ConfigurationValue = section.GetSection(ConfigurationFields.VariantDefinitionConfigurationValue), ConfigurationReference = section[ConfigurationFields.VariantDefinitionConfigurationReference], - StatusOverride = section.GetValue(ConfigurationFields.VariantDefinitionStatusOverride) + StatusOverride = statusOverride }; + variants.Add(variant); } } @@ -283,5 +307,34 @@ private IEnumerable GetFeatureDefinitionSections() return _configuration.GetChildren(); } } + + private T ParseEnum(string feature, string rawValue, string fieldKeyword) + where T: struct, Enum + { + Debug.Assert(!string.IsNullOrEmpty(rawValue)); + + if (!Enum.TryParse(rawValue, ignoreCase: true, out T value)) + { + throw new FeatureManagementException( + FeatureManagementError.InvalidConfigurationSetting, + string.Format(ParseValueErrorString, fieldKeyword, rawValue, feature)); + } + + return value; + } + + private double ParseDouble(string feature, string rawValue, string fieldKeyword) + { + Debug.Assert(!string.IsNullOrEmpty(rawValue)); + + if (!double.TryParse(rawValue, out double value)) + { + throw new FeatureManagementException( + FeatureManagementError.InvalidConfigurationSetting, + string.Format(ParseValueErrorString, fieldKeyword, rawValue, feature)); + } + + return value; + } } } diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index cb1c3036..632f41bc 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -1056,6 +1056,24 @@ public async Task VariantsInvalidScenarios() variant = await featureManager.GetVariantAsync("VariantFeatureBothConfigurations", cancellationToken); Assert.Equal("600px", variant.Configuration.Value); + + // Verify that an exception is thrown for invalid StatusOverride value + FeatureManagementException e = await Assert.ThrowsAsync(async () => + { + variant = await featureManager.GetVariantAsync("VariantFeatureInvalidStatusOverride", cancellationToken); + }); + + Assert.Equal(FeatureManagementError.InvalidConfigurationSetting, e.Error); + Assert.Contains(ConfigurationFields.VariantDefinitionStatusOverride, e.Message); + + // Verify that an exception is thrown for invalid doubles From and To in the Percentile section + e = await Assert.ThrowsAsync(async () => + { + variant = await featureManager.GetVariantAsync("VariantFeatureInvalidFromTo", cancellationToken); + }); + + Assert.Equal(FeatureManagementError.InvalidConfigurationSetting, e.Error); + Assert.Contains(ConfigurationFields.PercentileAllocationFrom, e.Message); } private static void DisableEndpointRouting(MvcOptions options) diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index 3c754bbe..ba6a18d7 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -356,6 +356,45 @@ "Name": "On" } ] + }, + "VariantFeatureInvalidStatusOverride": { + "Allocation": { + "DefaultWhenEnabled": "Small" + }, + "Variants": [ + { + "Name": "Small", + "ConfigurationValue": "300px", + "StatusOverride": "InvalidValue" + } + ], + "EnabledFor": [ + { + "Name": "On" + } + ] + }, + "VariantFeatureInvalidFromTo": { + "Allocation": { + "Percentile": [ + { + "Variant": "Small", + "From": "Invalid", + "To": "Invalid" + } + ] + }, + "Variants": [ + { + "Name": "Small", + "ConfigurationReference": "ShoppingCart:Small" + } + ], + "EnabledFor": [ + { + "Name": "On" + } + ] } } } From 827c0ae31b71211073fb1b5e0b4f01823dcf756c Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 7 Sep 2023 10:58:57 -0700 Subject: [PATCH 73/80] fix invalid scenarios test --- tests/Tests.FeatureManagement/FeatureManagement.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index 632f41bc..b43f406f 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -1037,6 +1037,12 @@ public async Task VariantsInvalidScenarios() .AddSingleton(config) .AddFeatureManagement(); + targetingContextAccessor.Current = new TargetingContext + { + UserId = "Marsha", + Groups = new List { "Group1" } + }; + ServiceProvider serviceProvider = services.BuildServiceProvider(); IVariantFeatureManager featureManager = serviceProvider.GetRequiredService(); From 656ec6759ed11374e8bd4989b34f109b8c3d6572 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 7 Sep 2023 11:00:28 -0700 Subject: [PATCH 74/80] simplify context in test --- tests/Tests.FeatureManagement/FeatureManagement.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index b43f406f..b690efc9 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -1039,8 +1039,7 @@ public async Task VariantsInvalidScenarios() targetingContextAccessor.Current = new TargetingContext { - UserId = "Marsha", - Groups = new List { "Group1" } + UserId = "Jeff" }; ServiceProvider serviceProvider = services.BuildServiceProvider(); From bed80931adb83e9166d174295f648931025ee4f3 Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 7 Sep 2023 11:12:06 -0700 Subject: [PATCH 75/80] remove unused using --- .../ConfigurationFeatureDefinitionProvider.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 5c74c8a7..c30bac05 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -7,7 +7,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; From 03e8e47c59ad3f1532266f713a16dea121b65ebc Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Thu, 7 Sep 2023 13:28:45 -0700 Subject: [PATCH 76/80] remove unused param --- src/Microsoft.FeatureManagement/FeatureManager.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 7ed3eea2..b365129a 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -119,7 +119,6 @@ private async Task IsEnabledWithVariantsAsync(string feature, TC variantDefinition = await GetAssignedVariantAsync( featureDefinition, targetingContext, - isFeatureEnabled, cancellationToken) .ConfigureAwait(false); } @@ -358,7 +357,7 @@ private async ValueTask GetVariantAsync(string feature, TargetingContex context = await ResolveTargetingContextAsync(cancellationToken).ConfigureAwait(false); } - variantDefinition = await GetAssignedVariantAsync(featureDefinition, context, isFeatureEnabled, cancellationToken).ConfigureAwait(false); + variantDefinition = await GetAssignedVariantAsync(featureDefinition, context, cancellationToken).ConfigureAwait(false); } if (variantDefinition == null) @@ -419,7 +418,7 @@ private async ValueTask ResolveTargetingContextAsync(Cancellat return context; } - private async ValueTask GetAssignedVariantAsync(FeatureDefinition featureDefinition, TargetingContext context, bool isFeatureEnabled, CancellationToken cancellationToken) + private async ValueTask GetAssignedVariantAsync(FeatureDefinition featureDefinition, TargetingContext context, CancellationToken cancellationToken) { VariantDefinition variantDefinition = null; From e1cb0d26b94388ab88297474d1ee2088653c5cb7 Mon Sep 17 00:00:00 2001 From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:03:05 -0700 Subject: [PATCH 77/80] Clarify how From and To bounds work in PercentileAllocation Co-authored-by: Ross Grambo --- .../Allocation/PercentileAllocation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.FeatureManagement/Allocation/PercentileAllocation.cs b/src/Microsoft.FeatureManagement/Allocation/PercentileAllocation.cs index 44d9015c..341d7d5d 100644 --- a/src/Microsoft.FeatureManagement/Allocation/PercentileAllocation.cs +++ b/src/Microsoft.FeatureManagement/Allocation/PercentileAllocation.cs @@ -15,12 +15,12 @@ public class PercentileAllocation public string Variant { get; set; } /// - /// The lower bound of the percentage to which the variant will be assigned. + /// The inclusive lower bound of the percentage to which the variant will be assigned. /// public double From { get; set; } /// - /// The upper bound of the percentage to which the variant will be assigned. + /// The exclusive upper bound of the percentage to which the variant will be assigned. /// public double To { get; set; } } From 23ff1efdda53bdc4c3d220e818d35875640ce61c Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Fri, 8 Sep 2023 13:10:24 -0700 Subject: [PATCH 78/80] fix error messages --- .../ConfigurationFeatureDefinitionProvider.cs | 2 +- src/Microsoft.FeatureManagement/FeatureManager.cs | 6 +++--- .../Targeting/TargetingEvaluator.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index c30bac05..8e84aecb 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -27,7 +27,7 @@ sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionProvider private IDisposable _changeSubscription; private int _stale = 0; - const string ParseValueErrorString = "Invalid {0} with value '{1}' for feature '{2}'."; + const string ParseValueErrorString = "Invalid setting '{0}' with value '{1}' for feature '{2}'."; public ConfigurationFeatureDefinitionProvider(IConfiguration configuration) { diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index b365129a..100cbfd3 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -447,7 +447,7 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur { if (string.IsNullOrEmpty(user.Variant)) { - _logger.LogWarning($"Missing variant name for {nameof(featureDefinition.Allocation.User)} allocation in feature {featureDefinition.Name}"); + _logger.LogWarning($"Missing variant name for user allocation in feature {featureDefinition.Name}"); return new ValueTask((VariantDefinition)null); } @@ -470,7 +470,7 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur { if (string.IsNullOrEmpty(group.Variant)) { - _logger.LogWarning($"Missing variant name for {nameof(featureDefinition.Allocation.Group)} allocation in feature {featureDefinition.Name}"); + _logger.LogWarning($"Missing variant name for group allocation in feature {featureDefinition.Name}"); return new ValueTask((VariantDefinition)null); } @@ -498,7 +498,7 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur { if (string.IsNullOrEmpty(percentile.Variant)) { - _logger.LogWarning($"Missing variant name for {nameof(featureDefinition.Allocation.Percentile)} allocation in feature {featureDefinition.Name}"); + _logger.LogWarning($"Missing variant name for percentile allocation in feature {featureDefinition.Name}"); return new ValueTask((VariantDefinition)null); } diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs index 998380f2..cc809e27 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs @@ -320,7 +320,7 @@ public static bool IsTargeted(ITargetingContext targetingContext, double from, d if (from > to) { - throw new ArgumentException($"Double {nameof(from)} cannot be larger than double {nameof(to)}."); + throw new ArgumentException($"Value of {nameof(from)} cannot be larger than value of {nameof(to)}."); } string userId = ignoreCase ? From 3827494d55a0d691f8c6801839809e4064e21d9c Mon Sep 17 00:00:00 2001 From: AMER JUSUPOVIC Date: Mon, 11 Sep 2023 12:18:15 -0700 Subject: [PATCH 79/80] add feature name as default seed with allocation prefix --- src/Microsoft.FeatureManagement/FeatureManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 100cbfd3..956026ae 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.FeatureManagement.FeatureFilters; using Microsoft.FeatureManagement.Targeting; @@ -494,7 +495,7 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur percentile.From, percentile.To, _assignerOptions.IgnoreCase, - featureDefinition.Allocation.Seed)) + featureDefinition.Allocation.Seed ?? $"allocation{featureDefinition.Name}")) { if (string.IsNullOrEmpty(percentile.Variant)) { From 755687e7b8882c8fb3c34c4874d83a274be99dd4 Mon Sep 17 00:00:00 2001 From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:53:20 -0700 Subject: [PATCH 80/80] Update src/Microsoft.FeatureManagement/FeatureManager.cs Co-authored-by: Jimmy Campbell --- src/Microsoft.FeatureManagement/FeatureManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 956026ae..76a30859 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -495,7 +495,7 @@ private ValueTask AssignVariantAsync(FeatureDefinition featur percentile.From, percentile.To, _assignerOptions.IgnoreCase, - featureDefinition.Allocation.Seed ?? $"allocation{featureDefinition.Name}")) + featureDefinition.Allocation.Seed ?? $"allocation\n{featureDefinition.Name}")) { if (string.IsNullOrEmpty(percentile.Variant)) {