diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 40a0f0a7..d9e8a99d 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -144,7 +144,7 @@ public async 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. + /// A context that provides information to evaluate whether a feature should be on or off. /// True if the feature is enabled, otherwise false. public async Task IsEnabledAsync(string feature, TContext appContext) { @@ -170,7 +170,7 @@ public async ValueTask IsEnabledAsync(string feature, CancellationToken ca /// 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. + /// A context that provides information 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. public async ValueTask IsEnabledAsync(string feature, TContext appContext, CancellationToken cancellationToken = default) @@ -216,7 +216,7 @@ public async ValueTask GetVariantAsync(string feature, CancellationToke throw new ArgumentNullException(nameof(feature)); } - EvaluationEvent evaluationEvent = await EvaluateFeature(feature, context: null, useContext: false, cancellationToken); + EvaluationEvent evaluationEvent = await EvaluateFeature(feature, context: null, useContext: false, cancellationToken); return evaluationEvent.Variant; } @@ -225,10 +225,10 @@ public async ValueTask GetVariantAsync(string feature, CancellationToke /// Gets the assigned variant for a specific feature. /// /// The name of the feature to evaluate. - /// An instance of used to evaluate which variant the user will be assigned. + /// A context that provides information to evaluate which variant will be assigned to the user. /// The cancellation token to cancel the operation. /// A variant assigned to the user based on the feature's configured allocation. - public async ValueTask GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken = default) + public async ValueTask GetVariantAsync(string feature, ITargetingContext context, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(feature)) { @@ -262,15 +262,19 @@ private async ValueTask EvaluateFeature(string featur // // Determine Targeting Context - TargetingContext targetingContext; + TargetingContext targetingContext = null; - if (useContext) + if (!useContext) { - targetingContext = context as TargetingContext; + targetingContext = await ResolveTargetingContextAsync(cancellationToken).ConfigureAwait(false); } - else + else if (context is ITargetingContext targetingInfo) { - targetingContext = await ResolveTargetingContextAsync(cancellationToken).ConfigureAwait(false); + targetingContext = new TargetingContext + { + UserId = targetingInfo.UserId, + Groups = targetingInfo.Groups + }; } evaluationEvent.TargetingContext = targetingContext; @@ -314,7 +318,7 @@ private async ValueTask EvaluateFeature(string featur if (useContext) { - message = $"A {nameof(TargetingContext)} required for variant assignment was not provided."; + message = $"The context of type {context.GetType().Name} does not implement {nameof(ITargetingContext)} for variant assignment."; } else if (TargetingContextAccessor == null) { @@ -496,7 +500,7 @@ private async ValueTask IsEnabledAsync(FeatureDefinition feature if (useAppContext) { - filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name, typeof(TContext)) ?? + filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name, appContext.GetType()) ?? GetFeatureFilterMetadata(featureFilterConfiguration.Name); } else @@ -538,7 +542,7 @@ private async ValueTask IsEnabledAsync(FeatureDefinition feature // IContextualFeatureFilter if (useAppContext) { - ContextualFeatureFilterEvaluator contextualFilter = GetContextualFeatureFilter(featureFilterConfiguration.Name, typeof(TContext)); + ContextualFeatureFilterEvaluator contextualFilter = GetContextualFeatureFilter(featureFilterConfiguration.Name, appContext.GetType()); if (contextualFilter != null && await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) == targetEvaluation) diff --git a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs index 6018931d..384bb2b1 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs @@ -97,7 +97,7 @@ public async ValueTask GetVariantAsync(string feature, CancellationToke return variant; } - public async ValueTask GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken) + public async ValueTask GetVariantAsync(string feature, ITargetingContext context, CancellationToken cancellationToken) { string cacheKey = GetVariantCacheKey(feature); diff --git a/src/Microsoft.FeatureManagement/IFeatureManager.cs b/src/Microsoft.FeatureManagement/IFeatureManager.cs index 1b4ea0cf..5f1006f5 100644 --- a/src/Microsoft.FeatureManagement/IFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IFeatureManager.cs @@ -28,7 +28,7 @@ public interface IFeatureManager /// 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. + /// A context that provides information to evaluate whether a feature should be on or off. /// True if the feature is enabled, otherwise false. Task IsEnabledAsync(string feature, TContext context); } diff --git a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs index 0b78a237..505b6652 100644 --- a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs @@ -32,7 +32,7 @@ public interface IVariantFeatureManager /// 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. + /// A context that provides information 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. ValueTask IsEnabledAsync(string feature, TContext context, CancellationToken cancellationToken = default); @@ -49,9 +49,9 @@ public interface IVariantFeatureManager /// Gets the assigned variant for a specific feature. /// /// The name of the feature to evaluate. - /// An instance of used to evaluate which variant the user will be assigned. + /// A context that provides information to evaluate which variant will be assigned to the user. /// 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, ITargetingContext context, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/VariantFeatureManagerExtensions.cs b/src/Microsoft.FeatureManagement/VariantFeatureManagerExtensions.cs new file mode 100644 index 00000000..97385729 --- /dev/null +++ b/src/Microsoft.FeatureManagement/VariantFeatureManagerExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.FeatureManagement.FeatureFilters; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement +{ + /// + /// Extensions for . + /// + public static class VariantFeatureManagerExtensions + { + /// + /// Gets the assigned variant for a specific feature. + /// + /// The instance. + /// 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 configured allocation. + public static ValueTask GetVariantAsync(this IVariantFeatureManager variantFeatureManager, string feature, TargetingContext context, CancellationToken cancellationToken = default) + { + return variantFeatureManager.GetVariantAsync(feature, context, cancellationToken); + } + } +} diff --git a/tests/Tests.FeatureManagement/AppContext.cs b/tests/Tests.FeatureManagement/AppContext.cs index 5431ac52..a55538e5 100644 --- a/tests/Tests.FeatureManagement/AppContext.cs +++ b/tests/Tests.FeatureManagement/AppContext.cs @@ -1,10 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using Microsoft.FeatureManagement.FeatureFilters; +using System.Collections.Generic; + namespace Tests.FeatureManagement { - class AppContext : IAccountContext + class AppContext : IAccountContext, ITargetingContext { public string AccountId { get; set; } + + public string UserId { get; set; } + + public IEnumerable Groups { get; set; } } } diff --git a/tests/Tests.FeatureManagement/FeatureManagementTest.cs b/tests/Tests.FeatureManagement/FeatureManagementTest.cs index e1a6efe1..48551376 100644 --- a/tests/Tests.FeatureManagement/FeatureManagementTest.cs +++ b/tests/Tests.FeatureManagement/FeatureManagementTest.cs @@ -503,7 +503,7 @@ public async Task UsesContext() IFeatureManager featureManager = provider.GetRequiredService(); - AppContext context = new AppContext(); + var context = new AppContext(); context.AccountId = "NotEnabledAccount"; @@ -1641,6 +1641,53 @@ public async Task VariantBasedInjection() } ); } + + [Fact] + public async Task VariantFeatureFlagWithContextualFeatureFilter() + { + IConfiguration configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + + IServiceCollection services = new ServiceCollection(); + + services.AddSingleton(configuration) + .AddFeatureManagement() + .AddFeatureFilter(); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + ContextualTestFilter contextualTestFeatureFilter = (ContextualTestFilter)serviceProvider.GetRequiredService>().First(f => f is ContextualTestFilter); + + contextualTestFeatureFilter.ContextualCallback = (ctx, accountContext) => + { + var allowedAccounts = new List(); + + ctx.Parameters.Bind("AllowedAccounts", allowedAccounts); + + return allowedAccounts.Contains(accountContext.AccountId); + }; + + IVariantFeatureManager featureManager = serviceProvider.GetRequiredService(); + + var context = new AppContext(); + + context.AccountId = "NotEnabledAccount"; + + Assert.False(await featureManager.IsEnabledAsync(Features.ContextualFeatureWithVariant, context)); + + Variant variant = await featureManager.GetVariantAsync(Features.ContextualFeatureWithVariant, context); + + Assert.Equal("Small", variant.Name); + + context.AccountId = "abc"; + + Assert.True(await featureManager.IsEnabledAsync(Features.ContextualFeatureWithVariant, context)); + + variant = await featureManager.GetVariantAsync(Features.ContextualFeatureWithVariant, context); + + Assert.Equal("Big", variant.Name); + } } public class FeatureManagementTelemetryTest diff --git a/tests/Tests.FeatureManagement/Features.cs b/tests/Tests.FeatureManagement/Features.cs index 51eef015..9562c9bb 100644 --- a/tests/Tests.FeatureManagement/Features.cs +++ b/tests/Tests.FeatureManagement/Features.cs @@ -30,5 +30,6 @@ static class Features public const string VariantImplementationFeature = "VariantImplementationFeature"; public const string OnTelemetryTestFeature = "OnTelemetryTestFeature"; public const string OffTelemetryTestFeature = "OffTelemetryTestFeature"; + public const string ContextualFeatureWithVariant = "ContextualFeatureWithVariant"; } } diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index cf72cce4..0556fdf0 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -530,6 +530,34 @@ "telemetry": { "enabled": true } + }, + { + "id": "ContextualFeatureWithVariant", + "enabled": true, + "conditions": { + "client_filters": [ + { + "name": "ContextualTest", + "parameters": { + "AllowedAccounts": [ + "abc" + ] + } + } + ] + }, + "variants": [ + { + "name": "Big" + }, + { + "name": "Small" + } + ], + "allocation": { + "default_when_enabled": "Big", + "default_when_disabled": "Small" + } } ] }