Skip to content
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