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.
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:
- Turn it on only for yourself to test.
- Turn it on for 5 percent of users to check nothing breaks.
- Slowly raise it to 50 percent and compare sales against the old button.
- If it wins, give it to everyone. If it loses, switch it off in seconds.
No redeploy. No panic. Just a knob you control.
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
Steps
Request
User hits an endpoint
FeatureManager
IsEnabledAsync is called
Config
Reads FeatureManagement section
Answer
Returns true or false
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:
| Filter | What it does | Good for |
|---|---|---|
Microsoft.Percentage | Turns the flag on for a random share of checks | Quick rough rollouts |
Microsoft.TimeWindow | Turns the flag on only between two dates | Scheduled launches and sales |
Microsoft.Targeting | Rolls out to named users, groups, and a stable percentage | A/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.
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
Steps
Split users
50/50 by user id
Show A or B
Variant stays sticky
Measure
Track clicks and sales
Pick winner
Compare the numbers
Ship
Give winner to all
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.
| Interface | When the answer can change | Use it when |
|---|---|---|
IVariantFeatureManager | Any time config refreshes | Background jobs, simple checks |
IVariantFeatureManagerSnapshot | Frozen for the whole request | Web 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();How I actually run an A/B test
Here is my simple recipe, the same one I would teach a beginner:
- Pick one clear goal. For example, "more people complete checkout." Decide the number you will watch before you start.
- Build both versions behind a variant flag, splitting users 50/50 with the targeting allocation.
- Make it sticky using the targeting context (the logged-in user id), so no one flips sides.
- 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.
- Wait for enough data. A handful of users is not proof. Let it run until the numbers settle.
- 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.
A tiny note on related tools
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.AspNetCorepackage; from .NET 8+ the main interface isIVariantFeatureManager. - Define plain flags as
true/falsein theFeatureManagementsection ofappsettings.json. - Feature filters add rules:
Microsoft.Percentage(random share),Microsoft.TimeWindow(date range), andMicrosoft.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
IVariantFeatureManagerSnapshotto 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
- .NET Feature Flag Management — Microsoft Learn
- Use variant feature flags from Azure App Configuration in ASP.NET Core — Microsoft Learn
- Quickstart: add feature flags to ASP.NET Core apps — Microsoft Learn
- Feature Flags in .NET and How I Use Them for A/B Testing — Milan Jovanović
- Microsoft.FeatureManagement release notes — GitHub
Related Posts
ASP.NET Core Integration Testing Best Practices (.NET 10)
A friendly .NET 10 guide to ASP.NET Core integration testing: WebApplicationFactory, real databases with Testcontainers, clean test isolation, and CI tips.
Creating Data-Driven Tests With xUnit (.NET 10)
A friendly .NET 10 guide to data-driven tests in xUnit: Theory, InlineData, MemberData, ClassData, strongly typed TheoryData, and xUnit v3 tips.
Top 15 Mistakes Developers Make When Creating Web APIs
A warm, beginner-friendly tour of the 15 most common Web API mistakes in ASP.NET Core, with simple fixes, diagrams, tables, and clear C# examples.
How to Increase the Performance of Web APIs in .NET
A friendly, step-by-step guide to making your ASP.NET Core Web APIs fast: async, caching, query tuning, compression, and pooling in .NET 10.
Authentication and Authorization Best Practices in ASP.NET Core
A friendly guide to authentication and authorization in ASP.NET Core for .NET 10 — JWT, cookies, claims, roles, policies, and security best practices with diagrams.
How to Set Up Production-Ready Monitoring With ASP.NET Core Health Checks
A friendly, step-by-step guide to production-ready monitoring with ASP.NET Core health checks: liveness, readiness, dependency checks, a UI, and probes.