Skip to content

Commit d8061e9

Browse files
Update FilterCollectionExtensions (#359)
* update * update * update * update AddForFeature * add test * update
1 parent 7b4f387 commit d8061e9

File tree

3 files changed

+143
-11
lines changed

3 files changed

+143
-11
lines changed

src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,73 @@
44
using Microsoft.AspNetCore.Mvc.Filters;
55
using Microsoft.Extensions.DependencyInjection;
66
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
79
using System.Threading.Tasks;
810

911
namespace Microsoft.FeatureManagement
1012
{
1113
/// <summary>
12-
/// A place holder MVC filter that is used to dynamically activate a filter based on whether a feature is enabled.
14+
/// A place holder MVC filter that is used to dynamically activate a filter based on whether a feature (or set of features) is enabled.
1315
/// </summary>
1416
/// <typeparam name="T">The filter that will be used instead of this placeholder.</typeparam>
1517
class FeatureGatedAsyncActionFilter<T> : IAsyncActionFilter where T : IAsyncActionFilter
1618
{
17-
public FeatureGatedAsyncActionFilter(string featureName)
19+
/// <summary>
20+
/// Creates a feature gated filter for multiple features with a specified requirement type and ability to negate the evaluation.
21+
/// </summary>
22+
/// <param name="requirementType">Specifies whether all or any of the provided features should be enabled.</param>
23+
/// <param name="negate">Whether to negate the evaluation result.</param>
24+
/// <param name="features">The features that control whether the wrapped filter executes.</param>
25+
public FeatureGatedAsyncActionFilter(RequirementType requirementType, bool negate, params string[] features)
1826
{
19-
if (string.IsNullOrEmpty(featureName))
27+
if (features == null || features.Length == 0)
2028
{
21-
throw new ArgumentNullException(nameof(featureName));
29+
throw new ArgumentNullException(nameof(features));
2230
}
2331

24-
FeatureName = featureName;
32+
Features = features;
33+
RequirementType = requirementType;
34+
Negate = negate;
2535
}
2636

27-
public string FeatureName { get; }
37+
/// <summary>
38+
/// The set of features that gate the wrapped filter.
39+
/// </summary>
40+
public IEnumerable<string> Features { get; }
41+
42+
/// <summary>
43+
/// Controls whether any or all features in <see cref="Features"/> should be enabled to allow the wrapped filter to execute.
44+
/// </summary>
45+
public RequirementType RequirementType { get; }
46+
47+
/// <summary>
48+
/// Negates the evaluation for whether or not the wrapped filter should execute.
49+
/// </summary>
50+
public bool Negate { get; }
2851

2952
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
3053
{
31-
IFeatureManager featureManager = context.HttpContext.RequestServices.GetRequiredService<IFeatureManagerSnapshot>();
54+
IFeatureManagerSnapshot featureManager = context.HttpContext.RequestServices.GetRequiredService<IFeatureManagerSnapshot>();
55+
56+
bool enabled;
57+
58+
// Enabled state is determined by either 'any' or 'all' features being enabled.
59+
if (RequirementType == RequirementType.All)
60+
{
61+
enabled = await Features.All(async f => await featureManager.IsEnabledAsync(f).ConfigureAwait(false));
62+
}
63+
else
64+
{
65+
enabled = await Features.Any(async f => await featureManager.IsEnabledAsync(f).ConfigureAwait(false));
66+
}
67+
68+
if (Negate)
69+
{
70+
enabled = !enabled;
71+
}
3272

33-
if (await featureManager.IsEnabledAsync(FeatureName).ConfigureAwait(false))
73+
if (enabled)
3474
{
3575
IAsyncActionFilter filter = ActivatorUtilities.CreateInstance<T>(context.HttpContext.RequestServices);
3676

src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,63 @@ public static class FilterCollectionExtensions
1616
/// <typeparam name="TFilterType">The MVC filter to add and use if the feature is enabled.</typeparam>
1717
/// <param name="filters">The filter collection to add to.</param>
1818
/// <param name="feature">The feature that will need to enabled to trigger the execution of the MVC filter.</param>
19-
/// <returns></returns>
19+
/// <returns>The reference to the added filter metadata.</returns>
2020
public static IFilterMetadata AddForFeature<TFilterType>(this FilterCollection filters, string feature) where TFilterType : IAsyncActionFilter
2121
{
22-
IFilterMetadata filterMetadata = null;
22+
IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter<TFilterType>(RequirementType.Any, false, feature);
2323

24-
filters.Add(new FeatureGatedAsyncActionFilter<TFilterType>(feature));
24+
filters.Add(filterMetadata);
25+
26+
return filterMetadata;
27+
}
28+
29+
/// <summary>
30+
/// Adds an MVC filter that will only activate during a request if the specified feature is enabled.
31+
/// </summary>
32+
/// <typeparam name="TFilterType">The MVC filter to add and use if the feature is enabled.</typeparam>
33+
/// <param name="filters">The filter collection to add to.</param>
34+
/// <param name="features">The features that control whether the MVC filter executes.</param>
35+
/// <returns>The reference to the added filter metadata.</returns>
36+
public static IFilterMetadata AddForFeature<TFilterType>(this FilterCollection filters, params string[] features) where TFilterType : IAsyncActionFilter
37+
{
38+
IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter<TFilterType>(RequirementType.Any, false, features);
39+
40+
filters.Add(filterMetadata);
41+
42+
return filterMetadata;
43+
}
44+
45+
/// <summary>
46+
/// Adds an MVC filter that will only activate during a request if the specified features are enabled based on the provided requirement type.
47+
/// </summary>
48+
/// <typeparam name="TFilterType">The MVC filter to add and use if the features condition is satisfied.</typeparam>
49+
/// <param name="filters">The filter collection to add to.</param>
50+
/// <param name="requirementType">Specifies whether all or any of the provided features should be enabled.</param>
51+
/// <param name="features">The features that control whether the MVC filter executes.</param>
52+
/// <returns>The reference to the added filter metadata.</returns>
53+
public static IFilterMetadata AddForFeature<TFilterType>(this FilterCollection filters, RequirementType requirementType, params string[] features) where TFilterType : IAsyncActionFilter
54+
{
55+
IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter<TFilterType>(requirementType, false, features);
56+
57+
filters.Add(filterMetadata);
58+
59+
return filterMetadata;
60+
}
61+
62+
/// <summary>
63+
/// 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.
64+
/// </summary>
65+
/// <typeparam name="TFilterType">The MVC filter to add and use if the features condition is satisfied.</typeparam>
66+
/// <param name="filters">The filter collection to add to.</param>
67+
/// <param name="requirementType">Specifies whether all or any of the provided features should be enabled.</param>
68+
/// <param name="negate">Whether to negate the evaluation result for the provided features set.</param>
69+
/// <param name="features">The features that control whether the MVC filter executes.</param>
70+
/// <returns>The reference to the added filter metadata.</returns>
71+
public static IFilterMetadata AddForFeature<TFilterType>(this FilterCollection filters, RequirementType requirementType, bool negate, params string[] features) where TFilterType : IAsyncActionFilter
72+
{
73+
IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter<TFilterType>(requirementType, negate, features);
74+
75+
filters.Add(filterMetadata);
2576

2677
return filterMetadata;
2778
}

tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,47 @@ public async Task GatesRazorPageFeatures()
202202
Assert.Equal(HttpStatusCode.OK, gateAnyNegateResponse.StatusCode);
203203
}
204204

205+
[Fact]
206+
public async Task GatesActionFilterFeatures()
207+
{
208+
IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
209+
210+
TestServer server = new TestServer(WebHost.CreateDefaultBuilder().ConfigureServices(services =>
211+
{
212+
services
213+
.AddSingleton(config)
214+
.AddFeatureManagement()
215+
.AddFeatureFilter<TestFilter>();
216+
217+
services.AddMvcCore(o =>
218+
{
219+
DisableEndpointRouting(o);
220+
o.Filters.AddForFeature<MvcFilter>(RequirementType.All, Features.ConditionalFeature, Features.ConditionalFeature2);
221+
});
222+
}).Configure(app => app.UseMvc()));
223+
224+
TestFilter filter = (TestFilter)server.Host.Services.GetRequiredService<IEnumerable<IFeatureFilterMetadata>>().First(f => f is TestFilter);
225+
HttpClient client = server.CreateClient();
226+
227+
//
228+
// Enable all features
229+
filter.Callback = _ => Task.FromResult(true);
230+
HttpResponseMessage res = await client.GetAsync("");
231+
Assert.True(res.Headers.Contains(nameof(MvcFilter)));
232+
233+
//
234+
// Enable 1/2 features
235+
filter.Callback = ctx => Task.FromResult(ctx.FeatureName == Features.ConditionalFeature);
236+
res = await client.GetAsync("");
237+
Assert.False(res.Headers.Contains(nameof(MvcFilter)));
238+
239+
//
240+
// Enable no
241+
filter.Callback = _ => Task.FromResult(false);
242+
res = await client.GetAsync("");
243+
Assert.False(res.Headers.Contains(nameof(MvcFilter)));
244+
}
245+
205246
private static void DisableEndpointRouting(MvcOptions options)
206247
{
207248
options.EnableEndpointRouting = false;

0 commit comments

Comments
 (0)