Skip to content

Commit

Permalink
Negate FeatureGateAttribute (#519)
Browse files Browse the repository at this point in the history
* add negate property for FeatureGateAttriburte

* update

* revert change

* add testcases

* fix lint
  • Loading branch information
zhiyuanliang-ms authored Dec 11, 2024
1 parent 57c8f24 commit 325271b
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 3 deletions.
65 changes: 63 additions & 2 deletions src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class FeatureGateAttribute : ActionFilterAttribute, IAsyncPageFilter
/// </summary>
/// <param name="features">The names of the features that the attribute will represent.</param>
public FeatureGateAttribute(params string[] features)
: this(RequirementType.All, features)
: this(RequirementType.All, false, features)
{
}

Expand All @@ -32,6 +32,27 @@ public FeatureGateAttribute(params string[] features)
/// <param name="requirementType">Specifies whether all or any of the provided features should be enabled in order to pass.</param>
/// <param name="features">The names of the features that the attribute will represent.</param>
public FeatureGateAttribute(RequirementType requirementType, params string[] features)
: this(requirementType, false, features)
{
}

/// <summary>
/// Creates an attribute that can be used to gate actions or pages. The gate can be configured to negate the evaluation result.
/// </summary>
/// <param name="negate">Specifies the evaluation for the provided features gate should be negated.</param>
/// <param name="features">The names of the features that the attribute will represent.</param>
public FeatureGateAttribute(bool negate, params string[] features)
: this(RequirementType.All, negate, features)
{
}

/// <summary>
/// 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.
/// </summary>
/// <param name="requirementType">Specifies whether all or any of the provided features should be enabled in order to pass.</param>
/// <param name="negate">Specifies the evaluation for the provided features gate should be negated.</param>
/// <param name="features">The names of the features that the attribute will represent.</param>
public FeatureGateAttribute(RequirementType requirementType, bool negate, params string[] features)
{
if (features == null || features.Length == 0)
{
Expand All @@ -41,14 +62,16 @@ public FeatureGateAttribute(RequirementType requirementType, params string[] fea
Features = features;

RequirementType = requirementType;

Negate = negate;
}

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

Expand All @@ -58,6 +81,27 @@ public FeatureGateAttribute(params object[] features)
/// <param name="requirementType">Specifies whether all or any of the provided features should be enabled in order to pass.</param>
/// <param name="features">A set of enums representing the features that the attribute will represent.</param>
public FeatureGateAttribute(RequirementType requirementType, params object[] features)
: this(requirementType, false, features)
{
}

/// <summary>
/// Creates an attribute that can be used to gate actions or pages. The gate can be configured to negate the evaluation result.
/// </summary>
/// <param name="negate">Specifies the evaluation for the provided features gate should be negated.</param>
/// <param name="features">A set of enums representing the features that the attribute will represent.</param>
public FeatureGateAttribute(bool negate, params object[] features)
: this(RequirementType.All, negate, features)
{
}

/// <summary>
/// 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.
/// </summary>
/// <param name="requirementType">Specifies whether all or any of the provided features should be enabled in order to pass.</param>
/// <param name="negate">Specifies the evaluation for the provided features gate should be negated.</param>
/// <param name="features">A set of enums representing the features that the attribute will represent.</param>
public FeatureGateAttribute(RequirementType requirementType, bool negate, params object[] features)
{
if (features == null || features.Length == 0)
{
Expand All @@ -82,6 +126,8 @@ public FeatureGateAttribute(RequirementType requirementType, params object[] fea
Features = fs;

RequirementType = requirementType;

Negate = negate;
}

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

/// <summary>
/// Negates the evaluation for whether or not a feature gate should activate.
/// </summary>
public bool Negate { get; }

/// <summary>
/// Performs controller action pre-processing to ensure that any or all of the specified features are enabled.
/// </summary>
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,29 +97,41 @@ 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
testFeatureFilter.Callback = ctx => Task.FromResult(ctx.FeatureName == Features.ConditionalFeature);

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
testFeatureFilter.Callback = ctx => Task.FromResult(false);

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]
Expand Down Expand Up @@ -153,29 +165,41 @@ 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
testFeatureFilter.Callback = ctx => Task.FromResult(ctx.FeatureName == Features.ConditionalFeature);

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
testFeatureFilter.Callback = ctx => Task.FromResult(false);

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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@page
@model RazorTestAllNegateModel
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@page
@model Tests.FeatureManagement.AspNetCore.Pages.RazorTestAnyNegateModel
@{
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
18 changes: 17 additions & 1 deletion tests/Tests.FeatureManagement.AspNetCore/TestController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}

0 comments on commit 325271b

Please sign in to comment.