Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use ITargetingContext when calling GetVariantAsync #484

Merged
merged 11 commits into from
Aug 26, 2024
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.
zhiyuanliang-ms marked this conversation as resolved.
Show resolved Hide resolved
/// </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 used to provide an overload for <see cref="IVariantFeatureManager.GetVariantAsync(string, ITargetingContext, CancellationToken)"/> that accepts a <see cref="TargetingContext"/> type directly.
zhiyuanliang-ms marked this conversation as resolved.
Show resolved Hide resolved
/// </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
Loading