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 bd88800a..427df019 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs @@ -106,8 +106,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 8763b850..3cf36d30 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; @@ -40,7 +41,7 @@ public void Dispose() _changeSubscription = null; } - public Task GetFeatureDefinitionAsync(string featureName) + public Task GetFeatureDefinitionAsync(string featureName, CancellationToken _) { if (featureName == null) { @@ -63,7 +64,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 e642eb35..c63441cc 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 @@ -30,8 +31,9 @@ public PercentageFilter(ILoggerFactory loggerFactory) /// 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) { PercentageFilterSettings settings = context.Parameters.Get() ?? new PercentageFilterSettings(); diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index 97c21b07..4c3abe48 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 @@ -30,8 +31,9 @@ public TimeWindowFilter(ILoggerFactory loggerFactory) /// 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) { TimeWindowFilterSettings settings = context.Parameters.Get() ?? new TimeWindowFilterSettings(); diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 25873f50..c57f03c5 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -7,6 +7,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 @@ -40,29 +42,31 @@ public FeatureManager( _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); } - 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; } } - 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) { @@ -72,7 +76,9 @@ private async Task IsEnabledAsync(string feature, TContext appCo bool enabled = false; - FeatureDefinition featureDefinition = await _featureDefinitionProvider.GetFeatureDefinitionAsync(feature).ConfigureAwait(false); + FeatureDefinition featureDefinition = await _featureDefinitionProvider + .GetFeatureDefinitionAsync(feature, cancellationToken) + .ConfigureAwait(false); if (featureDefinition != null) { @@ -122,7 +128,9 @@ private async Task IsEnabledAsync(string feature, TContext appCo { ContextualFeatureFilterEvaluator contextualFilter = GetContextualFeatureFilter(featureFilterConfiguration.Name, typeof(TContext)); - if (contextualFilter != null && await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false)) + if (contextualFilter != null && await contextualFilter + .EvaluateAsync(context, appContext, cancellationToken) + .ConfigureAwait(false)) { enabled = true; @@ -132,7 +140,9 @@ private async Task IsEnabledAsync(string feature, TContext appCo // // IFeatureFilter - if (filter is IFeatureFilter featureFilter && await featureFilter.EvaluateAsync(context).ConfigureAwait(false)) + if (filter is IFeatureFilter featureFilter && await featureFilter + .EvaluateAsync(context, cancellationToken) + .ConfigureAwait(false)) { enabled = true; @@ -144,7 +154,7 @@ private async Task IsEnabledAsync(string feature, TContext appCo 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 a218e1cc..46549f4d 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs @@ -3,6 +3,8 @@ // using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement @@ -21,13 +23,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); } @@ -41,7 +43,7 @@ public async IAsyncEnumerable GetFeatureNamesAsync() } } - public async Task IsEnabledAsync(string feature) + public async Task IsEnabledAsync(string feature, CancellationToken cancellationToken) { // // First, check local cache @@ -50,14 +52,14 @@ public async Task IsEnabledAsync(string feature) return _flagCache[feature]; } - bool enabled = await _featureManager.IsEnabledAsync(feature).ConfigureAwait(false); + bool enabled = await _featureManager.IsEnabledAsync(feature, cancellationToken).ConfigureAwait(false); _flagCache[feature] = enabled; return enabled; } - public async Task IsEnabledAsync(string feature, TContext context) + public async Task IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken) { // // First, check local cache @@ -66,7 +68,7 @@ public async Task IsEnabledAsync(string feature, TContext contex return _flagCache[feature]; } - bool enabled = await _featureManager.IsEnabledAsync(feature, context).ConfigureAwait(false); + bool enabled = await _featureManager.IsEnabledAsync(feature, context, cancellationToken).ConfigureAwait(false); _flagCache[feature] = enabled; 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 1b4ea0cf..d9b07e6a 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 @@ -14,22 +15,25 @@ 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. /// /// 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); /// /// 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); } } 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/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj index 3bb53e2d..878778f0 100644 --- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj +++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj @@ -3,9 +3,10 @@ - 2 - 3 + 3 + 0 0 + -preview diff --git a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs index a2d4404e..a43c1a80 100644 --- a/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs +++ b/src/Microsoft.FeatureManagement/Targeting/ContextualTargetingFilter.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Security.Cryptography; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters @@ -40,9 +41,10 @@ public ContextualTargetingFilter(IOptions options, I /// /// 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 283a6260..93e753ef 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.FeatureManagement.FeatureFilters @@ -36,9 +37,10 @@ public TargetingFilter(IOptions options, ITargetingC /// 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) { @@ -47,7 +49,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 @@ -60,7 +62,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 e30888e4..0a123ec2 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 true; }; - await featureManager.IsEnabledAsync(ConditionalFeature); + await featureManager.IsEnabledAsync(ConditionalFeature, CancellationToken.None); Assert.True(called); } @@ -204,10 +205,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] @@ -234,7 +235,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++; } @@ -272,21 +273,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 @@ -294,7 +295,7 @@ public async Task Targeting() { UserId = "Patty", Groups = new List() { "Ring1" } - })); + }, CancellationToken.None)); // // Not targeted by user id, default rollout or group rollout @@ -302,7 +303,7 @@ public async Task Targeting() { UserId = "Isaac", Groups = new List() { "Ring1" } - })); + }, CancellationToken.None)); } [Fact] @@ -340,7 +341,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 @@ -349,7 +350,7 @@ public async Task TargetingAccessor() UserId = "Patty" }; - Assert.False(await featureManager.IsEnabledAsync(beta)); + Assert.False(await featureManager.IsEnabledAsync(beta, CancellationToken.None)); } [Fact] @@ -382,11 +383,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] @@ -428,7 +429,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; @@ -454,7 +455,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); } @@ -480,7 +482,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); } @@ -533,7 +535,7 @@ public async Task CustomFeatureDefinitionProvider() return 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 91cbcefd..3aad4c8c 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 ed236742..be6e3be7 100644 --- a/tests/Tests.FeatureManagement/TestFilter.cs +++ b/tests/Tests.FeatureManagement/TestFilter.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 TestFilter : IFeatureFilter { public Func Callback { get; set; } - public Task EvaluateAsync(FeatureFilterEvaluationContext context) + public Task EvaluateAsync(FeatureFilterEvaluationContext context, CancellationToken _) { return Task.FromResult(Callback?.Invoke(context) ?? false); }