diff --git a/src/Microsoft.FeatureManagement/Allocation/Allocation.cs b/src/Microsoft.FeatureManagement/Allocation/Allocation.cs index b10ce33d..4a72df13 100644 --- a/src/Microsoft.FeatureManagement/Allocation/Allocation.cs +++ b/src/Microsoft.FeatureManagement/Allocation/Allocation.cs @@ -35,6 +35,11 @@ public class Allocation /// public IEnumerable Percentile { get; set; } + /// + /// Describes a mapping of contextual filters to variants. + /// + public IEnumerable AllocatedFor { get; set; } + /// /// Maps users to the same percentile across multiple feature flags. /// diff --git a/src/Microsoft.FeatureManagement/Allocation/ContextualFilterAllocation.cs b/src/Microsoft.FeatureManagement/Allocation/ContextualFilterAllocation.cs new file mode 100644 index 00000000..f4775521 --- /dev/null +++ b/src/Microsoft.FeatureManagement/Allocation/ContextualFilterAllocation.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// + +namespace Microsoft.FeatureManagement +{ + /// + /// The definition of a filter allocation. + /// + public class ContextualFilterAllocation : FeatureFilterConfiguration + { + /// + /// The name of the variant. + /// + public string Variant { get; set; } + } +} diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index e5d4d1b1..4d46d42b 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -451,6 +451,15 @@ private FeatureDefinition ParseMicrosoftSchemaFeatureDefinition(IConfigurationSe To = to }; }), + AllocatedFor = allocationSection.GetSection(MicrosoftFeatureManagementFields.ClientFilters).GetChildren().Select(filterAllocation => + { + return new ContextualFilterAllocation() + { + Name = filterAllocation[MicrosoftFeatureManagementFields.Name], + Parameters = new ConfigurationWrapper(filterAllocation.GetSection(MicrosoftFeatureManagementFields.Parameters)), + Variant = filterAllocation[MicrosoftFeatureManagementFields.AllocationVariantKeyword] + }; + }), Seed = allocationSection[MicrosoftFeatureManagementFields.AllocationSeed] }; } diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 2661bf88..17a8d9a8 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -223,7 +223,31 @@ public async ValueTask GetVariantAsync(string feature, CancellationToke /// 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, ITargetingContext context, CancellationToken cancellationToken = default) + // public async ValueTask GetVariantAsync(string feature, ITargetingContext context, CancellationToken cancellationToken = default) + // { + // if (string.IsNullOrEmpty(feature)) + // { + // throw new ArgumentNullException(nameof(feature)); + // } + + // if (context == null) + // { + // throw new ArgumentNullException(nameof(context)); + // } + + // EvaluationEvent evaluationEvent = await EvaluateFeature(feature, context, useContext: true, cancellationToken); + + // return evaluationEvent.Variant; + // } + + /// + /// Gets the assigned variant for a specific feature. + /// + /// The name of the feature to evaluate. + /// 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, TContext context, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(feature)) { @@ -307,15 +331,11 @@ private async ValueTask EvaluateFeature(string featur } else { - if (targetingContext == null) + if (context == null && targetingContext == null) { string message; - if (useContext) - { - message = $"The context of type {context.GetType().Name} does not implement {nameof(ITargetingContext)} for variant assignment."; - } - else if (TargetingContextAccessor == null) + if (TargetingContextAccessor == null) { message = $"No instance of {nameof(ITargetingContextAccessor)} could be found for variant assignment."; } @@ -331,6 +351,10 @@ private async ValueTask EvaluateFeature(string featur { variantDefinition = await AssignVariantAsync(evaluationEvent, targetingContext, cancellationToken).ConfigureAwait(false); } + else if (context != null && evaluationEvent.FeatureDefinition.Allocation != null) + { + variantDefinition = await AssignVariantAsync(evaluationEvent, context, cancellationToken).ConfigureAwait(false); + } if (evaluationEvent.VariantAssignmentReason == VariantAssignmentReason.None) { @@ -600,100 +624,188 @@ private async ValueTask ResolveTargetingContextAsync(Cancellat return context; } - private ValueTask AssignVariantAsync(EvaluationEvent evaluationEvent, TargetingContext targetingContext, CancellationToken cancellationToken) + private async ValueTask AssignVariantAsync(EvaluationEvent evaluationEvent, TContext context, CancellationToken cancellationToken) { Debug.Assert(evaluationEvent != null); - Debug.Assert(targetingContext != null); - Debug.Assert(evaluationEvent.FeatureDefinition.Allocation != null); VariantDefinition variant = null; - if (evaluationEvent.FeatureDefinition.Allocation.User != null) + if (context is ITargetingContext targetingContext) { - foreach (UserAllocation user in evaluationEvent.FeatureDefinition.Allocation.User) + if (evaluationEvent.FeatureDefinition.Allocation.User != null) { - if (TargetingEvaluator.IsTargeted(targetingContext.UserId, user.Users, _assignerOptions.IgnoreCase)) + foreach (UserAllocation user in evaluationEvent.FeatureDefinition.Allocation.User) { - if (string.IsNullOrEmpty(user.Variant)) + if (TargetingEvaluator.IsTargeted(targetingContext.UserId, user.Users, _assignerOptions.IgnoreCase)) { - Logger?.LogWarning($"Missing variant name for user allocation in feature {evaluationEvent.FeatureDefinition.Name}"); + if (string.IsNullOrEmpty(user.Variant)) + { + Logger?.LogWarning($"Missing variant name for user allocation in feature {evaluationEvent.FeatureDefinition.Name}"); - return new ValueTask((VariantDefinition)null); - } + return null; + } - Debug.Assert(evaluationEvent.FeatureDefinition.Variants != null); + Debug.Assert(evaluationEvent.FeatureDefinition.Variants != null); - evaluationEvent.VariantAssignmentReason = VariantAssignmentReason.User; + evaluationEvent.VariantAssignmentReason = VariantAssignmentReason.User; - return new ValueTask( - evaluationEvent.FeatureDefinition - .Variants - .FirstOrDefault(variant => - variant.Name == user.Variant)); + return evaluationEvent.FeatureDefinition + .Variants + .FirstOrDefault(variant => + variant.Name == user.Variant); + } } } - } - if (evaluationEvent.FeatureDefinition.Allocation.Group != null) - { - foreach (GroupAllocation group in evaluationEvent.FeatureDefinition.Allocation.Group) + if (evaluationEvent.FeatureDefinition.Allocation.Group != null) { - if (TargetingEvaluator.IsTargeted(targetingContext.Groups, group.Groups, _assignerOptions.IgnoreCase)) + foreach (GroupAllocation group in evaluationEvent.FeatureDefinition.Allocation.Group) { - if (string.IsNullOrEmpty(group.Variant)) + if (TargetingEvaluator.IsTargeted(targetingContext.Groups, group.Groups, _assignerOptions.IgnoreCase)) { - Logger?.LogWarning($"Missing variant name for group allocation in feature {evaluationEvent.FeatureDefinition.Name}"); + if (string.IsNullOrEmpty(group.Variant)) + { + Logger?.LogWarning($"Missing variant name for group allocation in feature {evaluationEvent.FeatureDefinition.Name}"); - return new ValueTask((VariantDefinition)null); + return null; + } + + Debug.Assert(evaluationEvent.FeatureDefinition.Variants != null); + + evaluationEvent.VariantAssignmentReason = VariantAssignmentReason.Group; + + return evaluationEvent.FeatureDefinition + .Variants + .FirstOrDefault(variant => + variant.Name == group.Variant); } + } + } + + if (evaluationEvent.FeatureDefinition.Allocation.Percentile != null) + { + foreach (PercentileAllocation percentile in evaluationEvent.FeatureDefinition.Allocation.Percentile) + { + if (TargetingEvaluator.IsTargeted( + targetingContext, + percentile.From, + percentile.To, + _assignerOptions.IgnoreCase, + evaluationEvent.FeatureDefinition.Allocation.Seed ?? $"allocation\n{evaluationEvent.FeatureDefinition.Name}")) + { + if (string.IsNullOrEmpty(percentile.Variant)) + { + Logger?.LogWarning($"Missing variant name for percentile allocation in feature {evaluationEvent.FeatureDefinition.Name}"); - Debug.Assert(evaluationEvent.FeatureDefinition.Variants != null); + return null; + } - evaluationEvent.VariantAssignmentReason = VariantAssignmentReason.Group; + Debug.Assert(evaluationEvent.FeatureDefinition.Variants != null); - return new ValueTask( - evaluationEvent.FeatureDefinition - .Variants - .FirstOrDefault(variant => - variant.Name == group.Variant)); + evaluationEvent.VariantAssignmentReason = VariantAssignmentReason.Percentile; + + return evaluationEvent.FeatureDefinition + .Variants + .FirstOrDefault(variant => + variant.Name == percentile.Variant); + } } } } - if (evaluationEvent.FeatureDefinition.Allocation.Percentile != null) + if (evaluationEvent.FeatureDefinition.Allocation.AllocatedFor != null) { - foreach (PercentileAllocation percentile in evaluationEvent.FeatureDefinition.Allocation.Percentile) + // + // Keep track of the index of the filter we are evaluating + int filterIndex = -1; + + // + // For all enabling filters listed in the feature's state, evaluate them according to requirement type + foreach (ContextualFilterAllocation featureFilterConfiguration in evaluationEvent.FeatureDefinition.Allocation.AllocatedFor) { - if (TargetingEvaluator.IsTargeted( - targetingContext, - percentile.From, - percentile.To, - _assignerOptions.IgnoreCase, - evaluationEvent.FeatureDefinition.Allocation.Seed ?? $"allocation\n{evaluationEvent.FeatureDefinition.Name}")) + filterIndex++; + + // + // Handle AlwaysOn and On filters + if (string.Equals(featureFilterConfiguration.Name, "AlwaysOn", StringComparison.OrdinalIgnoreCase) || + string.Equals(featureFilterConfiguration.Name, "On", StringComparison.OrdinalIgnoreCase)) + { + evaluationEvent.VariantAssignmentReason = VariantAssignmentReason.Filter; + return evaluationEvent.FeatureDefinition + .Variants + .FirstOrDefault(variant => + variant.Name == featureFilterConfiguration.Variant); + } + + IFeatureFilterMetadata filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name, context.GetType()) ?? + GetFeatureFilterMetadata(featureFilterConfiguration.Name); + + if (filter == null) { - if (string.IsNullOrEmpty(percentile.Variant)) + if (_featureFilters.Any(f => IsMatchingName(f.GetType(), featureFilterConfiguration.Name))) { - Logger?.LogWarning($"Missing variant name for percentile allocation in feature {evaluationEvent.FeatureDefinition.Name}"); + // + // Cannot find the appropriate registered feature filter which matches the filter name and the provided context type. + // But there is a registered feature filter which matches the filter name. + continue; + } + + string errorMessage = $"The feature filter '{featureFilterConfiguration.Name}' specified for feature '{evaluationEvent.FeatureDefinition.Name}' was not found."; - return new ValueTask((VariantDefinition)null); + if (!_options.IgnoreMissingFeatureFilters) + { + throw new FeatureManagementException(FeatureManagementError.MissingFeatureFilter, errorMessage); } - Debug.Assert(evaluationEvent.FeatureDefinition.Variants != null); + Logger?.LogWarning(errorMessage); + + continue; + } + + var evaluationContext = new FeatureFilterEvaluationContext() + { + FeatureName = evaluationEvent.FeatureDefinition.Name, + Parameters = featureFilterConfiguration.Parameters + }; - evaluationEvent.VariantAssignmentReason = VariantAssignmentReason.Percentile; + BindSettings(filter, evaluationContext, filterIndex); - return new ValueTask( - evaluationEvent.FeatureDefinition - .Variants - .FirstOrDefault(variant => - variant.Name == percentile.Variant)); + // + // IContextualFeatureFilter + if (context != null) + { + ContextualFeatureFilterEvaluator contextualFilter = GetContextualFeatureFilter(featureFilterConfiguration.Name, context.GetType()); + + if (contextualFilter != null && + await contextualFilter.EvaluateAsync(evaluationContext, context).ConfigureAwait(false)) + { + evaluationEvent.VariantAssignmentReason = VariantAssignmentReason.Filter; + return evaluationEvent.FeatureDefinition + .Variants + .FirstOrDefault(variant => + variant.Name == featureFilterConfiguration.Variant); + } + } + + // + // IFeatureFilter + if (filter is IFeatureFilter featureFilter) + { + if (await featureFilter.EvaluateAsync(evaluationContext).ConfigureAwait(false)) + { + evaluationEvent.VariantAssignmentReason = VariantAssignmentReason.Filter; + return evaluationEvent.FeatureDefinition + .Variants + .FirstOrDefault(variant => + variant.Name == featureFilterConfiguration.Variant); + } } } } - return new ValueTask(variant); + return variant; } private void BindSettings(IFeatureFilterMetadata filter, FeatureFilterEvaluationContext context, int filterIndex) diff --git a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs index 384bb2b1..a6b26910 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, ITargetingContext context, CancellationToken cancellationToken) + public async ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken) { string cacheKey = GetVariantCacheKey(feature); diff --git a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs index 505b6652..3a9b12eb 100644 --- a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs @@ -52,6 +52,6 @@ public interface IVariantFeatureManager /// 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, ITargetingContext context, CancellationToken cancellationToken = default); + ValueTask GetVariantAsync(string feature, TContext context, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs b/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs index bac65418..7331bd5f 100644 --- a/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs +++ b/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs @@ -32,6 +32,7 @@ internal static class MicrosoftFeatureManagementFields public const string PercentileAllocationSectionName = "percentile"; public const string PercentileAllocationFrom = "from"; public const string PercentileAllocationTo = "to"; + public const string FilterAllocationSectionName = ClientFilters; public const string AllocationSeed = "seed"; // diff --git a/src/Microsoft.FeatureManagement/Telemetry/VariantAssignmentReason.cs b/src/Microsoft.FeatureManagement/Telemetry/VariantAssignmentReason.cs index 0e2ce56a..af2c7706 100644 --- a/src/Microsoft.FeatureManagement/Telemetry/VariantAssignmentReason.cs +++ b/src/Microsoft.FeatureManagement/Telemetry/VariantAssignmentReason.cs @@ -36,6 +36,11 @@ public enum VariantAssignmentReason /// /// The variant is assigned because of the percentile allocation when a feature flag is enabled. /// - Percentile + Percentile, + + /// + /// The variant is assigned because of a matching filter. + /// + Filter } }