Learnixo
Back to blog
Backend Systemsintermediate

Feature Flags in .NET: Safe Deployments and Gradual Rollouts

Implement feature flags in ASP.NET Core. Covers Microsoft.FeatureManagement, Azure App Configuration, percentage rollouts, user targeting, A/B testing, kill switches, and feature flag best practices.

LearnixoJune 4, 20265 min read
.NETC#Feature FlagsAzure App ConfigurationDeploymentA/B Testing
Share:𝕏

Why Feature Flags?

Feature flags decouple deployment from release. You deploy code with a flag off — it's invisible in production. You turn it on when you're ready, for whoever you choose.

Use cases:

  • Kill switch — turn off a broken feature without deploying
  • Gradual rollout — roll out to 5% of users, watch metrics, expand
  • A/B testing — different users get different experiences
  • Beta programs — enable features for specific users or groups
  • Dark launch — run new code paths in production without exposing them

Setup (Microsoft.FeatureManagement)

Bash
dotnet add package Microsoft.FeatureManagement.AspNetCore
dotnet add package Microsoft.Azure.AppConfiguration.AspNetCore  # for Azure App Config
C#
// Program.cs
builder.Services.AddFeatureManagement();

// Or with Azure App Configuration (recommended for production)
builder.Configuration.AddAzureAppConfiguration(options =>
{
    options.Connect(builder.Configuration["AzureAppConfig:ConnectionString"])
           .UseFeatureFlags(ff => ff.CacheExpirationInterval = TimeSpan.FromMinutes(5));
});

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

app.UseAzureAppConfiguration(); // polls for changes

Defining Flags

JSON
// appsettings.json — local development
{
  "FeatureManagement": {
    "NewCheckoutFlow": true,
    "BetaDashboard": false,
    "NewPricingEngine": {
      "EnabledFor": [
        {
          "Name": "Percentage",
          "Parameters": { "Value": 20 }
        }
      ]
    }
  }
}

Using Flags in Code

C#
// Option 1: IFeatureManager
public class CheckoutService
{
    private readonly IFeatureManager _features;

    public CheckoutService(IFeatureManager features) => _features = features;

    public async Task<CheckoutResult> CheckoutAsync(Cart cart, CancellationToken ct)
    {
        if (await _features.IsEnabledAsync("NewCheckoutFlow"))
            return await NewCheckoutAsync(cart, ct);

        return await LegacyCheckoutAsync(cart, ct);
    }
}

// Option 2: [FeatureGate] on controller actions
[FeatureGate("BetaDashboard")]
[HttpGet("dashboard/beta")]
public async Task<IActionResult> BetaDashboard()
{
    return Ok(await _dashboardService.GetBetaDataAsync());
}
// Returns 404 when flag is off (configurable — can redirect instead)

// Option 3: Razor/Blazor tag helper
// <feature name="NewCheckoutFlow">
//     <NewCheckout />
// </feature>
// <feature name="NewCheckoutFlow" negate="true">
//     <LegacyCheckout />
// </feature>

Targeting: Per-User and Per-Group

C#
// Register targeting context (who is the current user?)
builder.Services.AddFeatureManagement()
    .WithTargeting<UserTargetingContextAccessor>();

public class UserTargetingContextAccessor : ITargetingContextAccessor
{
    private readonly IHttpContextAccessor _http;

    public UserTargetingContextAccessor(IHttpContextAccessor http) => _http = http;

    public ValueTask<TargetingContext> GetContextAsync()
    {
        var user = _http.HttpContext?.User;
        return ValueTask.FromResult(new TargetingContext
        {
            UserId = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "anonymous",
            Groups = user?.FindAll("role").Select(c => c.Value).ToList() ?? new()
        });
    }
}
JSON
// Azure App Configuration feature flag with targeting
{
  "id": "BetaDashboard",
  "enabled": true,
  "conditions": {
    "client_filters": [
      {
        "name": "Microsoft.Targeting",
        "parameters": {
          "Audience": {
            "Users": ["alice@example.com", "bob@example.com"],
            "Groups": [
              { "Name": "beta-testers", "RolloutPercentage": 100 },
              { "Name": "internal",     "RolloutPercentage": 50 }
            ],
            "DefaultRolloutPercentage": 5
          }
        }
      }
    ]
  }
}

Time Windows

JSON
// Enable only during business hours
{
  "id": "LiveSupport",
  "conditions": {
    "client_filters": [
      {
        "name": "Microsoft.TimeWindow",
        "parameters": {
          "Start": "Mon, 09:00:00 +00:00",
          "End":   "Mon, 18:00:00 +00:00"
        }
      }
    ]
  }
}

Custom Filters

C#
// Feature active only for premium customers
[FilterAlias("PremiumCustomer")]
public class PremiumCustomerFilter : IFeatureFilter
{
    private readonly ICurrentUserService _user;

    public PremiumCustomerFilter(ICurrentUserService user) => _user = user;

    public async Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
    {
        var currentUser = await _user.GetCurrentUserAsync();
        return currentUser?.SubscriptionTier == "Premium";
    }
}

// Register
builder.Services.AddFeatureManagement()
    .AddFeatureFilter<PremiumCustomerFilter>();
JSON
{
  "id": "AdvancedAnalytics",
  "conditions": {
    "client_filters": [{ "name": "PremiumCustomer" }]
  }
}

Feature Flag Lifecycle

Good flags have a lifecycle — they should be cleaned up:

1. Add flag (off by default) → deploy
2. Enable for internal team → test
3. Gradual rollout: 5% → 25% → 50% → 100%
4. Monitor metrics at each stage
5. Kill switch if issues found
6. After stable at 100%: remove flag + dead code path

Anti-patterns:

  • Flags that are never cleaned up (technical debt)
  • Flags guarding multiple unrelated features
  • Flags checked in database queries (performance impact)
  • Testing only the "flag on" path

Azure App Configuration Setup

Bash
# Create Azure App Configuration store
az appconfig create \
  --name orderflow-config \
  --resource-group orderflow-rg \
  --sku Free

# Add a feature flag
az appconfig feature set \
  --name orderflow-config \
  --feature NewCheckoutFlow \
  --yes

# Enable it
az appconfig feature enable \
  --name orderflow-config \
  --feature NewCheckoutFlow

Grant managed identity access:

Bash
az role assignment create \
  --role "App Configuration Data Reader" \
  --assignee <managed-identity-principal-id> \
  --scope /subscriptions/.../providers/Microsoft.AppConfiguration/configurationStores/orderflow-config

Interview Questions

Q: What is the difference between a feature flag and a configuration value? A configuration value changes behaviour permanently (connection string, timeout). A feature flag is a boolean gate controlling whether a code path runs — designed to be changed frequently without deployment and eventually removed. Feature flags enable deployment without release.

Q: How would you implement a gradual rollout safely? Use percentage rollout (Microsoft.Targeting or custom filter). Start at 5%, monitor error rate, latency, and business metrics. If stable, increase to 25%, 50%, 100%. Automate the monitoring — if error rate spikes above threshold, the flag is automatically disabled (circuit-breaking the rollout).

Q: What is a kill switch and when would you use it? A feature flag set to "off by default" for a risky new feature. If the feature causes issues in production, you flip the flag off — instant rollback without a deployment. Useful for features that are hard to roll back via deployment (database changes, API contracts).

Q: Why should you clean up feature flags? Dead flags accumulate over time — two code paths to maintain, confusion about which is "real", and test coverage required for both. Flags guarding fully-released features add cognitive overhead and performance overhead if evaluated on hot paths. Set a policy: flags older than 90 days at 100% rollout must be removed.

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.