Skip to content

Commit 325271b

Browse files
Negate FeatureGateAttribute (#519)
* add negate property for FeatureGateAttriburte * update * revert change * add testcases * fix lint
1 parent 57c8f24 commit 325271b

File tree

7 files changed

+147
-3
lines changed

7 files changed

+147
-3
lines changed

src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public class FeatureGateAttribute : ActionFilterAttribute, IAsyncPageFilter
2222
/// </summary>
2323
/// <param name="features">The names of the features that the attribute will represent.</param>
2424
public FeatureGateAttribute(params string[] features)
25-
: this(RequirementType.All, features)
25+
: this(RequirementType.All, false, features)
2626
{
2727
}
2828

@@ -32,6 +32,27 @@ public FeatureGateAttribute(params string[] features)
3232
/// <param name="requirementType">Specifies whether all or any of the provided features should be enabled in order to pass.</param>
3333
/// <param name="features">The names of the features that the attribute will represent.</param>
3434
public FeatureGateAttribute(RequirementType requirementType, params string[] features)
35+
: this(requirementType, false, features)
36+
{
37+
}
38+
39+
/// <summary>
40+
/// Creates an attribute that can be used to gate actions or pages. The gate can be configured to negate the evaluation result.
41+
/// </summary>
42+
/// <param name="negate">Specifies the evaluation for the provided features gate should be negated.</param>
43+
/// <param name="features">The names of the features that the attribute will represent.</param>
44+
public FeatureGateAttribute(bool negate, params string[] features)
45+
: this(RequirementType.All, negate, features)
46+
{
47+
}
48+
49+
/// <summary>
50+
/// Creates an attribute that can be used to gate actions or pages. The gate can be configured to require all or any of the provided feature(s) to pass or negate the evaluation result.
51+
/// </summary>
52+
/// <param name="requirementType">Specifies whether all or any of the provided features should be enabled in order to pass.</param>
53+
/// <param name="negate">Specifies the evaluation for the provided features gate should be negated.</param>
54+
/// <param name="features">The names of the features that the attribute will represent.</param>
55+
public FeatureGateAttribute(RequirementType requirementType, bool negate, params string[] features)
3556
{
3657
if (features == null || features.Length == 0)
3758
{
@@ -41,14 +62,16 @@ public FeatureGateAttribute(RequirementType requirementType, params string[] fea
4162
Features = features;
4263

4364
RequirementType = requirementType;
65+
66+
Negate = negate;
4467
}
4568

4669
/// <summary>
4770
/// Creates an attribute that will gate actions or pages unless all the provided feature(s) are enabled.
4871
/// </summary>
4972
/// <param name="features">A set of enums representing the features that the attribute will represent.</param>
5073
public FeatureGateAttribute(params object[] features)
51-
: this(RequirementType.All, features)
74+
: this(RequirementType.All, false, features)
5275
{
5376
}
5477

@@ -58,6 +81,27 @@ public FeatureGateAttribute(params object[] features)
5881
/// <param name="requirementType">Specifies whether all or any of the provided features should be enabled in order to pass.</param>
5982
/// <param name="features">A set of enums representing the features that the attribute will represent.</param>
6083
public FeatureGateAttribute(RequirementType requirementType, params object[] features)
84+
: this(requirementType, false, features)
85+
{
86+
}
87+
88+
/// <summary>
89+
/// Creates an attribute that can be used to gate actions or pages. The gate can be configured to negate the evaluation result.
90+
/// </summary>
91+
/// <param name="negate">Specifies the evaluation for the provided features gate should be negated.</param>
92+
/// <param name="features">A set of enums representing the features that the attribute will represent.</param>
93+
public FeatureGateAttribute(bool negate, params object[] features)
94+
: this(RequirementType.All, negate, features)
95+
{
96+
}
97+
98+
/// <summary>
99+
/// Creates an attribute that can be used to gate actions or pages. The gate can be configured to require all or any of the provided feature(s) to pass or negate the evaluation result.
100+
/// </summary>
101+
/// <param name="requirementType">Specifies whether all or any of the provided features should be enabled in order to pass.</param>
102+
/// <param name="negate">Specifies the evaluation for the provided features gate should be negated.</param>
103+
/// <param name="features">A set of enums representing the features that the attribute will represent.</param>
104+
public FeatureGateAttribute(RequirementType requirementType, bool negate, params object[] features)
61105
{
62106
if (features == null || features.Length == 0)
63107
{
@@ -82,6 +126,8 @@ public FeatureGateAttribute(RequirementType requirementType, params object[] fea
82126
Features = fs;
83127

84128
RequirementType = requirementType;
129+
130+
Negate = negate;
85131
}
86132

87133
/// <summary>
@@ -94,6 +140,11 @@ public FeatureGateAttribute(RequirementType requirementType, params object[] fea
94140
/// </summary>
95141
public RequirementType RequirementType { get; }
96142

143+
/// <summary>
144+
/// Negates the evaluation for whether or not a feature gate should activate.
145+
/// </summary>
146+
public bool Negate { get; }
147+
97148
/// <summary>
98149
/// Performs controller action pre-processing to ensure that any or all of the specified features are enabled.
99150
/// </summary>
@@ -110,6 +161,11 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context
110161
? await Features.All(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false))
111162
: await Features.Any(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false));
112163

164+
if (Negate)
165+
{
166+
enabled = !enabled;
167+
}
168+
113169
if (enabled)
114170
{
115171
await next().ConfigureAwait(false);
@@ -138,6 +194,11 @@ public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext contex
138194
? await Features.All(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false))
139195
: await Features.Any(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false));
140196

197+
if (Negate)
198+
{
199+
enabled = !enabled;
200+
}
201+
141202
if (enabled)
142203
{
143204
await next.Invoke().ConfigureAwait(false);

tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,29 +97,41 @@ public async Task GatesFeatures()
9797

9898
HttpResponseMessage gateAllResponse = await testServer.CreateClient().GetAsync("gateAll");
9999
HttpResponseMessage gateAnyResponse = await testServer.CreateClient().GetAsync("gateAny");
100+
HttpResponseMessage gateAllNegateResponse = await testServer.CreateClient().GetAsync("gateAllNegate");
101+
HttpResponseMessage gateAnyNegateResponse = await testServer.CreateClient().GetAsync("gateAnyNegate");
100102

101103
Assert.Equal(HttpStatusCode.OK, gateAllResponse.StatusCode);
102104
Assert.Equal(HttpStatusCode.OK, gateAnyResponse.StatusCode);
105+
Assert.Equal(HttpStatusCode.NotFound, gateAllNegateResponse.StatusCode);
106+
Assert.Equal(HttpStatusCode.NotFound, gateAnyNegateResponse.StatusCode);
103107

104108
//
105109
// Enable 1/2 features
106110
testFeatureFilter.Callback = ctx => Task.FromResult(ctx.FeatureName == Features.ConditionalFeature);
107111

108112
gateAllResponse = await testServer.CreateClient().GetAsync("gateAll");
109113
gateAnyResponse = await testServer.CreateClient().GetAsync("gateAny");
114+
gateAllNegateResponse = await testServer.CreateClient().GetAsync("gateAllNegate");
115+
gateAnyNegateResponse = await testServer.CreateClient().GetAsync("gateAnyNegate");
110116

111117
Assert.Equal(HttpStatusCode.NotFound, gateAllResponse.StatusCode);
112118
Assert.Equal(HttpStatusCode.OK, gateAnyResponse.StatusCode);
119+
Assert.Equal(HttpStatusCode.OK, gateAllNegateResponse.StatusCode);
120+
Assert.Equal(HttpStatusCode.NotFound, gateAnyNegateResponse.StatusCode);
113121

114122
//
115123
// Enable no
116124
testFeatureFilter.Callback = ctx => Task.FromResult(false);
117125

118126
gateAllResponse = await testServer.CreateClient().GetAsync("gateAll");
119127
gateAnyResponse = await testServer.CreateClient().GetAsync("gateAny");
128+
gateAllNegateResponse = await testServer.CreateClient().GetAsync("gateAllNegate");
129+
gateAnyNegateResponse = await testServer.CreateClient().GetAsync("gateAnyNegate");
120130

121131
Assert.Equal(HttpStatusCode.NotFound, gateAllResponse.StatusCode);
122132
Assert.Equal(HttpStatusCode.NotFound, gateAnyResponse.StatusCode);
133+
Assert.Equal(HttpStatusCode.OK, gateAllNegateResponse.StatusCode);
134+
Assert.Equal(HttpStatusCode.OK, gateAnyNegateResponse.StatusCode);
123135
}
124136

125137
[Fact]
@@ -153,29 +165,41 @@ public async Task GatesRazorPageFeatures()
153165

154166
HttpResponseMessage gateAllResponse = await testServer.CreateClient().GetAsync("RazorTestAll");
155167
HttpResponseMessage gateAnyResponse = await testServer.CreateClient().GetAsync("RazorTestAny");
168+
HttpResponseMessage gateAllNegateResponse = await testServer.CreateClient().GetAsync("RazorTestAllNegate");
169+
HttpResponseMessage gateAnyNegateResponse = await testServer.CreateClient().GetAsync("RazorTestAnyNegate");
156170

157171
Assert.Equal(HttpStatusCode.OK, gateAllResponse.StatusCode);
158172
Assert.Equal(HttpStatusCode.OK, gateAnyResponse.StatusCode);
173+
Assert.Equal(HttpStatusCode.NotFound, gateAllNegateResponse.StatusCode);
174+
Assert.Equal(HttpStatusCode.NotFound, gateAnyNegateResponse.StatusCode);
159175

160176
//
161177
// Enable 1/2 features
162178
testFeatureFilter.Callback = ctx => Task.FromResult(ctx.FeatureName == Features.ConditionalFeature);
163179

164180
gateAllResponse = await testServer.CreateClient().GetAsync("RazorTestAll");
165181
gateAnyResponse = await testServer.CreateClient().GetAsync("RazorTestAny");
182+
gateAllNegateResponse = await testServer.CreateClient().GetAsync("RazorTestAllNegate");
183+
gateAnyNegateResponse = await testServer.CreateClient().GetAsync("RazorTestAnyNegate");
166184

167185
Assert.Equal(HttpStatusCode.NotFound, gateAllResponse.StatusCode);
168186
Assert.Equal(HttpStatusCode.OK, gateAnyResponse.StatusCode);
187+
Assert.Equal(HttpStatusCode.OK, gateAllNegateResponse.StatusCode);
188+
Assert.Equal(HttpStatusCode.NotFound, gateAnyNegateResponse.StatusCode);
169189

170190
//
171191
// Enable no
172192
testFeatureFilter.Callback = ctx => Task.FromResult(false);
173193

174194
gateAllResponse = await testServer.CreateClient().GetAsync("RazorTestAll");
175195
gateAnyResponse = await testServer.CreateClient().GetAsync("RazorTestAny");
196+
gateAllNegateResponse = await testServer.CreateClient().GetAsync("RazorTestAllNegate");
197+
gateAnyNegateResponse = await testServer.CreateClient().GetAsync("RazorTestAnyNegate");
176198

177199
Assert.Equal(HttpStatusCode.NotFound, gateAllResponse.StatusCode);
178200
Assert.Equal(HttpStatusCode.NotFound, gateAnyResponse.StatusCode);
201+
Assert.Equal(HttpStatusCode.OK, gateAllNegateResponse.StatusCode);
202+
Assert.Equal(HttpStatusCode.OK, gateAnyNegateResponse.StatusCode);
179203
}
180204

181205
private static void DisableEndpointRouting(MvcOptions options)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@page
2+
@model RazorTestAllNegateModel
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.AspNetCore.Mvc.RazorPages;
6+
using Microsoft.FeatureManagement.Mvc;
7+
8+
namespace Tests.FeatureManagement.AspNetCore.Pages
9+
{
10+
[FeatureGate(negate: true, Features.ConditionalFeature, Features.ConditionalFeature2)]
11+
public class RazorTestAllNegateModel : PageModel
12+
{
13+
public IActionResult OnGet()
14+
{
15+
return new OkResult();
16+
}
17+
}
18+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@page
2+
@model Tests.FeatureManagement.AspNetCore.Pages.RazorTestAnyNegateModel
3+
@{
4+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.AspNetCore.Mvc.RazorPages;
6+
using Microsoft.FeatureManagement;
7+
using Microsoft.FeatureManagement.Mvc;
8+
9+
namespace Tests.FeatureManagement.AspNetCore.Pages
10+
{
11+
[FeatureGate(requirementType: RequirementType.Any, negate: true, Features.ConditionalFeature, Features.ConditionalFeature2)]
12+
public class RazorTestAnyNegateModel : PageModel
13+
{
14+
public IActionResult OnGet()
15+
{
16+
return new OkResult();
17+
}
18+
}
19+
}

tests/Tests.FeatureManagement.AspNetCore/TestController.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,26 @@ public IActionResult GateAll()
2626

2727
[Route("/gateAny")]
2828
[HttpGet]
29-
[FeatureGate(RequirementType.Any, Features.ConditionalFeature, Features.ConditionalFeature2)]
29+
[FeatureGate(requirementType: RequirementType.Any, Features.ConditionalFeature, Features.ConditionalFeature2)]
3030
public IActionResult GateAny()
3131
{
3232
return Ok();
3333
}
34+
35+
[Route("/gateAllNegate")]
36+
[HttpGet]
37+
[FeatureGate(negate: true, Features.ConditionalFeature, Features.ConditionalFeature2)]
38+
public IActionResult GateAllNegate()
39+
{
40+
return Ok();
41+
}
42+
43+
[Route("/gateAnyNegate")]
44+
[HttpGet]
45+
[FeatureGate(requirementType: RequirementType.Any, negate: true, Features.ConditionalFeature, Features.ConditionalFeature2)]
46+
public IActionResult GateAnyNegate()
47+
{
48+
return Ok();
49+
}
3450
}
3551
}

0 commit comments

Comments
 (0)