Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Update FilterCollectionExtensions (#359)
* update

* update

* update

* update AddForFeature

* add test

* update
  • Loading branch information
zhiyuanliang-ms authored Oct 24, 2025
commit d8061e9b4eccf80eeeeea77af2e2fe40319f0783
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The filter that will be used instead of this placeholder.</typeparam>
class FeatureGatedAsyncActionFilter<T> : IAsyncActionFilter where T : IAsyncActionFilter
{
public FeatureGatedAsyncActionFilter(string featureName)
/// <summary>
/// Creates a feature gated filter for multiple features with a specified requirement type and ability to negate the evaluation.
/// </summary>
/// <param name="requirementType">Specifies whether all or any of the provided features should be enabled.</param>
/// <param name="negate">Whether to negate the evaluation result.</param>
/// <param name="features">The features that control whether the wrapped filter executes.</param>
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; }
/// <summary>
/// The set of features that gate the wrapped filter.
/// </summary>
public IEnumerable<string> Features { get; }

/// <summary>
/// Controls whether any or all features in <see cref="Features"/> should be enabled to allow the wrapped filter to execute.
/// </summary>
public RequirementType RequirementType { get; }

/// <summary>
/// Negates the evaluation for whether or not the wrapped filter should execute.
/// </summary>
public bool Negate { get; }

public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
IFeatureManager featureManager = context.HttpContext.RequestServices.GetRequiredService<IFeatureManagerSnapshot>();
IFeatureManagerSnapshot featureManager = context.HttpContext.RequestServices.GetRequiredService<IFeatureManagerSnapshot>();

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<T>(context.HttpContext.RequestServices);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,63 @@ public static class FilterCollectionExtensions
/// <typeparam name="TFilterType">The MVC filter to add and use if the feature is enabled.</typeparam>
/// <param name="filters">The filter collection to add to.</param>
/// <param name="feature">The feature that will need to enabled to trigger the execution of the MVC filter.</param>
/// <returns></returns>
/// <returns>The reference to the added filter metadata.</returns>
public static IFilterMetadata AddForFeature<TFilterType>(this FilterCollection filters, string feature) where TFilterType : IAsyncActionFilter
{
IFilterMetadata filterMetadata = null;
IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter<TFilterType>(RequirementType.Any, false, feature);

filters.Add(new FeatureGatedAsyncActionFilter<TFilterType>(feature));
filters.Add(filterMetadata);

return filterMetadata;
}

/// <summary>
/// Adds an MVC filter that will only activate during a request if the specified feature is enabled.
/// </summary>
/// <typeparam name="TFilterType">The MVC filter to add and use if the feature is enabled.</typeparam>
/// <param name="filters">The filter collection to add to.</param>
/// <param name="features">The features that control whether the MVC filter executes.</param>
/// <returns>The reference to the added filter metadata.</returns>
public static IFilterMetadata AddForFeature<TFilterType>(this FilterCollection filters, params string[] features) where TFilterType : IAsyncActionFilter
{
IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter<TFilterType>(RequirementType.Any, false, features);

filters.Add(filterMetadata);

return filterMetadata;
}

/// <summary>
/// Adds an MVC filter that will only activate during a request if the specified features are enabled based on the provided requirement type.
/// </summary>
/// <typeparam name="TFilterType">The MVC filter to add and use if the features condition is satisfied.</typeparam>
/// <param name="filters">The filter collection to add to.</param>
/// <param name="requirementType">Specifies whether all or any of the provided features should be enabled.</param>
/// <param name="features">The features that control whether the MVC filter executes.</param>
/// <returns>The reference to the added filter metadata.</returns>
public static IFilterMetadata AddForFeature<TFilterType>(this FilterCollection filters, RequirementType requirementType, params string[] features) where TFilterType : IAsyncActionFilter
{
IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter<TFilterType>(requirementType, false, features);

filters.Add(filterMetadata);

return filterMetadata;
}

/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TFilterType">The MVC filter to add and use if the features condition is satisfied.</typeparam>
/// <param name="filters">The filter collection to add to.</param>
/// <param name="requirementType">Specifies whether all or any of the provided features should be enabled.</param>
/// <param name="negate">Whether to negate the evaluation result for the provided features set.</param>
/// <param name="features">The features that control whether the MVC filter executes.</param>
/// <returns>The reference to the added filter metadata.</returns>
public static IFilterMetadata AddForFeature<TFilterType>(this FilterCollection filters, RequirementType requirementType, bool negate, params string[] features) where TFilterType : IAsyncActionFilter
{
IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter<TFilterType>(requirementType, negate, features);

filters.Add(filterMetadata);

return filterMetadata;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestFilter>();

services.AddMvcCore(o =>
{
DisableEndpointRouting(o);
o.Filters.AddForFeature<MvcFilter>(RequirementType.All, Features.ConditionalFeature, Features.ConditionalFeature2);
});
}).Configure(app => app.UseMvc()));

TestFilter filter = (TestFilter)server.Host.Services.GetRequiredService<IEnumerable<IFeatureFilterMetadata>>().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;
Expand Down
Loading