Feature Flags
Deploy new behaviour switched off, turn it on without a release, and delete the flag once the question is settled.
if (await featureManager.IsEnabledAsync("NewCheckout", cancellationToken)) { }A feature flag is a configuration value that decides whether a piece of behaviour runs. Both the old and the new code ship in the same build, and the flag chooses between them at run time. That separates deploying code from turning it on, which means a problem is fixed by flipping a value rather than by rolling back a release.
Where the flag is read
Read it as late as possible: inside the request handler, from a source that can change. A flag copied into a static field at startup is not a flag, because the value cannot change until the process restarts. IOptionsMonitor<T> works for a flag stored in configuration. The Microsoft.FeatureManagement package adds AddFeatureManagement() and IVariantFeatureManager, whose IsEnabledAsync(name, cancellationToken) returns ValueTask<bool>; it reads the feature_management section.
Two schemas
Current flags are a feature_flags array under feature_management, each entry with an id and enabled. Older code uses a FeatureManagement section whose properties are flag names. Both are still read; a flag declared in both takes its definition from feature_management.
In MVC, [FeatureGate("Name")] on a controller or action is shorthand for the same IsEnabledAsync check, returning 404 when the flag is off.
Removing a flag
A flag is a question, and a question stops being one. Once the new path has been on everywhere for a while, delete the check, the old branch and the configuration key. A flag left in place is a branch the tests still have to cover.
Calling curl http://localhost:5000/checkout against the example returns new checkout, or old checkout once enabled is set to false.
Example
// Requires: dotnet add package Microsoft.FeatureManagement.AspNetCore
using Microsoft.FeatureManagement;
var builder = WebApplication.CreateBuilder(args);
// Stands in for the feature_management section of appsettings.json.
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
["feature_management:feature_flags:0:id"] = "NewCheckout",
["feature_management:feature_flags:0:enabled"] = "true"
});
builder.Services.AddFeatureManagement();
var app = builder.Build();
// The check happens per request, so the answer can change without a restart.
app.MapGet("/checkout", async (
IVariantFeatureManager features,
CancellationToken cancellationToken) =>
{
if (await features.IsEnabledAsync("NewCheckout", cancellationToken))
{
return "new checkout";
}
return "old checkout";
});
app.Run();When to use it
- Deploy a rewritten checkout switched off, then enable it for 10 percent of traffic without another release.
- Turn off an expensive report endpoint during an incident by changing a configuration value.
- Remove a flag and its old code path once the new behaviour has been on in every environment for a month.
More examples
Declaring flags in configuration
A flag with enabled false is off. With enabled true and no conditions it is on. With conditions, the client_filters decide, and the built-in Microsoft.Percentage filter turns the feature on for that share of evaluations. A percentage filter re-rolls on every check, so one visitor can see the feature on one page and not the next; use the targeting filter instead when a user must get a consistent answer.
// appsettings.json
{
"feature_management": {
"feature_flags": [
{
"id": "NewCheckout",
"enabled": false
},
{
"id": "GiftWrapping",
"enabled": true,
"conditions": {
"client_filters": [
{
"name": "Microsoft.Percentage",
"parameters": {
"Value": 10
}
}
]
}
}
]
}
}A flag without the library
For a plain on-or-off switch this is the whole job, and it is worth preferring while that is all you need. Reach for the feature management library when you want percentage rollouts, per-user targeting or evaluation telemetry. Note IOptionsMonitor rather than IOptions: IOptions<T> is built once for the life of the process, so the flag would need a restart.
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
// reloadOnChange: true re-reads the file when an operator edits it.
builder.Configuration.AddJsonFile("flags.json", optional: true, reloadOnChange: true);
builder.Services.Configure<FeatureOptions>(builder.Configuration.GetSection("Features"));
var app = builder.Build();
// CurrentValue is rebuilt after a reload, so no restart is needed.
app.MapGet("/checkout", (IOptionsMonitor<FeatureOptions> flags) =>
flags.CurrentValue.NewCheckout ? "new checkout" : "old checkout");
app.Run();
public sealed class FeatureOptions
{
public bool NewCheckout { get; set; }
}Gating a controller action
Use FeatureGate when a whole endpoint is the feature, rather than a branch inside one. It is the same check the long form performs, so the two are interchangeable. Be deliberate about the 404: clients cannot tell a gated-off endpoint from a wrong URL. When a caller needs to know the difference, do the check in the handler and return your own response instead.
using Microsoft.AspNetCore.Mvc;
using Microsoft.FeatureManagement.Mvc;
namespace Shop.Controllers;
[ApiController]
[Route("checkout")]
public sealed class CheckoutController : ControllerBase
{
// Shorthand for the IsEnabledAsync check around the whole action.
// When NewCheckout is off, the action returns 404 Not Found.
[FeatureGate("NewCheckout")]
[HttpPost("v2")]
public IActionResult PostV2() => Ok("new checkout");
[HttpPost("v1")]
public IActionResult PostV1() => Ok("old checkout");
}
Discussion