Skip to content

Commit

Permalink
Use ITargetingContext when calling GetVariantAsync (#484)
Browse files Browse the repository at this point in the history
* add variant feature manager extensions

* use TContext to avoid future breaking change

* TargetingContext in EvaluationEvent

* update comments & not use ambient context in contextual case

* update comment

* use ITargetingContext & check runtime context type

* use TargetingCotnext for private method

* use var instead of type

* revert change on ContextualTestFilter

* update comment

* update comment
  • Loading branch information
zhiyuanliang-ms authored Aug 26, 2024
1 parent 882c7b7 commit 9284d27
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 20 deletions.
30 changes: 17 additions & 13 deletions src/Microsoft.FeatureManagement/FeatureManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ public async Task<bool> IsEnabledAsync(string feature)
/// Checks whether a given feature is enabled.
/// </summary>
/// <param name="feature">The name of the feature to check.</param>
/// <param name="appContext">A context providing information that can be used to evaluate whether a feature should be on or off.</param>
/// <param name="appContext">A context that provides information to evaluate whether a feature should be on or off.</param>
/// <returns>True if the feature is enabled, otherwise false.</returns>
public async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appContext)
{
Expand All @@ -170,7 +170,7 @@ public async ValueTask<bool> IsEnabledAsync(string feature, CancellationToken ca
/// Checks whether a given feature is enabled.
/// </summary>
/// <param name="feature">The name of the feature to check.</param>
/// <param name="appContext">A context providing information that can be used to evaluate whether a feature should be on or off.</param>
/// <param name="appContext">A context that provides information to evaluate whether a feature should be on or off.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>True if the feature is enabled, otherwise false.</returns>
public async ValueTask<bool> IsEnabledAsync<TContext>(string feature, TContext appContext, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -216,7 +216,7 @@ public async ValueTask<Variant> GetVariantAsync(string feature, CancellationToke
throw new ArgumentNullException(nameof(feature));
}

EvaluationEvent evaluationEvent = await EvaluateFeature<TargetingContext>(feature, context: null, useContext: false, cancellationToken);
EvaluationEvent evaluationEvent = await EvaluateFeature<object>(feature, context: null, useContext: false, cancellationToken);

return evaluationEvent.Variant;
}
Expand All @@ -225,10 +225,10 @@ public async ValueTask<Variant> GetVariantAsync(string feature, CancellationToke
/// Gets the assigned variant for a specific feature.
/// </summary>
/// <param name="feature">The name of the feature to evaluate.</param>
/// <param name="context">An instance of <see cref="TargetingContext"/> used to evaluate which variant the user will be assigned.</param>
/// <param name="context">A context that provides information to evaluate which variant will be assigned to the user.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>A variant assigned to the user based on the feature's configured allocation.</returns>
public async ValueTask<Variant> GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken = default)
public async ValueTask<Variant> GetVariantAsync(string feature, ITargetingContext context, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(feature))
{
Expand Down Expand Up @@ -262,15 +262,19 @@ private async ValueTask<EvaluationEvent> EvaluateFeature<TContext>(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;
Expand Down Expand Up @@ -314,7 +318,7 @@ private async ValueTask<EvaluationEvent> EvaluateFeature<TContext>(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)
{
Expand Down Expand Up @@ -496,7 +500,7 @@ private async ValueTask<bool> IsEnabledAsync<TContext>(FeatureDefinition feature

if (useAppContext)
{
filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name, typeof(TContext)) ??
filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name, appContext.GetType()) ??
GetFeatureFilterMetadata(featureFilterConfiguration.Name);
}
else
Expand Down Expand Up @@ -538,7 +542,7 @@ private async ValueTask<bool> IsEnabledAsync<TContext>(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)
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public async ValueTask<Variant> GetVariantAsync(string feature, CancellationToke
return variant;
}

public async ValueTask<Variant> GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken)
public async ValueTask<Variant> GetVariantAsync(string feature, ITargetingContext context, CancellationToken cancellationToken)
{
string cacheKey = GetVariantCacheKey(feature);

Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.FeatureManagement/IFeatureManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public interface IFeatureManager
/// Checks whether a given feature is enabled.
/// </summary>
/// <param name="feature">The name of the feature to check.</param>
/// <param name="context">A context providing information that can be used to evaluate whether a feature should be on or off.</param>
/// <param name="context">A context that provides information to evaluate whether a feature should be on or off.</param>
/// <returns>True if the feature is enabled, otherwise false.</returns>
Task<bool> IsEnabledAsync<TContext>(string feature, TContext context);
}
Expand Down
6 changes: 3 additions & 3 deletions src/Microsoft.FeatureManagement/IVariantFeatureManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public interface IVariantFeatureManager
/// Checks whether a given feature is enabled.
/// </summary>
/// <param name="feature">The name of the feature to check.</param>
/// <param name="context">A context providing information that can be used to evaluate whether a feature should be on or off.</param>
/// <param name="context">A context that provides information to evaluate whether a feature should be on or off.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>True if the feature is enabled, otherwise false.</returns>
ValueTask<bool> IsEnabledAsync<TContext>(string feature, TContext context, CancellationToken cancellationToken = default);
Expand All @@ -49,9 +49,9 @@ public interface IVariantFeatureManager
/// Gets the assigned variant for a specific feature.
/// </summary>
/// <param name="feature">The name of the feature to evaluate.</param>
/// <param name="context">An instance of <see cref="TargetingContext"/> used to evaluate which variant the user will be assigned.</param>
/// <param name="context">A context that provides information to evaluate which variant will be assigned to the user.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>A variant assigned to the user based on the feature's configured allocation.</returns>
ValueTask<Variant> GetVariantAsync(string feature, TargetingContext context, CancellationToken cancellationToken = default);
ValueTask<Variant> GetVariantAsync(string feature, ITargetingContext context, CancellationToken cancellationToken = default);
}
}
28 changes: 28 additions & 0 deletions src/Microsoft.FeatureManagement/VariantFeatureManagerExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Extensions for <see cref="IVariantFeatureManager"/>.
/// </summary>
public static class VariantFeatureManagerExtensions
{
/// <summary>
/// Gets the assigned variant for a specific feature.
/// </summary>
/// <param name="variantFeatureManager">The <see cref="IVariantFeatureManager"/> instance.</param>
/// <param name="feature">The name of the feature to evaluate.</param>
/// <param name="context">An instance of <see cref="TargetingContext"/> used to evaluate which variant the user will be assigned.</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
/// <returns>A variant assigned to the user based on the feature's configured allocation.</returns>
public static ValueTask<Variant> GetVariantAsync(this IVariantFeatureManager variantFeatureManager, string feature, TargetingContext context, CancellationToken cancellationToken = default)
{
return variantFeatureManager.GetVariantAsync(feature, context, cancellationToken);
}
}
}
9 changes: 8 additions & 1 deletion tests/Tests.FeatureManagement/AppContext.cs
Original file line number Diff line number Diff line change
@@ -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<string> Groups { get; set; }
}
}
49 changes: 48 additions & 1 deletion tests/Tests.FeatureManagement/FeatureManagementTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ public async Task UsesContext()

IFeatureManager featureManager = provider.GetRequiredService<IFeatureManager>();

AppContext context = new AppContext();
var context = new AppContext();

context.AccountId = "NotEnabledAccount";

Expand Down Expand Up @@ -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<ContextualTestFilter>();

ServiceProvider serviceProvider = services.BuildServiceProvider();

ContextualTestFilter contextualTestFeatureFilter = (ContextualTestFilter)serviceProvider.GetRequiredService<IEnumerable<IFeatureFilterMetadata>>().First(f => f is ContextualTestFilter);

contextualTestFeatureFilter.ContextualCallback = (ctx, accountContext) =>
{
var allowedAccounts = new List<string>();

ctx.Parameters.Bind("AllowedAccounts", allowedAccounts);

return allowedAccounts.Contains(accountContext.AccountId);
};

IVariantFeatureManager featureManager = serviceProvider.GetRequiredService<IVariantFeatureManager>();

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
Expand Down
1 change: 1 addition & 0 deletions tests/Tests.FeatureManagement/Features.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
28 changes: 28 additions & 0 deletions tests/Tests.FeatureManagement/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
]
}
Expand Down

0 comments on commit 9284d27

Please sign in to comment.