Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@ public class FeatureFilterEvaluationContext
public IConfiguration Parameters { get; set; }

/// <summary>
/// The settings provided for the feature filter to use when evaluating whether the feature should be enabled. This property takes precedence over <see cref="Settings"/> and <see cref="Parameters"/> if both are provided.
/// </summary>
public object ParametersObject { get; set; }

/// <summary>
/// A settings object, if any, that has been pre-bound from <see cref="Parameters"/>.
/// The settings are made available for <see cref="IFeatureFilter"/>s that implement <see cref="IFilterParametersBinder"/>.
/// A settings object, if any, provided for the feature filter to use when evaluating whether the feature should be enabled.
/// This property is populated in two cases:
/// <list type="bullet">
/// <item>For features that provide parameters as an object, via <see cref="FeatureFilterConfiguration.ParametersObject"/>.</item>
/// <item>For <see cref="IFeatureFilter"/>s that implement <see cref="IFilterParametersBinder"/>.</item>
/// </list>
/// </summary>
public object Settings { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,16 @@ public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
throw new ArgumentNullException(nameof(context));
}

//
// Check if ParametersObject available (takes precedence), then prebound settings, otherwise bind from parameters.
PercentageFilterSettings settings;

if (context.ParametersObject != null && !(context.ParametersObject is PercentageFilterSettings))
if (context.Settings != null && !(context.Settings is PercentageFilterSettings))
{
throw new ArgumentException(
$"The '{Alias}' feature filter for feature '{context.FeatureName}' has a {nameof(context.ParametersObject)} of type '{context.ParametersObject.GetType()}', but expected '{typeof(PercentageFilterSettings)}'.",
nameof(context.ParametersObject));
$"The '{Alias}' feature filter for feature '{context.FeatureName}' has a {nameof(context.Settings)} value of type '{context.Settings.GetType()}', but expected '{typeof(PercentageFilterSettings)}'.",
nameof(context.Settings));
}

settings = (PercentageFilterSettings)context.ParametersObject ?? (PercentageFilterSettings)context.Settings ?? (PercentageFilterSettings)BindParameters(context.Parameters);
//
// Check if prebound settings available, otherwise bind from parameters.
PercentageFilterSettings settings = (PercentageFilterSettings)context.Settings ?? (PercentageFilterSettings)BindParameters(context.Parameters);

bool result = true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,16 @@ public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
throw new ArgumentNullException(nameof(context));
}

//
// Check if ParametersObject available (takes precedence), then prebound settings, otherwise bind from parameters.
TimeWindowFilterSettings settings;

if (context.ParametersObject != null && !(context.ParametersObject is TimeWindowFilterSettings))
if (context.Settings != null && !(context.Settings is TimeWindowFilterSettings))
{
throw new ArgumentException(
$"The '{Alias}' feature filter for feature '{context.FeatureName}' has a {nameof(context.ParametersObject)} of type '{context.ParametersObject.GetType()}', but expected '{typeof(TimeWindowFilterSettings)}'.",
nameof(context.ParametersObject));
$"The '{Alias}' feature filter for feature '{context.FeatureName}' has a {nameof(context.Settings)} value of type '{context.Settings.GetType()}', but expected '{typeof(TimeWindowFilterSettings)}'.",
nameof(context.Settings));
}

settings = (TimeWindowFilterSettings)context.ParametersObject ?? (TimeWindowFilterSettings)context.Settings ?? (TimeWindowFilterSettings)BindParameters(context.Parameters);
//
// Check if prebound settings available, otherwise bind from parameters.
TimeWindowFilterSettings settings = (TimeWindowFilterSettings)context.Settings ?? (TimeWindowFilterSettings)BindParameters(context.Parameters);

DateTimeOffset now = SystemClock?.GetUtcNow() ?? DateTimeOffset.UtcNow;

Expand Down
13 changes: 11 additions & 2 deletions src/Microsoft.FeatureManagement/FeatureManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -498,11 +498,14 @@ private async ValueTask<bool> IsEnabledAsync<TContext>(FeatureDefinition feature
{
FeatureName = featureDefinition.Name,
Parameters = featureFilterConfiguration.Parameters,
ParametersObject = featureFilterConfiguration.ParametersObject,
Settings = featureFilterConfiguration.ParametersObject,
CancellationToken = cancellationToken
};

BindSettings(filter, context, filterIndex);
if (context.Settings == null)
{
BindSettings(filter, context, filterIndex);
}

//
// IContextualFeatureFilter
Expand Down Expand Up @@ -681,6 +684,12 @@ private void BindSettings(IFeatureFilterMetadata filter, FeatureFilterEvaluation
return;
}

// Skip parameter binding if the provider has already supplied a parameters object.
if (context.Settings != null)
{
return;
}

if (!(context.Parameters is ConfigurationWrapper) || Cache == null)
{
context.Settings = binder.BindParameters(context.Parameters);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,16 @@ public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context, ITargeti
throw new ArgumentNullException(nameof(targetingContext));
}

//
// Check if ParametersObject available (takes precedence), then prebound settings, otherwise bind from parameters.
TargetingFilterSettings settings;

if (context.ParametersObject != null && !(context.ParametersObject is TargetingFilterSettings))
if (context.Settings != null && !(context.Settings is TargetingFilterSettings))
{
throw new ArgumentException(
$"The '{Alias}' feature filter for feature '{context.FeatureName}' has a {nameof(context.ParametersObject)} of type '{context.ParametersObject.GetType()}', but expected '{typeof(TargetingFilterSettings)}'.",
nameof(context.ParametersObject));
$"The '{Alias}' feature filter for feature '{context.FeatureName}' has a {nameof(context.Settings)} value of type '{context.Settings.GetType()}', but expected '{typeof(TargetingFilterSettings)}'.",
nameof(context.Settings));
}

settings = (TargetingFilterSettings)context.ParametersObject ?? (TargetingFilterSettings)context.Settings ?? (TargetingFilterSettings)BindParameters(context.Parameters);
//
// Check if prebound settings available, otherwise bind from parameters.
TargetingFilterSettings settings = (TargetingFilterSettings)context.Settings ?? (TargetingFilterSettings)BindParameters(context.Parameters);

return Task.FromResult(TargetingEvaluator.IsTargeted(targetingContext, settings, _options.IgnoreCase, context.FeatureName));
}
Expand Down
122 changes: 118 additions & 4 deletions tests/Tests.FeatureManagement/FeatureManagementTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1094,9 +1094,9 @@ public async Task UsesParametersObject()
testFeatureFilter.Callback = (evaluationContext) =>
{
//
// When ParametersObject is set, it should be available on the context
// so custom filters can use it with their own precedence logic.
Assert.Same(parameterObject, evaluationContext.ParametersObject);
// When ParametersObject is set, it should be available on the context via settings
// so custom filters can use it.
Assert.Same(parameterObject, evaluationContext.Settings);

return Task.FromResult(true);
};
Expand Down Expand Up @@ -1157,7 +1157,6 @@ public async Task ParametersObjectFallsBackToParametersWhenNull()
//
// When ParametersObject is null, Settings should be populated
// by IFilterParametersBinder as usual.
Assert.Null(evaluationContext.ParametersObject);
Assert.NotNull(evaluationContext.Settings);

return Task.FromResult(true);
Expand Down Expand Up @@ -1925,6 +1924,121 @@ public async Task TimeWindowFilterThrowsOnInvalidParametersObjectType()

await Assert.ThrowsAsync<ArgumentException>(() => featureManager.IsEnabledAsync("BadFeature"));
}

[Fact]
public async Task TargetingFilterUsesParametersObject()
{
var services = new ServiceCollection();

var definitionProvider = new InMemoryFeatureDefinitionProvider(
new FeatureDefinition[]
{
new FeatureDefinition
{
Name = "TargetingFeature",
EnabledFor = new List<FeatureFilterConfiguration>()
{
new FeatureFilterConfiguration
{
Name = "Microsoft.Targeting",
ParametersObject = new TargetingFilterSettings
{
Audience = new Audience
{
Users = new List<string> { "Jeff" },
Groups = new List<GroupRollout>
{
new GroupRollout
{
Name = "Ring1",
RolloutPercentage = 100
}
},
DefaultRolloutPercentage = 0
}
}
}
}
}
});

services.AddSingleton<IFeatureDefinitionProvider>(definitionProvider)
.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build())
.AddFeatureManagement();

ServiceProvider serviceProvider = services.BuildServiceProvider();

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

//
// Targeted user should be enabled
var targetingContext = new TargetingContext
{
UserId = "Jeff",
Groups = new List<string> { "Ring0" }
};

Assert.True(await featureManager.IsEnabledAsync("TargetingFeature", targetingContext));

//
// User in targeted group should be enabled
targetingContext = new TargetingContext
{
UserId = "NotTargeted",
Groups = new List<string> { "Ring1" }
};

Assert.True(await featureManager.IsEnabledAsync("TargetingFeature", targetingContext));

//
// Non-targeted user should be disabled (0% default rollout)
targetingContext = new TargetingContext
{
UserId = "NotTargeted",
Groups = new List<string> { "Ring0" }
};

Assert.False(await featureManager.IsEnabledAsync("TargetingFeature", targetingContext));
}

[Fact]
public async Task TargetingFilterThrowsOnInvalidParametersObjectType()
{
var services = new ServiceCollection();

var definitionProvider = new InMemoryFeatureDefinitionProvider(
new FeatureDefinition[]
{
new FeatureDefinition
{
Name = "BadFeature",
EnabledFor = new List<FeatureFilterConfiguration>()
{
new FeatureFilterConfiguration
{
Name = "Microsoft.Targeting",
ParametersObject = "wrong type"
}
}
}
});

services.AddSingleton<IFeatureDefinitionProvider>(definitionProvider)
.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build())
.AddFeatureManagement();

ServiceProvider serviceProvider = services.BuildServiceProvider();

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

var targetingContext = new TargetingContext
{
UserId = "Jeff",
Groups = new List<string>()
};

await Assert.ThrowsAsync<ArgumentException>(() => featureManager.IsEnabledAsync("BadFeature", targetingContext).AsTask());
}
}

public class FeatureManagementVariantTest
Expand Down
Loading