From 1fdf4f50f31b90efb3f870fc293bd8b57a92c345 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 1 Feb 2024 15:41:54 +0800 Subject: [PATCH 1/6] update --- .../FilterCollectionExtensions.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs index cfd46554..d710af2c 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs @@ -16,14 +16,12 @@ 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. - /// - public static IFilterMetadata AddForFeature(this FilterCollection filters, string feature) where TFilterType : IAsyncActionFilter + /// A . + public static FilterCollection AddForFeature(this FilterCollection filters, string feature) where TFilterType : IAsyncActionFilter { - IFilterMetadata filterMetadata = null; - filters.Add(new FeatureGatedAsyncActionFilter(feature)); - return filterMetadata; + return filters; } } } From 1f9ff9de9501cf036d803916a5620382b9fa9d71 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 1 Feb 2024 16:12:11 +0800 Subject: [PATCH 2/6] update --- .../FilterCollectionExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs index d710af2c..b8382de9 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs @@ -16,7 +16,7 @@ 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. - /// A . + /// A reference to the instance after the operation has completed. public static FilterCollection AddForFeature(this FilterCollection filters, string feature) where TFilterType : IAsyncActionFilter { filters.Add(new FeatureGatedAsyncActionFilter(feature)); From bf5ee687c6f169428c1683e236e82b9ce329601b Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 1 Feb 2024 16:43:20 +0800 Subject: [PATCH 3/6] update --- .../FilterCollectionExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs index b8382de9..32c83f90 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs @@ -16,7 +16,7 @@ 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. - /// A reference to the instance after the operation has completed. + /// The reference to the filter collection. public static FilterCollection AddForFeature(this FilterCollection filters, string feature) where TFilterType : IAsyncActionFilter { filters.Add(new FeatureGatedAsyncActionFilter(feature)); From 9a4a188dc2a05a781be589d64c00f180e189c553 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 20 Oct 2025 17:05:07 +0800 Subject: [PATCH 4/6] update AddForFeature --- .../FeatureGatedAsyncActionFilter.cs | 56 ++++++++++++++++--- .../FilterCollectionExtensions.cs | 51 ++++++++++++++++- 2 files changed, 97 insertions(+), 10 deletions(-) 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 32c83f90..48f1715a 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs @@ -16,10 +16,57 @@ 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. + /// + public static IFilterMetadata AddForFeature(this FilterCollection filters, string feature) where TFilterType : IAsyncActionFilter + { + IFilterMetadata filterMetadata = null; + + filters.Add(new FeatureGatedAsyncActionFilter(RequirementType.Any, false, feature)); + + 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 filter collection. + public static FilterCollection AddForFeature(this FilterCollection filters, params string[] features) where TFilterType : IAsyncActionFilter + { + filters.Add(new FeatureGatedAsyncActionFilter(RequirementType.Any, false, features)); + + return filters; + } + + /// + /// 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 filter collection. + public static FilterCollection AddForFeature(this FilterCollection filters, RequirementType requirementType, params string[] features) where TFilterType : IAsyncActionFilter + { + filters.Add(new FeatureGatedAsyncActionFilter(requirementType, false, features)); + + return filters; + } + + /// + /// 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 filter collection. - public static FilterCollection AddForFeature(this FilterCollection filters, string feature) where TFilterType : IAsyncActionFilter + public static FilterCollection AddForFeature(this FilterCollection filters, RequirementType requirementType, bool negate, params string[] features) where TFilterType : IAsyncActionFilter { - filters.Add(new FeatureGatedAsyncActionFilter(feature)); + filters.Add(new FeatureGatedAsyncActionFilter(requirementType, negate, features)); return filters; } From ff52bbef9564f1a4eabff2c3d621c405f5422f7e Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 20 Oct 2025 17:22:30 +0800 Subject: [PATCH 5/6] add test --- .../FeatureManagementAspNetCore.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) 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; From fc01b24ef97516a01fa1e3823a55ce26f2ca6a79 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Wed, 22 Oct 2025 18:38:19 +0800 Subject: [PATCH 6/6] update --- .../FilterCollectionExtensions.cs | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs index 48f1715a..86bece85 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs @@ -16,12 +16,12 @@ 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(RequirementType.Any, false, feature)); + filters.Add(filterMetadata); return filterMetadata; } @@ -32,12 +32,14 @@ public static IFilterMetadata AddForFeature(this FilterCollection f /// 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 filter collection. - public static FilterCollection AddForFeature(this FilterCollection filters, params string[] features) where TFilterType : IAsyncActionFilter + /// The reference to the added filter metadata. + public static IFilterMetadata AddForFeature(this FilterCollection filters, params string[] features) where TFilterType : IAsyncActionFilter { - filters.Add(new FeatureGatedAsyncActionFilter(RequirementType.Any, false, features)); + IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter(RequirementType.Any, false, features); - return filters; + filters.Add(filterMetadata); + + return filterMetadata; } /// @@ -47,12 +49,14 @@ public static FilterCollection AddForFeature(this FilterCollection /// 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 filter collection. - public static FilterCollection AddForFeature(this FilterCollection filters, RequirementType requirementType, params string[] features) where TFilterType : IAsyncActionFilter + /// The reference to the added filter metadata. + public static IFilterMetadata AddForFeature(this FilterCollection filters, RequirementType requirementType, params string[] features) where TFilterType : IAsyncActionFilter { - filters.Add(new FeatureGatedAsyncActionFilter(requirementType, false, features)); + IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter(requirementType, false, features); + + filters.Add(filterMetadata); - return filters; + return filterMetadata; } /// @@ -63,12 +67,14 @@ public static FilterCollection AddForFeature(this FilterCollection /// 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 filter collection. - public static FilterCollection AddForFeature(this FilterCollection filters, RequirementType requirementType, bool negate, params string[] features) where TFilterType : IAsyncActionFilter + /// The reference to the added filter metadata. + public static IFilterMetadata AddForFeature(this FilterCollection filters, RequirementType requirementType, bool negate, params string[] features) where TFilterType : IAsyncActionFilter { - filters.Add(new FeatureGatedAsyncActionFilter(requirementType, negate, features)); + IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter(requirementType, negate, features); + + filters.Add(filterMetadata); - return filters; + return filterMetadata; } } }