Skip to content

Commit

Permalink
update documentation for endpoint feature flags support and add tests…
Browse files Browse the repository at this point in the history
… for FeatureFlagsEndpointFilterExtensions
  • Loading branch information
crodriguesbr committed Dec 30, 2024
1 parent 3339e7a commit a25a92f
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,33 @@
namespace Microsoft.FeatureManagement.AspNetCore;

/// <summary>
/// Extension methods that provide feature management integration for ASP.NET Core application building.
/// Extension methods that provide feature management integration for ASP.NET Core endpoint building.
/// </summary>
public static class FeatureFlagsEndpointFilterExtensions
{
/// <summary>
/// Adds a feature flag filter to the endpoint.
/// Adds a feature flag filter to the endpoint that controls access based on feature state.
/// </summary>
/// <param name="builder"></param>
/// <param name="featureName"></param>
/// <param name="predicate"></param>
/// <typeparam name="TBuilder"></typeparam>
/// <returns></returns>
/// <param name="builder">The endpoint convention builder.</param>
/// <param name="featureName">The name of the feature flag to evaluate.</param>
/// <param name="predicate">A function that provides the targeting context for feature evaluation.</param>
/// <typeparam name="TBuilder">The type of the endpoint convention builder.</typeparam>
/// <returns>The endpoint convention builder for chaining.</returns>
/// <remarks>
/// This extension method enables feature flag control over endpoint access. When the feature is disabled,
/// requests to the endpoint will return a 404 Not Found response. The targeting context from the predicate
/// is used to evaluate the feature state for the current request.
/// </remarks>
/// <example>
/// <code>
/// endpoints.MapGet("/api/feature", () => "Feature Enabled")
/// .WithFeatureFlag("MyFeature", () => new TargetingContext
/// {
/// UserId = "user123",
/// Groups = new[] { "beta-testers" }
/// });
/// </code>
/// </example>
public static TBuilder WithFeatureFlag<TBuilder>(this TBuilder builder,
string featureName,
Func<TargetingContext> predicate) where TBuilder : IEndpointConventionBuilder
Expand All @@ -39,20 +54,29 @@ public class FeatureFlagsEndpointFilter : IEndpointFilter
/// <summary>
/// Creates a new instance of <see cref="FeatureFlagsEndpointFilter"/>.
/// </summary>
/// <param name="featureName"></param>
/// <param name="predicate"></param>
/// <param name="featureName">The name of the feature flag to evaluate for this endpoint.</param>
/// <param name="predicate">A function that provides the targeting context for feature evaluation.</param>
/// <exception cref="ArgumentNullException">Thrown when featureName or predicate is null.</exception>
public FeatureFlagsEndpointFilter(string featureName, Func<TargetingContext> predicate)
{
_featureName = featureName;
_predicate = predicate;
}

/// <summary>
/// Invokes the feature flag filter.
/// Invokes the feature flag filter to control endpoint access based on feature state.
/// </summary>
/// <param name="context"></param>
/// <param name="next"></param>
/// <returns></returns>
/// <param name="context">The endpoint filter invocation context containing the current HTTP context.</param>
/// <param name="next">The delegate representing the next filter in the pipeline.</param>
/// <returns>
/// A NotFound result if the feature is disabled, otherwise continues the pipeline by calling the next delegate.
/// Returns a ValueTask containing the result object.
/// </returns>
/// <remarks>
/// The filter retrieves the IFeatureManager from request services and evaluates the feature flag.
/// If the feature manager is not available, the filter allows the request to proceed.
/// For disabled features, returns a 404 Not Found response instead of executing the endpoint.
/// </remarks>
public async ValueTask<object> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
Expand Down
111 changes: 111 additions & 0 deletions tests/Tests.FeatureManagement.AspNetCore/FeatureFlagsEndpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#if NET7_0_OR_GREATER
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.FeatureManagement;
using Microsoft.FeatureManagement.AspNetCore;
using Microsoft.FeatureManagement.FeatureFilters;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;

namespace Tests.FeatureManagement.AspNetCore
{
public class TestTargetingContextAccessor : ITargetingContextAccessor
{
public ValueTask<TargetingContext> GetContextAsync()
{
return new ValueTask<TargetingContext>(new TargetingContext
{
UserId = "testUser",
Groups = new[] { "testGroup" }
});
}
}

public class FeatureTestServer : IDisposable
{
private readonly IHost _host;
private readonly HttpClient _client;
private readonly bool _featureEnabled;

public FeatureTestServer(bool featureEnabled = true)
{
_featureEnabled = featureEnabled;
_host = CreateHostBuilder().Build();
_host.Start();
_client = _host.GetTestServer().CreateClient();
}

private IHostBuilder CreateHostBuilder()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
["FeatureManagement:TestFeature"] = _featureEnabled.ToString().ToLower()
})
.Build();

return Host.CreateDefaultBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddSingleton<IConfiguration>(configuration);
services.AddSingleton<ITargetingContextAccessor, TestTargetingContextAccessor>();
services.AddFeatureManagement();
services.AddRouting();
})
.Configure(app =>
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/test", new Func<string>(() => "Feature Enabled"))
.WithFeatureFlag("TestFeature", () =>
new TargetingContext
{
UserId = "testUser",
Groups = new[] { "testGroup" }
});
});
});
});
}

public HttpClient Client => _client;

public void Dispose()
{
_host?.Dispose();
_client?.Dispose();
}
}

public class FeatureFlagsEndpointFilterTests
{
[Fact]
public async Task WhenFeatureEnabled_ReturnsSuccess()
{
using var server = new FeatureTestServer(featureEnabled: true);
var response = await server.Client.GetAsync("/test");
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
}

[Fact]
public async Task WhenFeatureDisabled_ReturnsNotFound()
{
using var server = new FeatureTestServer(featureEnabled: false);
var response = await server.Client.GetAsync("/test");
Assert.Equal(System.Net.HttpStatusCode.NotFound, response.StatusCode);
}
}
}
#endif

0 comments on commit a25a92f

Please sign in to comment.