diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs
index 073f953f..80f540e8 100644
--- a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs
+++ b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs
@@ -4,33 +4,73 @@
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.FeatureManagement
{
///
- /// A place holder MVC filter that is used to dynamically activate a filter based on whether a feature is enabled.
+ /// A place holder MVC filter that is used to dynamically activate a filter based on whether a feature (or set of features) is enabled.
///
/// The filter that will be used instead of this placeholder.
class FeatureGatedAsyncActionFilter : IAsyncActionFilter where T : IAsyncActionFilter
{
- public FeatureGatedAsyncActionFilter(string featureName)
+ ///
+ /// Creates a feature gated filter for multiple features with a specified requirement type and ability to negate the evaluation.
+ ///
+ /// Specifies whether all or any of the provided features should be enabled.
+ /// Whether to negate the evaluation result.
+ /// The features that control whether the wrapped filter executes.
+ public FeatureGatedAsyncActionFilter(RequirementType requirementType, bool negate, params string[] features)
{
- if (string.IsNullOrEmpty(featureName))
+ if (features == null || features.Length == 0)
{
- throw new ArgumentNullException(nameof(featureName));
+ throw new ArgumentNullException(nameof(features));
}
- FeatureName = featureName;
+ Features = features;
+ RequirementType = requirementType;
+ Negate = negate;
}
- public string FeatureName { get; }
+ ///
+ /// The set of features that gate the wrapped filter.
+ ///
+ public IEnumerable Features { get; }
+
+ ///
+ /// Controls whether any or all features in should be enabled to allow the wrapped filter to execute.
+ ///
+ public RequirementType RequirementType { get; }
+
+ ///
+ /// Negates the evaluation for whether or not the wrapped filter should execute.
+ ///
+ public bool Negate { get; }
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
- IFeatureManager featureManager = context.HttpContext.RequestServices.GetRequiredService();
+ IFeatureManagerSnapshot featureManager = context.HttpContext.RequestServices.GetRequiredService();
+
+ bool enabled;
+
+ // Enabled state is determined by either 'any' or 'all' features being enabled.
+ if (RequirementType == RequirementType.All)
+ {
+ enabled = await Features.All(async f => await featureManager.IsEnabledAsync(f).ConfigureAwait(false));
+ }
+ else
+ {
+ enabled = await Features.Any(async f => await featureManager.IsEnabledAsync(f).ConfigureAwait(false));
+ }
+
+ if (Negate)
+ {
+ enabled = !enabled;
+ }
- if (await featureManager.IsEnabledAsync(FeatureName).ConfigureAwait(false))
+ if (enabled)
{
IAsyncActionFilter filter = ActivatorUtilities.CreateInstance(context.HttpContext.RequestServices);
diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs
index cfd46554..86bece85 100644
--- a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs
+++ b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs
@@ -16,12 +16,63 @@ public static class FilterCollectionExtensions
/// The MVC filter to add and use if the feature is enabled.
/// The filter collection to add to.
/// The feature that will need to enabled to trigger the execution of the MVC filter.
- ///
+ /// The reference to the added filter metadata.
public static IFilterMetadata AddForFeature(this FilterCollection filters, string feature) where TFilterType : IAsyncActionFilter
{
- IFilterMetadata filterMetadata = null;
+ IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter(RequirementType.Any, false, feature);
- filters.Add(new FeatureGatedAsyncActionFilter(feature));
+ filters.Add(filterMetadata);
+
+ return filterMetadata;
+ }
+
+ ///
+ /// Adds an MVC filter that will only activate during a request if the specified feature is enabled.
+ ///
+ /// The MVC filter to add and use if the feature is enabled.
+ /// The filter collection to add to.
+ /// The features that control whether the MVC filter executes.
+ /// The reference to the added filter metadata.
+ public static IFilterMetadata AddForFeature(this FilterCollection filters, params string[] features) where TFilterType : IAsyncActionFilter
+ {
+ IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter(RequirementType.Any, false, features);
+
+ filters.Add(filterMetadata);
+
+ return filterMetadata;
+ }
+
+ ///
+ /// Adds an MVC filter that will only activate during a request if the specified features are enabled based on the provided requirement type.
+ ///
+ /// The MVC filter to add and use if the features condition is satisfied.
+ /// The filter collection to add to.
+ /// Specifies whether all or any of the provided features should be enabled.
+ /// The features that control whether the MVC filter executes.
+ /// The reference to the added filter metadata.
+ public static IFilterMetadata AddForFeature(this FilterCollection filters, RequirementType requirementType, params string[] features) where TFilterType : IAsyncActionFilter
+ {
+ IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter(requirementType, false, features);
+
+ filters.Add(filterMetadata);
+
+ return filterMetadata;
+ }
+
+ ///
+ /// Adds an MVC filter that will only activate during a request if the specified features are enabled based on the provided requirement type and negation flag.
+ ///
+ /// The MVC filter to add and use if the features condition is satisfied.
+ /// The filter collection to add to.
+ /// Specifies whether all or any of the provided features should be enabled.
+ /// Whether to negate the evaluation result for the provided features set.
+ /// The features that control whether the MVC filter executes.
+ /// The reference to the added filter metadata.
+ public static IFilterMetadata AddForFeature(this FilterCollection filters, RequirementType requirementType, bool negate, params string[] features) where TFilterType : IAsyncActionFilter
+ {
+ IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter(requirementType, negate, features);
+
+ filters.Add(filterMetadata);
return filterMetadata;
}
diff --git a/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs b/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs
index 68b7efc1..8bfd5ec4 100644
--- a/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs
+++ b/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs
@@ -202,6 +202,47 @@ public async Task GatesRazorPageFeatures()
Assert.Equal(HttpStatusCode.OK, gateAnyNegateResponse.StatusCode);
}
+ [Fact]
+ public async Task GatesActionFilterFeatures()
+ {
+ IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
+
+ TestServer server = new TestServer(WebHost.CreateDefaultBuilder().ConfigureServices(services =>
+ {
+ services
+ .AddSingleton(config)
+ .AddFeatureManagement()
+ .AddFeatureFilter();
+
+ services.AddMvcCore(o =>
+ {
+ DisableEndpointRouting(o);
+ o.Filters.AddForFeature(RequirementType.All, Features.ConditionalFeature, Features.ConditionalFeature2);
+ });
+ }).Configure(app => app.UseMvc()));
+
+ TestFilter filter = (TestFilter)server.Host.Services.GetRequiredService>().First(f => f is TestFilter);
+ HttpClient client = server.CreateClient();
+
+ //
+ // Enable all features
+ filter.Callback = _ => Task.FromResult(true);
+ HttpResponseMessage res = await client.GetAsync("");
+ Assert.True(res.Headers.Contains(nameof(MvcFilter)));
+
+ //
+ // Enable 1/2 features
+ filter.Callback = ctx => Task.FromResult(ctx.FeatureName == Features.ConditionalFeature);
+ res = await client.GetAsync("");
+ Assert.False(res.Headers.Contains(nameof(MvcFilter)));
+
+ //
+ // Enable no
+ filter.Callback = _ => Task.FromResult(false);
+ res = await client.GetAsync("");
+ Assert.False(res.Headers.Contains(nameof(MvcFilter)));
+ }
+
private static void DisableEndpointRouting(MvcOptions options)
{
options.EnableEndpointRouting = false;