Skip to main content
SEMastery
Testingintermediate

Feature Flags in .NET and How I Use Them for A/B Testing

A friendly .NET 10 guide to feature flags with Microsoft.FeatureManagement: on/off flags, percentage and targeting filters, and variants for A/B testing.

12 min readUpdated January 30, 2026

The light switch and the dimmer knob

Think about the lights in your home. A normal light switch has two positions: on and off. You flip it up and the bulb glows; you flip it down and the room goes dark. Simple.

Now think about a dimmer knob in a fancy living room. Instead of just on or off, you can turn the brightness to many levels. You can even set different rooms to different brightness for different moods.

A feature flag in .NET is exactly like that light switch. It lets you turn a part of your app on or off without rewiring the whole house (without redeploying your code). And when you want to test two ideas against each other, the flag becomes a dimmer knob: some users see version A, some see version B. That is A/B testing, and it is the main thing I want to teach you in this article.

By the end you will know how to add flags to an ASP.NET Core app, roll a feature out to a slice of users, and run a clean A/B test using variants.

What problem are we actually solving?

Imagine you built a new checkout button. You think it will sell more, but you are not sure. The old, scary way is: deploy it to everyone and pray. If it is bad, every customer suffers, and you have to rush a fix.

Feature flags give you a calmer way. You ship the new button hidden behind a flag. Then you:

  1. Turn it on only for yourself to test.
  2. Turn it on for 5 percent of users to check nothing breaks.
  3. Slowly raise it to 50 percent and compare sales against the old button.
  4. If it wins, give it to everyone. If it loses, switch it off in seconds.

No redeploy. No panic. Just a knob you control.

Shipping a new feature safely behind a flag, step by step.

The library: Microsoft.FeatureManagement

Microsoft ships an official package for this. For an ASP.NET Core app you add Microsoft.FeatureManagement.AspNetCore. It reads your flags from normal configuration, so you do not need any cloud account to start.

// In your terminal:
// dotnet add package Microsoft.FeatureManagement.AspNetCore
 
var builder = WebApplication.CreateBuilder(args);
 
// This wires up feature management and reads the
// "FeatureManagement" section from your configuration.
builder.Services.AddFeatureManagement();
 
var app = builder.Build();
app.Run();

That is the whole setup. Now you describe your flags in appsettings.json:

{
  "FeatureManagement": {
    "NewCheckoutButton": false,
    "BetaDashboard": true
  }
}

Here NewCheckoutButton is off and BetaDashboard is on. These are the plain light-switch flags: just true or false.

Reading a flag in your code

From .NET 8 onward, the main interface you use is IVariantFeatureManager. It can handle both simple on/off flags and the richer variant flags we will meet later. You inject it and call IsEnabledAsync.

public class CheckoutService
{
    private readonly IVariantFeatureManager _features;
 
    public CheckoutService(IVariantFeatureManager features)
    {
        _features = features;
    }
 
    public async Task<string> RenderButtonAsync(CancellationToken ct)
    {
        // Ask the manager: is this flag on right now?
        if (await _features.IsEnabledAsync("NewCheckoutButton", ct))
        {
            return "Buy now in one tap";   // new path
        }
 
        return "Proceed to checkout";       // old path
    }
}

The key idea: your code asks a question ("is this on?") instead of deciding by itself. The answer comes from configuration, so you can change it without touching the code.

How a flag check flows

Request
FeatureManager
Config
Answer

Steps

1

Request

User hits an endpoint

2

FeatureManager

IsEnabledAsync is called

3

Config

Reads FeatureManagement section

4

Answer

Returns true or false

The request asks the feature manager, which reads the rules from configuration.

Beyond on/off: feature filters

A plain true/false flag is useful, but real rollouts need more. This is where feature filters come in. The library ships three built-in filters:

FilterWhat it doesGood for
Microsoft.PercentageTurns the flag on for a random share of checksQuick rough rollouts
Microsoft.TimeWindowTurns the flag on only between two datesScheduled launches and sales
Microsoft.TargetingRolls out to named users, groups, and a stable percentageA/B testing and gradual rollout

A filter is just a rule that decides the answer each time you ask. Instead of a fixed true, the flag says "ask this filter."

The percentage filter

Here is a flag that is on roughly half the time:

{
  "FeatureManagement": {
    "NewCheckoutButton": {
      "EnabledFor": [
        {
          "Name": "Microsoft.Percentage",
          "Parameters": { "Value": 50 }
        }
      ]
    }
  }
}

Be careful: the percentage filter does not remember users. The same person might see the button on one click and off on the next. That makes it fine for "let about 10 percent of traffic try this" but wrong for an A/B test, where each user must stick to one side.

The time window filter

This flag switches on automatically for a festival sale and switches off after:

{
  "FeatureManagement": {
    "DiwaliSaleBanner": {
      "EnabledFor": [
        {
          "Name": "Microsoft.TimeWindow",
          "Parameters": {
            "Start": "01 Nov 2026 00:00:00 +05:30",
            "End": "05 Nov 2026 00:00:00 +05:30"
          }
        }
      ]
    }
  }
}

No one has to stay up at midnight to flip a switch. The clock does it for you.

The targeting filter: the heart of A/B testing

The targeting filter is the smart one. It can:

  • Always include certain users (your testers).
  • Always include or exclude whole groups (like "BetaTesters" or "Interns").
  • Roll out to a steady percentage of everyone else.

Crucially, it is sticky. It looks at a "targeting context" (normally the user id), hashes it, and gives the same user the same answer every time. That stickiness is what makes A/B testing fair.

{
  "FeatureManagement": {
    "NewCheckoutButton": {
      "EnabledFor": [
        {
          "Name": "Microsoft.Targeting",
          "Parameters": {
            "Audience": {
              "Users": [ "[email protected]" ],
              "Groups": [
                { "Name": "BetaTesters", "RolloutPercentage": 100 }
              ],
              "DefaultRolloutPercentage": 25,
              "Exclusion": { "Users": [ "[email protected]" ] }
            }
          }
        }
      ]
    }
  }
}

Read that as: Asha always gets it. Everyone in BetaTesters gets it. Everyone else has a 25 percent chance, decided once and kept stable. The CEO never gets it.

How the targeting filter decides for one user.

To use targeting, you register it and tell the library how to find the current user:

builder.Services.AddFeatureManagement()
    .WithTargeting();

In ASP.NET Core, WithTargeting() uses a default targeting context accessor. It reads the current user from HttpContext.User: the user id comes from the NameIdentifier claim, and groups come from Role claims. So once a user is logged in, the library already knows who they are.

Now the real thing: variants for A/B testing

On/off flags answer "should I show this?" A/B testing asks a different question: "which version should I show?" For that we use variant feature flags.

A variant is a named value a flag can return. It can be a string, a number, a boolean, or a whole configuration object. You define a list of variants and an allocation that splits users between them.

{
  "FeatureManagement": {
    "CheckoutButton": {
      "Variants": [
        {
          "Name": "Original",
          "ConfigurationValue": { "Text": "Proceed to checkout", "Color": "grey" }
        },
        {
          "Name": "Bold",
          "ConfigurationValue": { "Text": "Buy now", "Color": "green" }
        }
      ],
      "Allocation": {
        "DefaultWhenEnabled": "Original",
        "Percentile": [
          { "Variant": "Original", "From": 0,  "To": 50 },
          { "Variant": "Bold",     "From": 50, "To": 100 }
        ]
      },
      "EnabledFor": [
        { "Name": "AlwaysOn" }
      ]
    }
  }
}

This splits users 50/50. Half see the grey "Proceed to checkout" button; half see the green "Buy now" button. Because allocation uses the targeting context, each user stays on their side for the whole experiment. That is the property you must have for a trustworthy test.

You read a variant with GetVariantAsync:

public class CheckoutController : ControllerBase
{
    private readonly IVariantFeatureManager _features;
 
    public CheckoutController(IVariantFeatureManager features)
    {
        _features = features;
    }
 
    [HttpGet("button")]
    public async Task<IActionResult> GetButton(CancellationToken ct)
    {
        Variant variant = await _features.GetVariantAsync("CheckoutButton", ct);
 
        // Read the configuration object we set in JSON.
        var text = variant?.Configuration?["Text"]?.Value<string>()
                   ?? "Checkout";
 
        return Ok(new { label = text });
    }
}

One warning from the docs: a flag returns one variant per user. If you set up multiple variants in a way that forces more than one, the library throws. Keep your allocation clean so every user lands in exactly one bucket.

An A/B test with variants

Split users
Show A or B
Measure
Pick winner
Ship

Steps

1

Split users

50/50 by user id

2

Show A or B

Variant stays sticky

3

Measure

Track clicks and sales

4

Pick winner

Compare the numbers

5

Ship

Give winner to all

Users are split, behaviour is measured, and the winner ships.

Keeping the answer stable during a request

Sometimes a single request checks the same flag in several places. If your configuration changes mid-request, you could get different answers within one page, which is confusing.

To avoid this, use IVariantFeatureManagerSnapshot. A snapshot caches the result the first time you ask, so every later check in the same request gives the same answer. It is the safest choice inside controllers and Razor pages.

InterfaceWhen the answer can changeUse it when
IVariantFeatureManagerAny time config refreshesBackground jobs, simple checks
IVariantFeatureManagerSnapshotFrozen for the whole requestWeb requests that check a flag more than once

Moving the knobs to the cloud: Azure App Configuration

Everything so far works with just appsettings.json. But editing JSON and redeploying to flip a flag is slow. Azure App Configuration is the optional upgrade that gives you a dashboard.

With it you can:

  • Turn flags on and off from a web portal, no redeploy.
  • Refresh values while the app is running.
  • Get built-in telemetry for variant flags, which tells you which variant each user saw. That telemetry is gold for A/B tests, because it links "who saw what" to "what they did."

The wiring looks like this:

builder.Configuration.AddAzureAppConfiguration(options =>
{
    options.Connect(builder.Configuration["AppConfigConnection"])
           .UseFeatureFlags();   // pull feature flags from the store
});
 
builder.Services.AddAzureAppConfiguration();
builder.Services.AddFeatureManagement()
    .WithTargeting();
Where flags live: local file vs Azure App Configuration.

How I actually run an A/B test

Here is my simple recipe, the same one I would teach a beginner:

  1. Pick one clear goal. For example, "more people complete checkout." Decide the number you will watch before you start.
  2. Build both versions behind a variant flag, splitting users 50/50 with the targeting allocation.
  3. Make it sticky using the targeting context (the logged-in user id), so no one flips sides.
  4. Measure by logging which variant each user saw, plus whether they reached the goal. Azure telemetry does this for you, or you can log it yourself.
  5. Wait for enough data. A handful of users is not proof. Let it run until the numbers settle.
  6. Pick the winner and roll it to 100 percent. Then delete the losing code so your app stays clean.

That last step matters. Old flags pile up like clutter. Once an experiment is done, remove the flag and the dead branch. A flag is a temporary tool, not furniture.

Feature flags pair nicely with good tests and clean architecture. If you are wiring flags into controllers, integration tests help you check both the on and off paths. As a side note for accuracy: some popular .NET libraries that people used to grab for free, like MediatR and MassTransit, are now commercially licensed. They are not needed for feature flags at all. The official Microsoft.FeatureManagement packages stay free and open, so you can lean on them without licensing worries.

Common mistakes to avoid

  • Using the percentage filter for A/B tests. It is not sticky, so users jump between versions and your results are noise. Use the targeting filter or variant allocation instead.
  • Leaving flags forever. Old flags make code hard to read. Clean them up after the experiment.
  • Testing too many things at once. Change one thing per experiment so you know what caused the difference.
  • Forgetting the targeting context. Without a stable user id, the library cannot keep users on the same side.
  • Trusting tiny samples. Ten users clicking is luck, not evidence. Wait for enough traffic.

Quick recap

  • A feature flag is a switch that turns app behaviour on or off without redeploying your code.
  • Use the official Microsoft.FeatureManagement.AspNetCore package; from .NET 8+ the main interface is IVariantFeatureManager.
  • Define plain flags as true/false in the FeatureManagement section of appsettings.json.
  • Feature filters add rules: Microsoft.Percentage (random share), Microsoft.TimeWindow (date range), and Microsoft.Targeting (sticky, per-user rollout).
  • The targeting filter is sticky, so it is the right tool for fair rollouts and A/B tests.
  • For real A/B testing, use variants plus an allocation that splits users by percentage, and read them with GetVariantAsync.
  • Use IVariantFeatureManagerSnapshot to keep the answer stable across one web request.
  • Azure App Configuration is an optional upgrade for remote control, live refresh, and built-in A/B telemetry.
  • Always clean up a flag once its experiment is finished.

References and further reading

Related Posts