Skip to content
Open
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
8 changes: 8 additions & 0 deletions src/Microsoft.FeatureManagement/FeatureFilterConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,13 @@ public class FeatureFilterConfiguration
/// Configurable parameters that can change across instances of a feature filter.
/// </summary>
public IConfiguration Parameters { get; set; } = new ConfigurationRoot(new List<IConfigurationProvider>());

/// <summary>
/// A strongly-typed parameter object that can be used as an alternative to <see cref="Parameters"/>.
/// Custom <see cref="IFeatureDefinitionProvider"/> implementations can populate this property directly
/// instead of constructing an <see cref="IConfiguration"/> instance.
/// When set, feature filters should prefer this over <see cref="Parameters"/>.
/// </summary>
public object ParameterObject { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ public class FeatureFilterEvaluationContext
/// </summary>
public IConfiguration Parameters { get; set; }

/// <summary>
/// A strongly-typed parameter object, if any, provided by a custom <see cref="IFeatureDefinitionProvider"/>.
/// When set, feature filters should prefer this over <see cref="Parameters"/>.
/// </summary>
public object ParameterObject { 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"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
}

//
// Check if prebound settings available, otherwise bind from parameters.
PercentageFilterSettings settings = (PercentageFilterSettings)context.Settings ?? (PercentageFilterSettings)BindParameters(context.Parameters);
// Check if ParameterObject available (takes precedence), then prebound settings, otherwise bind from parameters.
PercentageFilterSettings settings = context.ParameterObject != null
? (PercentageFilterSettings)context.ParameterObject
: (PercentageFilterSettings)context.Settings ?? (PercentageFilterSettings)BindParameters(context.Parameters);

bool result = true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
}

//
// Check if prebound settings available, otherwise bind from parameters.
TimeWindowFilterSettings settings = (TimeWindowFilterSettings)context.Settings ?? (TimeWindowFilterSettings)BindParameters(context.Parameters);
// Check if ParameterObject available (takes precedence), then prebound settings, otherwise bind from parameters.
TimeWindowFilterSettings settings = context.ParameterObject != null
? (TimeWindowFilterSettings)context.ParameterObject
: (TimeWindowFilterSettings)context.Settings ?? (TimeWindowFilterSettings)BindParameters(context.Parameters);

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

Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.FeatureManagement/FeatureManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ private async ValueTask<bool> IsEnabledAsync<TContext>(FeatureDefinition feature
{
FeatureName = featureDefinition.Name,
Parameters = featureFilterConfiguration.Parameters,
ParameterObject = featureFilterConfiguration.ParameterObject,
CancellationToken = cancellationToken
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@ public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context, ITargeti
}

//
// Check if prebound settings available, otherwise bind from parameters.
TargetingFilterSettings settings = (TargetingFilterSettings)context.Settings ?? (TargetingFilterSettings)BindParameters(context.Parameters);
// Check if ParameterObject available (takes precedence), then prebound settings, otherwise bind from parameters.
TargetingFilterSettings settings = context.ParameterObject != null
? (TargetingFilterSettings)context.ParameterObject
: (TargetingFilterSettings)context.Settings ?? (TargetingFilterSettings)BindParameters(context.Parameters);

return Task.FromResult(TargetingEvaluator.IsTargeted(targetingContext, settings, _options.IgnoreCase, context.FeatureName));
}
Expand Down
254 changes: 254 additions & 0 deletions tests/Tests.FeatureManagement/FeatureManagementTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,124 @@ public async Task BindsFeatureFlagSettings()

Assert.True(called);
}

[Fact]
public async Task UsesParameterObject()
{
var parameterObject = new object();

FeatureFilterConfiguration testFilterConfiguration = new FeatureFilterConfiguration
{
Name = "Test",
ParameterObject = parameterObject
};

var services = new ServiceCollection();

var definitionProvider = new InMemoryFeatureDefinitionProvider(
new FeatureDefinition[]
{
new FeatureDefinition
{
Name = Features.ConditionalFeature,
EnabledFor = new List<FeatureFilterConfiguration>()
{
testFilterConfiguration
}
}
});

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

ServiceProvider serviceProvider = services.BuildServiceProvider();

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

IEnumerable<IFeatureFilterMetadata> featureFilters = serviceProvider.GetRequiredService<IEnumerable<IFeatureFilterMetadata>>();

TestFilter testFeatureFilter = (TestFilter)featureFilters.First(f => f is TestFilter);

testFeatureFilter.Callback = (evaluationContext) =>
{
//
// When ParameterObject is set, it should be available on the context
// so custom filters can use it with their own precedence logic.
Assert.Same(parameterObject, evaluationContext.ParameterObject);

return Task.FromResult(true);
};

bool result = await featureManager.IsEnabledAsync(Features.ConditionalFeature);

Assert.True(result);
}

[Fact]
public async Task ParameterObjectFallsBackToParametersWhenNull()
{
FeatureFilterConfiguration testFilterConfiguration = new FeatureFilterConfiguration
{
Name = "Test",
ParameterObject = null
};

var services = new ServiceCollection();

var definitionProvider = new InMemoryFeatureDefinitionProvider(
new FeatureDefinition[]
{
new FeatureDefinition
{
Name = Features.ConditionalFeature,
EnabledFor = new List<FeatureFilterConfiguration>()
{
testFilterConfiguration
}
}
});

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

ServiceProvider serviceProvider = services.BuildServiceProvider();

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

IEnumerable<IFeatureFilterMetadata> featureFilters = serviceProvider.GetRequiredService<IEnumerable<IFeatureFilterMetadata>>();

TestFilter testFeatureFilter = (TestFilter)featureFilters.First(f => f is TestFilter);

bool binderCalled = false;

testFeatureFilter.ParametersBinderCallback = (parameters) =>
{
binderCalled = true;

return parameters;
};

testFeatureFilter.Callback = (evaluationContext) =>
{
//
// When ParameterObject is null, Settings should be populated
// by IFilterParametersBinder as usual.
Assert.Null(evaluationContext.ParameterObject);
Assert.NotNull(evaluationContext.Settings);

return Task.FromResult(true);
};

bool result = await featureManager.IsEnabledAsync(Features.ConditionalFeature);

Assert.True(result);

Assert.True(binderCalled);
}
}

public class FeatureManagementBuiltInFeatureFilterTest
Expand Down Expand Up @@ -1671,6 +1789,142 @@ public async Task CustomFilterContextualTargetingWithNullSetting()

Assert.True(await featureManager.IsEnabledAsync("CustomFilterFeature"));
}

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

var definitionProvider = new InMemoryFeatureDefinitionProvider(
new FeatureDefinition[]
{
new FeatureDefinition
{
Name = "PercentageFeature",
EnabledFor = new List<FeatureFilterConfiguration>()
{
new FeatureFilterConfiguration
{
Name = "Microsoft.Percentage",
ParameterObject = new PercentageFilterSettings { Value = 100 }
}
}
}
});

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

ServiceProvider serviceProvider = services.BuildServiceProvider();

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

Assert.True(await featureManager.IsEnabledAsync("PercentageFeature"));
}

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

var definitionProvider = new InMemoryFeatureDefinitionProvider(
new FeatureDefinition[]
{
new FeatureDefinition
{
Name = "TimeWindowFeature",
EnabledFor = new List<FeatureFilterConfiguration>()
{
new FeatureFilterConfiguration
{
Name = "Microsoft.TimeWindow",
ParameterObject = new TimeWindowFilterSettings
{
Start = DateTimeOffset.UtcNow.AddDays(-1),
End = DateTimeOffset.UtcNow.AddDays(1)
}
}
}
}
});

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

ServiceProvider serviceProvider = services.BuildServiceProvider();

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

Assert.True(await featureManager.IsEnabledAsync("TimeWindowFeature"));
}

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

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

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

ServiceProvider serviceProvider = services.BuildServiceProvider();

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

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

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

var definitionProvider = new InMemoryFeatureDefinitionProvider(
new FeatureDefinition[]
{
new FeatureDefinition
{
Name = "BadFeature",
EnabledFor = new List<FeatureFilterConfiguration>()
{
new FeatureFilterConfiguration
{
Name = "Microsoft.TimeWindow",
ParameterObject = 42
}
}
}
});

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

ServiceProvider serviceProvider = services.BuildServiceProvider();

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

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

public class FeatureManagementVariantTest
Expand Down
Loading