---
title: "Feature Flag Management in .NET Core: Complete Guide"  
description: "Learn feature flag management in .NET Core with Microsoft.FeatureManagement. Covers filters, targeting, live refresh, testing and best practices, with code"  
author: "ICSM Computer"  
published: 2026-11-11  
updated: 2026-11-11  
canonical: https://answers.mindstick.com/blog/1539/feature-flag-management-in-dot-net-core-complete-guide  
category: ".NET Framework"  
tags: [".NET", ".NET Core", "features"]  
reading_time: 14 minutes  

---

# Feature Flag Management in .NET Core: Complete Guide

Imagine it's Friday evening and your team has just finished a big new checkout flow. It's merged and tested, but you aren't sure it's ready for every customer. Do you hold the release? Do you deploy and hope?

Feature flags (also called feature toggles) are the answer. They let you **ship code without exposing it**, then switch it on for the right people at the right time, with no redeploy. This guide covers what feature flags are, the different types, how to implement them in ASP.NET Core with `Microsoft.FeatureManagement`, and how to avoid the problems they can cause.

![Feature Flag Management in .NET Core: Complete Guide](https://answers.mindstick.com/blogs/7faa4ed2-9333-40d4-bce6-00e465dddb38/images/75e5e22c-f6b8-42b6-90ac-9ccf9ed7cb89.png)

## 1. What is a feature flag?

A feature flag is a runtime condition that decides whether a piece of functionality is active:

```cs
if (await featureManager.IsEnabledAsync("NewCheckout"))
{
    // new behaviour
}
else
{
    // old behaviour
}
```

The flag's value lives outside the compiled code, in configuration, a database, or a cloud service. Changing the value changes the behaviour without a new build.

## 2. Why use feature flags?

| Benefit | What it means |
| --- | --- |
| **Decouple deployment from release** | Code reaches production on Tuesday. The feature is released to users on Thursday, when the business is ready. |
| **Trunk-based development** | Unfinished work is merged to `main` behind a flag. You avoid long-lived branches and painful merges. |
| **Progressive rollout** | Release to 1% of users, then 10%, then 100%, watching errors and metrics at each step. |
| **Kill switch** | If a feature misbehaves, switch it off in seconds instead of rolling back a deployment. |
| **A/B testing** | Show variant A to half the users and variant B to the rest, then compare results. |
| **Targeted access** | Beta testers, internal staff, a single tenant or a single country can see a feature first. |
| **Safer testing in production** | Verify a feature with real data for a small group before everyone gets it. |

## 3. Types of feature flags

Treating every flag the same is the first mistake teams make. Flags differ in purpose and lifetime:

1. **Release flags** hide incomplete or new features. They are short-lived, so delete them after full rollout.
2. **Ops (kill-switch) flags** let operations disable a heavy or risky component, such as a third-party call or an expensive report. They may live for a long time.
3. **Experiment flags** drive A/B or multivariate tests. They last until the experiment ends.
4. **Permission/entitlement flags** gate features by plan, role or tenant (for example, "Premium only"). They are often permanent.

Knowing the type tells you when to remove the flag.

## 4. Options in .NET

- **Plain** `IConfiguration` **/** `IOptions`: a `bool` in `appsettings.json`. It's quick for a prototype but has no filters, no percentages and no cleanup discipline.
- `Microsoft.FeatureManagement`: Microsoft's official library. It has filters, targeting, ASP.NET Core integration and variants. This guide focuses on it.
- **Azure App Configuration**: a managed store with a UI. It plugs into Microsoft.FeatureManagement and supports live refresh.
- **Third-party platforms** (LaunchDarkly, Unleash, Flagsmith, and others): they add analytics, audit logs and UIs.
- **OpenFeature**: a vendor-neutral standard with a .NET SDK. You can swap providers without changing application code.
- **Custom provider**: you build your own store, such as a database table with an admin screen. See section 15.

## 5. Getting started

Install the packages:

```plaintext
dotnet add package Microsoft.FeatureManagement.AspNetCore
```

Add flags to `appsettings.json`:

```plaintext
{
  "FeatureManagement": {
    "NewCheckout": true,
    "DarkMode": false
  }
}
```

Register the service in `Program.cs`:

```cs
using Microsoft.FeatureManagement;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddFeatureManagement();   // reads the "FeatureManagement" section

var app = builder.Build();
```

`AddFeatureManagement()` reads the `FeatureManagement` section by default. You can pass a different `IConfiguration` section if needed.

## 6. Avoid magic strings

Typos in flag names fail silently, because the flag is simply treated as disabled. Put names in one place:

```cs
public static class FeatureFlags
{
    public const string NewCheckout = "NewCheckout";
    public const string DarkMode    = "DarkMode";
}
```

Enums also work with `[FeatureGate]`. Pick one convention and stick to it.

## 7. Checking a flag in code

Inject `IFeatureManager`:

```cs
public class CheckoutService
{
    private readonly IFeatureManager _features;
    public CheckoutService(IFeatureManager features) => _features = features;

    public async Task<Order> PlaceOrderAsync(Cart cart)
    {
        if (await _features.IsEnabledAsync(FeatureFlags.NewCheckout))
            return await NewFlowAsync(cart);

        return await LegacyFlowAsync(cart);
    }
}
```

Evaluation is asynchronous because filters may call external systems, such as a database or a remote service.

`IFeatureManagerSnapshot` is the request-scoped version. It caches each flag's result for the lifetime of the request. This matters when a flag could change mid-request, for example during a live config refresh. Without a snapshot, the top of your request could take the "new" path and the bottom the "old" one. Inject `IFeatureManagerSnapshot` in web request code to keep behaviour consistent.

## 8. Protecting controllers and actions with `[FeatureGate]`

```cs
[FeatureGate(FeatureFlags.NewCheckout)]
public class CheckoutController : Controller
{
    public IActionResult Index() => View();
}
```

If the flag is off, the framework returns **404 Not Found** by default, so the endpoint appears not to exist. You can gate individual actions, or require several flags:

```cs
[FeatureGate(RequirementType.All, FeatureFlags.NewCheckout, FeatureFlags.DarkMode)]
```

`RequirementType.Any` (the default) needs at least one flag enabled. `All` needs every flag enabled.

### Custom response when a feature is off

Implement `IDisabledFeaturesHandler` if you want something other than a 404:

```cs
public class RedirectDisabledHandler : IDisabledFeaturesHandler
{
    public Task HandleDisabledFeatures(IEnumerable<string> features, ActionExecutingContext context)
    {
        context.Result = new RedirectToActionResult("ComingSoon", "Home", null);
        return Task.CompletedTask;
    }
}

builder.Services.AddFeatureManagement()
    .UseDisabledFeaturesHandler(new RedirectDisabledHandler());
```

## 9. Hiding UI in Razor views

Register the tag helper in `_ViewImports.cshtml`:

```cs
@addTagHelper *, Microsoft.FeatureManagement.AspNetCore
```

Then wrap markup:

```cs
<feature name="NewCheckout">
    <a asp-controller="Checkout" asp-action="Index">Try the new checkout</a>
</feature>

<feature name="NewCheckout" negate="true">
    <a asp-controller="Cart" asp-action="Legacy">Checkout</a>
</feature>
```

`negate="true"` shows the content when the flag is **off**. You can list several names with `requirement="All"` or `"Any"`.

## 10. Minimal APIs

Minimal APIs have no `[FeatureGate]`, so inject the manager and check it directly:

```cs
app.MapGet("/api/reports/v2", async (IFeatureManager fm) =>
    await fm.IsEnabledAsync(FeatureFlags.NewReports)
        ? Results.Ok(new { version = 2 })
        : Results.NotFound());
```

For reuse across many endpoints, write an `IEndpointFilter` that takes a flag name and returns `Results.NotFound()` when it's off.

## 11. Conditional middleware

Run a middleware only when a flag is on:

```cs
app.UseMiddlewareForFeature<ResponseCompressionExperimentMiddleware>("CompressionExperiment");
```

This works as a branch in the pipeline. Requests pass through the middleware only while the feature is enabled.

## 12. Feature filters: flags with conditions

So far the flags are plain on/off. Real rollouts need conditions, and that's what filters provide. A flag lists its filters in `EnabledFor`:

```plaintext
"FeatureManagement": {
  "NewCheckout": {
    "EnabledFor": [
      { "Name": "Percentage", "Parameters": { "Value": 25 } }
    ]
  }
}
```

If any filter returns true, the feature is on. Set `"RequirementType": "All"` on the flag to require every filter to pass instead.

Register built-in filters that need explicit registration:

```cs
builder.Services.AddFeatureManagement()
    .AddFeatureFilter<PercentageFilter>()
    .AddFeatureFilter<TimeWindowFilter>();
```

### 12.1 Percentage filter

Enables the feature for a random percentage of evaluations. It works well for quick canary-style tests. It re-rolls on every evaluation, so the same user may see different behaviour between requests. For per-user stickiness, use targeting.

### 12.2 Time window filter

Enables the feature between two dates, for example a sale or a planned launch:

```plaintext
{
  "Name": "TimeWindow",
  "Parameters": {
    "Start": "Mon, 01 Dec 2026 00:00:00 GMT",
    "End": "Mon, 08 Dec 2026 00:00:00 GMT"
  }
}
```

Use the RFC 1123 date format shown, and remember times are UTC.

### 12.3 Targeting filter

This is the most useful filter for gradual rollouts. It enables a feature for specific users, groups and percentages, and the same user always gets the same result:

```plaintext
{
  "Name": "Targeting",
  "Parameters": {
    "Audience": {
      "Users": [
        "alice@example.com"
      ],
      "Groups": [
        {
          "Name": "Beta",
          "RolloutPercentage": 100
        },
        {
          "Name": "Staff",
          "RolloutPercentage": 50
        }
      ],
      "DefaultRolloutPercentage": 10,
      "Exclusion": {
        "Users": [
          "blocked@example.com"
        ]
      }
    }
  }
}
```

It needs to know who the current user is. Provide a targeting context accessor:

```cs
public class HttpTargetingContextAccessor : ITargetingContextAccessor
{
    private readonly IHttpContextAccessor _http;
    public HttpTargetingContextAccessor(IHttpContextAccessor http) => _http = http;

    public ValueTask<TargetingContext> GetContextAsync()
    {
        var user = _http.HttpContext!.User;
        return new ValueTask<TargetingContext>(new TargetingContext
        {
            UserId = user.Identity?.Name,
            Groups = user.FindAll(ClaimTypes.Role).Select(c => c.Value)
        });
    }
}

builder.Services.AddHttpContextAccessor();
builder.Services.AddFeatureManagement()
    .WithTargeting<HttpTargetingContextAccessor>();
```

The order of evaluation is exclusion, then listed users, then groups, then the default percentage. Rollout is hash-based, so a user who is inside the 10% stays inside it when you raise the value to 20%. Nobody flips back and forth.

## 13. Writing your own feature filter

When built-in filters don't fit, such as "only for tenant X" or "only in India", write a custom filter:

```cs
[FilterAlias("Country")]
public class CountryFilter : IFeatureFilter
{
    private readonly IHttpContextAccessor _http;
    public CountryFilter(IHttpContextAccessor http) => _http = http;

    public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
    {
        var settings = context.Parameters.Get<CountrySettings>() ?? new();
        var country = _http.HttpContext?.Request.Headers["X-Country"].ToString();
        return Task.FromResult(settings.Allowed.Contains(country));
    }
}

public class CountrySettings { public string[] Allowed { get; set; } = Array.Empty<string>(); }
```

Register it with `.AddFeatureFilter<CountryFilter>()`, then use it in configuration:

```plaintext
{
  "Name": "Country",
  "Parameters": {
    "Allowed": [
      "IN",
      "US"
    ]
  }
}
```

**Contextual filters** (`IContextualFeatureFilter<TContext>`) take the data to evaluate as a parameter instead of reading ambient state. You call `IsEnabledAsync("Feature", myContext)`. They are easier to unit test and don't depend on `HttpContext`.

## 14. Changing flags without redeploying (live refresh)

A file in `appsettings.json` still needs a redeploy or restart to change. To change flags live, use a dynamic store such as Azure App Configuration:

```cs
builder.Configuration.AddAzureAppConfiguration(options =>
{
    options.Connect(builder.Configuration["AppConfig:ConnectionString"])
           .UseFeatureFlags(ff => ff.SetRefreshInterval(TimeSpan.FromSeconds(30)));
});

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

var app = builder.Build();
app.UseAzureAppConfiguration();   // triggers the refresh check
```

The default refresh interval is 30 seconds. Flag lookups happen in the background, so request latency isn't affected. Pair this with `IFeatureManagerSnapshot` for per-request consistency.

You don't need Azure for this. Any configuration source that reloads, such as a database provider, works because the feature manager reads from `IConfiguration`.

## 15. Building a database-backed flag store

If you have an admin panel, you can manage flags from a UI. The extension point is `IFeatureDefinitionProvider`:

```cs
public class DbFeatureDefinitionProvider : IFeatureDefinitionProvider
{
    public Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName) { /* load from DB (cache it) */ }
    public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync() { /* yield all */ }
}

builder.Services.AddSingleton<IFeatureDefinitionProvider, DbFeatureDefinitionProvider>();
builder.Services.AddFeatureManagement();
```

Keep these points in mind:

- **Cache** definitions (for example for 30 seconds). A database hit for every flag check will hurt performance.
- **Audit** every change: who, what, when.
- **Restrict** who can flip flags in production, using role permissions.
- Always give the code a safe default, so a failure to read the store results in "off".

## 16. Variants and experiments

Newer versions (4.x) add **variants**. A flag can return one of several named values, not just on/off. For example, `"Button": "Blue" | "Green"` with a percentage split. You then read the assigned variant through `IVariantFeatureManager` and use its configuration value. This is how you run multi-way experiments with the same targeting rules. Check the package docs for your installed version, because the configuration schema (`feature_management` with `feature_flags`) differs from the older `FeatureManagement` section.

## 17. Telemetry: measure what your flags do

A rollout without measurement is just a guess. Recent versions of the library can publish an event each time a flag is evaluated, including its variant. You can send these to Application Insights or any tracing system. Tag your logs and metrics with the flag state so you can answer questions like "Did errors rise for users who got NewCheckout?"

Even without the built-in support, log flag evaluations for important features.

## 18. Testing code with flags

Test **both** paths, enabled and disabled.

Unit test by mocking `IFeatureManager`:

```cs
var fm = new Mock<IFeatureManager>();
fm.Setup(f => f.IsEnabledAsync(FeatureFlags.NewCheckout)).ReturnsAsync(true);
var sut = new CheckoutService(fm.Object);
```

Integration test by overriding configuration in `WebApplicationFactory`:

```cs
builder.ConfigureAppConfiguration((_, cfg) =>
    cfg.AddInMemoryCollection(new Dictionary<string, string?>
    {
        ["FeatureManagement:NewCheckout"] = "true"
    }));
```

## 19. Best practices

1. **Default to off.** A new flag should be disabled everywhere until you deliberately turn it on. A missing flag must also mean "off".
2. **Give each flag an owner and a removal date.** Flags without an owner are never cleaned up.
3. **Delete release flags quickly.** Once a feature is at 100% and stable, remove the flag and the old code path. Stale flags are technical debt.
4. **Keep flag checks at the edges.** Decide once at a controller, service or factory, and call different implementations. Scattering `if (flag)` through business logic makes code unreadable. Use the strategy pattern or dependency injection to swap implementations.
5. **Avoid nested and interdependent flags.** Flag A requires B, which requires C, produces combinations nobody has tested.
6. **Use clear names.** Prefer `NewCheckout` over `Flag12` or `TestFeature`. Name flags after the capability, not the state ("UseNewCheckout", not "OldCheckoutOff").
7. **Don't use flags for security.** A flag is not authorization. Hiding a button doesn't protect the endpoint behind it. Keep real permission checks in place.
8. **Handle database changes safely.** Use the expand/contract pattern. Add new columns first, write to both, then switch the flag, then drop the old column in a later release. A flag can switch code instantly but cannot undo a destructive migration.
9. **Make it observable.** Log flag state with errors and track who changes flags and when.
10. **Control who can change flags.** Production flag changes are production changes. Use permissions, approvals and an audit trail.
11. **Test in both states and in combination** for flags that interact.
12. **Don't evaluate repeatedly in hot loops.** Evaluate once per request or operation, and reuse the result.

## 20. Common pitfalls

- **Flag debt**: hundreds of old flags nobody dares to remove.
- **Typos**: a misspelled name means the flag is silently off. Use constants.
- **Inconsistent behaviour within a request** when config refreshes mid-request. Use `IFeatureManagerSnapshot`.
- **Percentage filter and "sticky" expectations**: if users must keep the same experience, use targeting.
- **Forgetting UTC** in time-window filters.
- **Different environments, different values**: staging passes with a flag on, production runs with it off. Keep per-environment config explicit and visible.
- **Flag as permanent config**: if a "flag" is permanent behaviour, such as a plan entitlement, treat it as a proper setting or permission instead.

## 21. Flag lifecycle at a glance

1. **Create**: the flag is off by default, with an owner and a purpose.
2. **Develop**: merge code behind the flag.
3. **Test**: turn it on in dev and staging.
4. **Roll out**: internal users, then 1%, 10%, 50%, 100%, while monitoring.
5. **Stabilize**: confirm metrics are healthy.
6. **Retire**: remove the flag, the old code path and the configuration entry.

## Conclusion

Feature flags turn releasing from a risky event into a routine, controllable action. With `Microsoft.FeatureManagement` in ASP.NET Core you get a good set of tools. Boolean flags handle simple cases, `[FeatureGate]` and the `<feature>` tag helper protect actions and UI, and filters cover time windows, percentages and per-user targeting. Custom filters and providers handle the rest, and live refresh lets you change behaviour without redeploying.

Flags are only useful if you manage them well. Name them clearly, default them to off, assign owners, measure their effect, and delete them once they've done their job. Start with one flag on your next risky feature and add targeting once you're comfortable.

I wrote this inline rather than saving it to a file. If you want it as a file or a doc, I can do that. I can also trim it to a shorter post, or add a worked example based on your admin panel's flags, such as a database-backed flag store with an admin screen.

---

Original Source: https://answers.mindstick.com/blog/1539/feature-flag-management-in-dot-net-core-complete-guide

Copyright © MindStick Software Pvt. Ltd. This Markdown version is provided for developers, AI systems, and offline reading.
