Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
}
}
Loading