The riskiest moment in most deployments isn't the deploy itself — it's the release. Feature flags separate the two: you can ship code to production dark, turn it on for 1% of traffic, watch it, and turn it back off in seconds if something's wrong, without touching the deployment pipeline at all.

Key takeaways
  • Deploying and releasing are different events. Flags let you decouple them so a bad release is a config change, not a rollback.
  • Microsoft.FeatureManagement gives you flag evaluation, targeting, and percentage rollout without adopting a full third-party platform.
  • Back flags with Azure App Configuration for dynamic, no-redeploy toggling in production.
  • Every flag is technical debt the moment it's fully rolled out. Track flag age and delete the ones that have outlived their purpose.

Setup

bash
dotnet add package Microsoft.FeatureManagement.AspNetCore
json
{
  "FeatureManagement": {
    "NewCheckoutFlow": true,
    "BulkExportApi": false
  }
}
csharp
builder.Services.AddFeatureManagement();

Gating an endpoint or a service

The [FeatureGate] attribute works on MVC controllers and actions. For Minimal APIs or arbitrary business logic, inject IFeatureManager directly and check it explicitly — which also makes intent obvious in a code review.

csharp
app.MapPost("/api/checkout", async (
    CheckoutRequest req, IFeatureManager features, ICheckoutService svc, CancellationToken ct) =>
{
    var result = await features.IsEnabledAsync("NewCheckoutFlow")
        ? await svc.CheckoutV2Async(req, ct)
        : await svc.CheckoutV1Async(req, ct);

    return Results.Ok(result);
});

Gradual rollout with targeting filters

A flag that's simply on or off doesn't let you roll out gradually. The targeting and percentage filters that ship with Microsoft.FeatureManagement let you roll a feature out to a percentage of users, or to specific groups, before flipping it on for everyone.

json
{
  "FeatureManagement": {
    "NewCheckoutFlow": {
      "EnabledFor": [
        {
          "Name": "Microsoft.Targeting",
          "Parameters": {
            "Audience": {
              "DefaultRolloutPercentage": 10,
              "Groups": [ { "Name": "BetaTesters", "RolloutPercentage": 100 } ]
            }
          }
        }
      ]
    }
  }
}
csharp
builder.Services.AddFeatureManagement()
    .AddFeatureFilter();

builder.Services.AddScoped();

Dynamic flags without a redeploy

Flags in appsettings.json still need a deploy to change. Backing them with Azure App Configuration lets you flip a flag from a portal or CLI and have running instances pick it up on the next refresh cycle — no redeploy, no restart.

csharp
builder.Configuration.AddAzureAppConfiguration(options =>
{
    options.Connect(builder.Configuration.GetConnectionString("AppConfig"))
        .UseFeatureFlags(ff => ff.PollInterval = TimeSpan.FromSeconds(30));
});

builder.Services.AddAzureAppConfiguration();
builder.Services.AddFeatureManagement();

// after building the app
app.UseAzureAppConfiguration();
HousekeepingA flag at 100% rollout for a month is a flag that should be deleted, with its old code path removed along with it. We track flag creation dates and review anything older than one release cycle — flags left in place indefinitely are how codebases end up with permanent, untested branches.

Need help with this in production?

astrivo helps teams design, build, and modernize systems like this. Let's talk about your context.