Zum Hauptinhalt springen

Feature Flag in Csharp

This provides a way to toggle Features without re-compiling or even restarting the service.

Add the package

Install Microsoft.FeatureManagement.AspNetCore

The base min-api-template:

using Microsoft.FeatureManagement;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// inject our FeatureManager
builder.Services.AddFeatureManagement();

var app = builder.Build();
if (app.Environment.IsDevelopment()) {
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
var summaries = new[] {
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

// we pass the injected featureManager to the handler:
app.MapGet("/weatherforecast", async (IFeatureManager featureManager) =>
{
// first we check for the current state of our flag:
var isFlag10Weathers = await featureManager.IsEnabledAsync(FeatureFlags.TenWeathers);

// if isFlag we return 10 weathers, otherwise the default 5
var forecast = Enumerable.Range(1, isFlag10Weathers ? 10 : 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

app.Run();
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) {
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

// we could pass plain strings into .IsEnabledAsync("TenWeathers")
// - but cleaner to have them all in one Class:
public class FeatureFlags {
public const string TenWeathers = "Tenweathers";
}
  • appsettings.json

The default configuration provider is appsettings.json. We just add the current value for the flag:

{
"FeatureManagement": {
"TenWeathers": false
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

More Examples

We can toggle certain middleware with those 2 methods:

  • app.UseForFeature()
  • app.UseMiddleWareForFeature<>

Minimal Apis can also use toggleable EndpointFilters.


Example for MVC Controllers

// you can toggle Existence of whole Controllers
[FeatureGate("isThisControllerActive")]
[ApiController]
public class ExampleController: ControllerBase
{
// but also for single Actions
[FeatureGate("isThisActionActive")]
public IActionResult Action()
{
return Ok();
}
}

Endpoint Filter Example

  • FeatureFlagEndpointFilters.cs
using Microsoft.FeatureManagement;

namespace minApiFeatureFlags;

// Extension to make registration of Filters easier:
public static class FeatureFlagEndpointFilterExtensions
{
public static TBuilder WithFeatureFlag<TBuilder>(
this TBuilder builder, string endpointName)
where TBuilder : IEndpointConventionBuilder
{
builder.AddEndpointFilter(new FeatureFlagEndpointFilters(endpointName));
return builder;
}
}

// Container for all our EndpointFilters:
public class FeatureFlagEndpointFilters : IEndpointFilter
{
// to differentiate between endpoint-filters we pass down a identifier string:
private readonly string _endpointName;

public FeatureFlagEndpointFilters(string endpointName)
{
_endpointName = endpointName;
}

// if Flags are set we will pass context down the Pipeline
// otherwise we return a generic 404
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{

var featureManager = context.HttpContext
.RequestServices.GetRequiredService<IFeatureManager>();

var isEnabled = await featureManager
.IsEnabledAsync($"Endpoints_{_endpointName}");

if (!isEnabled)
{
return Results.NotFound();
}

return await next(context);
}
}
  • we create a new endpoint we want to hide with our filter implementation:
app.MapGet("/weatherforecastslim", (IFeatureManager featureManager) =>
{
return Enumerable.Range(1, 5).Select(index =>
new WeatherForecastSlim
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55)
))
.ToArray();
})
.WithFeatureFlag("GetWeatherForecastSlim")
.WithName("GetWeatherForecastSlim")
.WithOpenApi();

internal record WeatherForecastSlim(DateOnly Date, int TemperatureC) {
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
  • and now we can toggle the flag in our appsettings:
  "FeatureManagement": {
"Endpoints_GetWeatherForecastSlim": false
},

More Advanced Filtering techniques

Canary with a Percantagefilter

Canary ish functionality. Immagine we want to test a feature on only a percantage of requests. While monitoring closely if we break something (or if performance improves etc.)

builder.Services.AddFeatureManagement()
.AddFeatureFilter<Percantagefilter>();
// .AddSessionManager<> // for more features
  • now we could specify to only enable the new slim variant for 50% of all requests:
  "FeatureManagement": {
"Endpoints_GetWeatherForecastSlim": {
"EnabledFor": [
{
"Name": "Percantage",
"Parameters":{
"Value": 50
}
}
]
}
},

TargetingFilter

  • example only target non subscription users. Or Only users that are in the platform for over 2 years. To access those features.
builder.Services.AddFeatureManagement().AddFeatureFilter<TargetingFilter>();

TimeWindowFilter

  • Feature is only available for a limited time period.
builder.Services.AddFeatureManagement().AddFeatureFilter<TimeWindowFilter>();