diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs
index 98ba2096..0c87b107 100644
--- a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs
+++ b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs
@@ -22,7 +22,7 @@ public class FeatureGateAttribute : ActionFilterAttribute, IAsyncPageFilter
///
/// The names of the features that the attribute will represent.
public FeatureGateAttribute(params string[] features)
- : this(RequirementType.All, features)
+ : this(RequirementType.All, false, features)
{
}
@@ -32,6 +32,27 @@ public FeatureGateAttribute(params string[] features)
/// Specifies whether all or any of the provided features should be enabled in order to pass.
/// The names of the features that the attribute will represent.
public FeatureGateAttribute(RequirementType requirementType, params string[] features)
+ : this(requirementType, false, features)
+ {
+ }
+
+ ///
+ /// Creates an attribute that can be used to gate actions or pages. The gate can be configured to negate the evaluation result.
+ ///
+ /// Specifies the evaluation for the provided features gate should be negated.
+ /// The names of the features that the attribute will represent.
+ public FeatureGateAttribute(bool negate, params string[] features)
+ : this(RequirementType.All, negate, features)
+ {
+ }
+
+ ///
+ /// 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.
+ ///
+ /// Specifies whether all or any of the provided features should be enabled in order to pass.
+ /// Specifies the evaluation for the provided features gate should be negated.
+ /// The names of the features that the attribute will represent.
+ public FeatureGateAttribute(RequirementType requirementType, bool negate, params string[] features)
{
if (features == null || features.Length == 0)
{
@@ -41,6 +62,8 @@ public FeatureGateAttribute(RequirementType requirementType, params string[] fea
Features = features;
RequirementType = requirementType;
+
+ Negate = negate;
}
///
@@ -48,7 +71,7 @@ public FeatureGateAttribute(RequirementType requirementType, params string[] fea
///
/// A set of enums representing the features that the attribute will represent.
public FeatureGateAttribute(params object[] features)
- : this(RequirementType.All, features)
+ : this(RequirementType.All, false, features)
{
}
@@ -58,6 +81,27 @@ public FeatureGateAttribute(params object[] features)
/// Specifies whether all or any of the provided features should be enabled in order to pass.
/// A set of enums representing the features that the attribute will represent.
public FeatureGateAttribute(RequirementType requirementType, params object[] features)
+ : this(requirementType, false, features)
+ {
+ }
+
+ ///
+ /// Creates an attribute that can be used to gate actions or pages. The gate can be configured to negate the evaluation result.
+ ///
+ /// Specifies the evaluation for the provided features gate should be negated.
+ /// A set of enums representing the features that the attribute will represent.
+ public FeatureGateAttribute(bool negate, params object[] features)
+ : this(RequirementType.All, negate, features)
+ {
+ }
+
+ ///
+ /// 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.
+ ///
+ /// Specifies whether all or any of the provided features should be enabled in order to pass.
+ /// Specifies the evaluation for the provided features gate should be negated.
+ /// A set of enums representing the features that the attribute will represent.
+ public FeatureGateAttribute(RequirementType requirementType, bool negate, params object[] features)
{
if (features == null || features.Length == 0)
{
@@ -82,6 +126,8 @@ public FeatureGateAttribute(RequirementType requirementType, params object[] fea
Features = fs;
RequirementType = requirementType;
+
+ Negate = negate;
}
///
@@ -94,6 +140,11 @@ public FeatureGateAttribute(RequirementType requirementType, params object[] fea
///
public RequirementType RequirementType { get; }
+ ///
+ /// Negates the evaluation for whether or not a feature gate should activate.
+ ///
+ public bool Negate { get; }
+
///
/// Performs controller action pre-processing to ensure that any or all of the specified features are enabled.
///
@@ -110,6 +161,11 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context
? await Features.All(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false))
: await Features.Any(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false));
+ if (Negate)
+ {
+ enabled = !enabled;
+ }
+
if (enabled)
{
await next().ConfigureAwait(false);
@@ -138,6 +194,11 @@ public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext contex
? await Features.All(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false))
: await Features.Any(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false));
+ if (Negate)
+ {
+ enabled = !enabled;
+ }
+
if (enabled)
{
await next.Invoke().ConfigureAwait(false);
diff --git a/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs b/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs
index 79352588..68b7efc1 100644
--- a/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs
+++ b/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs
@@ -97,9 +97,13 @@ public async Task GatesFeatures()
HttpResponseMessage gateAllResponse = await testServer.CreateClient().GetAsync("gateAll");
HttpResponseMessage gateAnyResponse = await testServer.CreateClient().GetAsync("gateAny");
+ HttpResponseMessage gateAllNegateResponse = await testServer.CreateClient().GetAsync("gateAllNegate");
+ HttpResponseMessage gateAnyNegateResponse = await testServer.CreateClient().GetAsync("gateAnyNegate");
Assert.Equal(HttpStatusCode.OK, gateAllResponse.StatusCode);
Assert.Equal(HttpStatusCode.OK, gateAnyResponse.StatusCode);
+ Assert.Equal(HttpStatusCode.NotFound, gateAllNegateResponse.StatusCode);
+ Assert.Equal(HttpStatusCode.NotFound, gateAnyNegateResponse.StatusCode);
//
// Enable 1/2 features
@@ -107,9 +111,13 @@ public async Task GatesFeatures()
gateAllResponse = await testServer.CreateClient().GetAsync("gateAll");
gateAnyResponse = await testServer.CreateClient().GetAsync("gateAny");
+ gateAllNegateResponse = await testServer.CreateClient().GetAsync("gateAllNegate");
+ gateAnyNegateResponse = await testServer.CreateClient().GetAsync("gateAnyNegate");
Assert.Equal(HttpStatusCode.NotFound, gateAllResponse.StatusCode);
Assert.Equal(HttpStatusCode.OK, gateAnyResponse.StatusCode);
+ Assert.Equal(HttpStatusCode.OK, gateAllNegateResponse.StatusCode);
+ Assert.Equal(HttpStatusCode.NotFound, gateAnyNegateResponse.StatusCode);
//
// Enable no
@@ -117,9 +125,13 @@ public async Task GatesFeatures()
gateAllResponse = await testServer.CreateClient().GetAsync("gateAll");
gateAnyResponse = await testServer.CreateClient().GetAsync("gateAny");
+ gateAllNegateResponse = await testServer.CreateClient().GetAsync("gateAllNegate");
+ gateAnyNegateResponse = await testServer.CreateClient().GetAsync("gateAnyNegate");
Assert.Equal(HttpStatusCode.NotFound, gateAllResponse.StatusCode);
Assert.Equal(HttpStatusCode.NotFound, gateAnyResponse.StatusCode);
+ Assert.Equal(HttpStatusCode.OK, gateAllNegateResponse.StatusCode);
+ Assert.Equal(HttpStatusCode.OK, gateAnyNegateResponse.StatusCode);
}
[Fact]
@@ -153,9 +165,13 @@ public async Task GatesRazorPageFeatures()
HttpResponseMessage gateAllResponse = await testServer.CreateClient().GetAsync("RazorTestAll");
HttpResponseMessage gateAnyResponse = await testServer.CreateClient().GetAsync("RazorTestAny");
+ HttpResponseMessage gateAllNegateResponse = await testServer.CreateClient().GetAsync("RazorTestAllNegate");
+ HttpResponseMessage gateAnyNegateResponse = await testServer.CreateClient().GetAsync("RazorTestAnyNegate");
Assert.Equal(HttpStatusCode.OK, gateAllResponse.StatusCode);
Assert.Equal(HttpStatusCode.OK, gateAnyResponse.StatusCode);
+ Assert.Equal(HttpStatusCode.NotFound, gateAllNegateResponse.StatusCode);
+ Assert.Equal(HttpStatusCode.NotFound, gateAnyNegateResponse.StatusCode);
//
// Enable 1/2 features
@@ -163,9 +179,13 @@ public async Task GatesRazorPageFeatures()
gateAllResponse = await testServer.CreateClient().GetAsync("RazorTestAll");
gateAnyResponse = await testServer.CreateClient().GetAsync("RazorTestAny");
+ gateAllNegateResponse = await testServer.CreateClient().GetAsync("RazorTestAllNegate");
+ gateAnyNegateResponse = await testServer.CreateClient().GetAsync("RazorTestAnyNegate");
Assert.Equal(HttpStatusCode.NotFound, gateAllResponse.StatusCode);
Assert.Equal(HttpStatusCode.OK, gateAnyResponse.StatusCode);
+ Assert.Equal(HttpStatusCode.OK, gateAllNegateResponse.StatusCode);
+ Assert.Equal(HttpStatusCode.NotFound, gateAnyNegateResponse.StatusCode);
//
// Enable no
@@ -173,9 +193,13 @@ public async Task GatesRazorPageFeatures()
gateAllResponse = await testServer.CreateClient().GetAsync("RazorTestAll");
gateAnyResponse = await testServer.CreateClient().GetAsync("RazorTestAny");
+ gateAllNegateResponse = await testServer.CreateClient().GetAsync("RazorTestAllNegate");
+ gateAnyNegateResponse = await testServer.CreateClient().GetAsync("RazorTestAnyNegate");
Assert.Equal(HttpStatusCode.NotFound, gateAllResponse.StatusCode);
Assert.Equal(HttpStatusCode.NotFound, gateAnyResponse.StatusCode);
+ Assert.Equal(HttpStatusCode.OK, gateAllNegateResponse.StatusCode);
+ Assert.Equal(HttpStatusCode.OK, gateAnyNegateResponse.StatusCode);
}
private static void DisableEndpointRouting(MvcOptions options)
diff --git a/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAllNegate.cshtml b/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAllNegate.cshtml
new file mode 100644
index 00000000..a4e91e45
--- /dev/null
+++ b/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAllNegate.cshtml
@@ -0,0 +1,2 @@
+@page
+@model RazorTestAllNegateModel
diff --git a/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAllNegate.cshtml.cs b/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAllNegate.cshtml.cs
new file mode 100644
index 00000000..ba9aff1c
--- /dev/null
+++ b/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAllNegate.cshtml.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.FeatureManagement.Mvc;
+
+namespace Tests.FeatureManagement.AspNetCore.Pages
+{
+ [FeatureGate(negate: true, Features.ConditionalFeature, Features.ConditionalFeature2)]
+ public class RazorTestAllNegateModel : PageModel
+ {
+ public IActionResult OnGet()
+ {
+ return new OkResult();
+ }
+ }
+}
diff --git a/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAnyNegate.cshtml b/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAnyNegate.cshtml
new file mode 100644
index 00000000..d232c3fe
--- /dev/null
+++ b/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAnyNegate.cshtml
@@ -0,0 +1,4 @@
+@page
+@model Tests.FeatureManagement.AspNetCore.Pages.RazorTestAnyNegateModel
+@{
+}
diff --git a/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAnyNegate.cshtml.cs b/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAnyNegate.cshtml.cs
new file mode 100644
index 00000000..09821913
--- /dev/null
+++ b/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAnyNegate.cshtml.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.FeatureManagement;
+using Microsoft.FeatureManagement.Mvc;
+
+namespace Tests.FeatureManagement.AspNetCore.Pages
+{
+ [FeatureGate(requirementType: RequirementType.Any, negate: true, Features.ConditionalFeature, Features.ConditionalFeature2)]
+ public class RazorTestAnyNegateModel : PageModel
+ {
+ public IActionResult OnGet()
+ {
+ return new OkResult();
+ }
+ }
+}
diff --git a/tests/Tests.FeatureManagement.AspNetCore/TestController.cs b/tests/Tests.FeatureManagement.AspNetCore/TestController.cs
index 2f4c8ce5..6fc000a5 100644
--- a/tests/Tests.FeatureManagement.AspNetCore/TestController.cs
+++ b/tests/Tests.FeatureManagement.AspNetCore/TestController.cs
@@ -26,10 +26,26 @@ public IActionResult GateAll()
[Route("/gateAny")]
[HttpGet]
- [FeatureGate(RequirementType.Any, Features.ConditionalFeature, Features.ConditionalFeature2)]
+ [FeatureGate(requirementType: RequirementType.Any, Features.ConditionalFeature, Features.ConditionalFeature2)]
public IActionResult GateAny()
{
return Ok();
}
+
+ [Route("/gateAllNegate")]
+ [HttpGet]
+ [FeatureGate(negate: true, Features.ConditionalFeature, Features.ConditionalFeature2)]
+ public IActionResult GateAllNegate()
+ {
+ return Ok();
+ }
+
+ [Route("/gateAnyNegate")]
+ [HttpGet]
+ [FeatureGate(requirementType: RequirementType.Any, negate: true, Features.ConditionalFeature, Features.ConditionalFeature2)]
+ public IActionResult GateAnyNegate()
+ {
+ return Ok();
+ }
}
}