Feature Flags in .NET 8: ASP.NET Core, Minimal APIs, Blazor

Shipping .NET Features Without Risk
Every .NET team eventually hits the same wall: you have a feature ready in staging, but pushing it to production means flipping a switch for every user at once. If something breaks — wrong assumption, edge case, performance regression — your only recovery is a rollback and redeploy.
Feature flags solve this by separating deployment from release. You ship the code behind a flag, then control who sees it from a dashboard, without touching your pipeline. Start with 1% of users, watch your metrics, expand to 100%. If errors spike at any stage, disable the flag in seconds.
This guide shows how to add feature flags to .NET 8 applications — ASP.NET Core controllers, Minimal APIs, and Blazor — using the Rollgate .NET SDK. If you are new to feature flags, see our complete feature flag overview first.
Follow along after creating a free Rollgate account — SDK docs are there too.
Quick Start: Feature Flags in .NET 8
Install the Rollgate SDK from NuGet:
dotnet add package Rollgate.SDK
Initialize the client at application startup:
using Rollgate.SDK;
var client = new RollgateClient(new RollgateConfig
{
ApiKey = Environment.GetEnvironmentVariable("ROLLGATE_API_KEY") ?? "",
});
await client.InitializeAsync();
if (client.IsEnabled("new-checkout", false))
{
Console.WriteLine("New checkout enabled");
}
client.Dispose();
InitializeAsync() fetches all flag configurations from Rollgate in a single HTTP request and caches them locally. After that, IsEnabled() reads from an in-memory dictionary — no network call per evaluation, single-digit microsecond overhead.
Registering the Client with Dependency Injection
In ASP.NET Core applications, register the client as a singleton so it is shared across all requests:
// Program.cs
using Rollgate.SDK;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<RollgateClient>(sp =>
{
var client = new RollgateClient(new RollgateConfig
{
ApiKey = builder.Configuration["Rollgate:ApiKey"] ?? "",
RefreshInterval = TimeSpan.FromSeconds(30),
EnableStreaming = false,
});
// For tutorial simplicity. In production, prefer an IHostedService
// that calls InitializeAsync() during startup — see "Production Considerations" below.
client.InitializeAsync().GetAwaiter().GetResult();
return client;
});
var app = builder.Build();
Register your API key in appsettings.json:
{
"Rollgate": {
"ApiKey": "your-sdk-key"
}
}
For production, use dotnet user-secrets or environment variables rather than committing the key to source control.
Feature Flags in ASP.NET Core Controllers
Define a small abstraction so controllers depend on an interface (clean for testing) rather than the concrete SDK type:
public interface IFeatureFlags
{
bool IsEnabled(string flagKey, bool defaultValue = false);
}
public sealed class RollgateFeatureFlags : IFeatureFlags
{
private readonly RollgateClient _client;
public RollgateFeatureFlags(RollgateClient client) => _client = client;
public bool IsEnabled(string flagKey, bool defaultValue = false)
=> _client.IsEnabled(flagKey, defaultValue);
}
Register the wrapper alongside the singleton client:
builder.Services.AddSingleton<IFeatureFlags, RollgateFeatureFlags>();
Now inject IFeatureFlags into your controllers:
[ApiController]
[Route("api/[controller]")]
public class CheckoutController : ControllerBase
{
private readonly IFeatureFlags _flags;
public CheckoutController(IFeatureFlags flags) => _flags = flags;
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] OrderRequest request)
{
if (_flags.IsEnabled("checkout-v2", false))
return Ok(await ProcessV2CheckoutAsync(request));
return Ok(await ProcessV1CheckoutAsync(request));
}
}
Identifying the user (do it once per session, not per request)
RollgateClient.IdentifyAsync issues an HTTP request and triggers a flag refresh. Do not call it on every request — that would add a network round-trip to every endpoint and defeat the in-memory evaluation model.
The right place is an action filter that runs once per authenticated user and short-circuits when the identity has not changed:
public class FeatureFlagIdentityFilter : IAsyncActionFilter
{
private readonly RollgateClient _client;
// Per-process cache of users we've already identified.
// For multi-instance deployments, identify at login instead of per-request.
private static readonly HashSet<string> _identified = new();
private static readonly SemaphoreSlim _gate = new(1, 1);
public FeatureFlagIdentityFilter(RollgateClient client) => _client = client;
public async Task OnActionExecutionAsync(ActionExecutingContext ctx, ActionExecutionDelegate next)
{
var userId = ctx.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (!string.IsNullOrEmpty(userId) && !_identified.Contains(userId))
{
await _gate.WaitAsync();
try
{
if (!_identified.Contains(userId))
{
await _client.IdentifyAsync(new UserContext
{
Id = userId,
Email = ctx.HttpContext.User.FindFirstValue(ClaimTypes.Email) ?? "",
});
_identified.Add(userId);
}
}
finally { _gate.Release(); }
}
await next();
}
}
Register it globally:
builder.Services.AddControllers(options =>
options.Filters.Add<FeatureFlagIdentityFilter>());
A cleaner alternative for production: call IdentifyAsync once at login (in your authentication handler or hub), then never again until the user signs out.
Feature Flags in Minimal APIs
Minimal APIs in .NET 8 inject services directly into the handler signature. Assuming user identification has already happened (e.g. via the filter above or at login), the handler is just an IsEnabled check:
app.MapPost("/api/search", async (
SearchRequest req,
IFeatureFlags flags) =>
{
if (flags.IsEnabled("semantic-search", false))
return Results.Ok(await RunSemanticSearchAsync(req.Query));
return Results.Ok(await RunKeywordSearchAsync(req.Query));
});
For endpoints where a flag gates the entire route, an endpoint filter is the cleanest pattern. Resolve the flag service from request services so the extension does not need it as a parameter:
public static class FeatureFlagEndpointExtensions
{
public static TBuilder RequireFeature<TBuilder>(
this TBuilder builder,
string flagKey) where TBuilder : IEndpointConventionBuilder
{
return builder.AddEndpointFilter(async (context, next) =>
{
var flags = context.HttpContext.RequestServices
.GetRequiredService<IFeatureFlags>();
if (!flags.IsEnabled(flagKey, false))
return Results.NotFound();
return await next(context);
});
}
}
// Usage
app.MapGet("/api/v2/analytics", GetAnalyticsV2Handler)
.RequireFeature("analytics-v2")
.RequireAuthorization();
Feature Flags in Blazor
Blazor Server
In Blazor Server, inject IFeatureFlags (and AuthenticationStateProvider if you need the user identity) into your components:
@page "/checkout"
@inject IFeatureFlags Flags
@inject AuthenticationStateProvider AuthStateProvider
@if (_showNewCheckout)
{
<NewCheckoutFlow />
}
else
{
<LegacyCheckoutFlow />
}
@code {
private bool _showNewCheckout;
protected override async Task OnInitializedAsync()
{
// OnInitializedAsync runs once per circuit (Blazor Server),
// so identifying the user here is safe — it does NOT fire per render.
// (For Blazor WebAssembly, see the next section.)
_showNewCheckout = Flags.IsEnabled("checkout-v2", false);
}
}
If you do need to refresh flags for the current user (e.g. their plan just changed), call IdentifyAsync on the underlying RollgateClient, not on every render.
Blazor WebAssembly
Blazor WebAssembly runs in the browser, so you typically evaluate flags on the server and pass them down. The cleanest pattern is a flag-serving API endpoint plus a typed client:
// Server: expose flags for the current user
app.MapGet("/api/flags", async (RollgateClient flags, ClaimsPrincipal user) =>
{
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous";
await flags.IdentifyAsync(new UserContext { Id = userId });
return Results.Ok(flags.GetAllFlags());
}).RequireAuthorization();
// Blazor WASM: fetch and cache flags at startup
public class FlagService
{
private Dictionary<string, bool> _flags = new();
private readonly HttpClient _http;
public FlagService(HttpClient http) => _http = http;
public async Task LoadAsync()
{
_flags = await _http.GetFromJsonAsync<Dictionary<string, bool>>("/api/flags")
?? new Dictionary<string, bool>();
}
public bool IsEnabled(string key, bool defaultValue = false)
=> _flags.TryGetValue(key, out var val) ? val : defaultValue;
}
Register FlagService and call LoadAsync() once at WASM startup, before RunAsync():
// Program.cs (Blazor WebAssembly)
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddSingleton<FlagService>();
var host = builder.Build();
// Load flags BEFORE the app starts rendering — otherwise components
// would see an empty dictionary and fall through to defaultValue.
await host.Services.GetRequiredService<FlagService>().LoadAsync();
await host.RunAsync();
@* In any Blazor component *@
@inject FlagService Flags
@if (Flags.IsEnabled("new-dashboard"))
{
<NewDashboard />
}
Gradual Rollouts and User Targeting
For a safe gradual rollout, you need two things: a user identity passed to IdentifyAsync, and a rollout percentage configured in the Rollgate dashboard.
The SDK uses consistent hashing on the user ID to decide which percentage bucket a user falls into. This means the same user always gets the same result for a given flag — no flickering between page loads.
await _flags.IdentifyAsync(new UserContext
{
Id = userId,
Email = userEmail,
Attributes = new Dictionary<string, object?>
{
["plan"] = user.SubscriptionPlan, // "free", "starter", "pro", "enterprise"
["country"] = user.Country, // "US", "DE", "IT"
["beta"] = user.IsBetaTester, // bool
["team_size"] = user.Organization?.MemberCount,
}
});
In the Rollgate dashboard you can configure rules like:
- Percentage rollout: Enable for 10% of all users, increase incrementally
- Attribute targeting: Enable only for
plan = "enterprise" - Combined: 50% rollout, but only for users where
country = "US"
The SDK evaluates all of this locally after the initial fetch — no per-evaluation API call.
Testing Feature Flags in C#
Because controllers depend on IFeatureFlags (introduced earlier), tests just need a simple fake — no mocking framework required:
public class FakeFeatureFlags : IFeatureFlags
{
private readonly Dictionary<string, bool> _flags;
public FakeFeatureFlags(Dictionary<string, bool>? flags = null)
=> _flags = flags ?? new Dictionary<string, bool>();
public bool IsEnabled(string flagKey, bool defaultValue = false)
=> _flags.TryGetValue(flagKey, out var val) ? val : defaultValue;
}
// xUnit test — always test both flag states
public class CheckoutControllerTests
{
[Fact]
public async Task Post_WhenCheckoutV2Enabled_ReturnsV2Response()
{
var flags = new FakeFeatureFlags(new() { ["checkout-v2"] = true });
var controller = new CheckoutController(flags);
var result = await controller.CreateOrder(new OrderRequest { Amount = 99 });
var ok = Assert.IsType<OkObjectResult>(result);
Assert.Equal("v2", ((dynamic)ok.Value!).version);
}
[Fact]
public async Task Post_WhenCheckoutV2Disabled_ReturnsV1Response()
{
var flags = new FakeFeatureFlags(new() { ["checkout-v2"] = false });
var controller = new CheckoutController(flags);
var result = await controller.CreateOrder(new OrderRequest { Amount = 99 });
var ok = Assert.IsType<OkObjectResult>(result);
Assert.Equal("v1", ((dynamic)ok.Value!).version);
}
}
Always test both the enabled and the disabled path. Flags that are only tested in one state are bugs waiting to happen — see our feature flags vs feature branches guide for more on this.
Evaluation Details for Debugging
When you need to know why a flag returned a value — targeting rule, percentage rollout, or fallthrough — use IsEnabledDetail directly on RollgateClient (not exposed on the IFeatureFlags abstraction since it is debug-only):
public class DebugController : ControllerBase
{
private readonly RollgateClient _client;
private readonly ILogger<DebugController> _logger;
public DebugController(RollgateClient client, ILogger<DebugController> logger)
{
_client = client;
_logger = logger;
}
[HttpGet("/api/debug/flag/{key}")]
public IActionResult Inspect(string key)
{
var detail = _client.IsEnabledDetail(key, false);
_logger.LogDebug(
"Flag {FlagKey}={Value} reason={Reason}",
key, detail.Value, detail.Reason.Kind);
return Ok(new { detail.Value, reason = detail.Reason.Kind.ToString() });
}
}
This is useful for diagnosing why a specific user is (or is not) seeing a rollout.
Production Considerations
Circuit Breaker
The SDK includes a built-in circuit breaker. After 5 consecutive failures, it stops making requests and uses cached values as fallback:
var client = new RollgateClient(new RollgateConfig
{
ApiKey = Environment.GetEnvironmentVariable("ROLLGATE_API_KEY") ?? "",
CircuitBreaker = new CircuitBreakerConfig
{
FailureThreshold = 5,
RecoveryTimeout = TimeSpan.FromSeconds(30),
SuccessThreshold = 3,
},
Cache = new CacheConfig
{
Ttl = TimeSpan.FromMinutes(5),
StaleTtl = TimeSpan.FromHours(1),
Enabled = true,
}
});
Your .NET service will never fail because the feature flag service is unreachable. Flags degrade gracefully to the last known value, then to defaultValue if no cache exists.
SSE Streaming for Kill Switches
For flags you need to propagate instantly — kill switches, incident response, maintenance mode — enable SSE streaming:
var client = new RollgateClient(new RollgateConfig
{
ApiKey = Environment.GetEnvironmentVariable("ROLLGATE_API_KEY") ?? "",
EnableStreaming = true,
});
With streaming enabled, flag changes reach your .NET service in under one second. No polling delay.
For a deep dive on rollback patterns in production, see our gradual rollouts guide and A/B testing with feature flags.
FAQ
Which .NET feature flag approach should I use in 2026?
You have three solid options. Microsoft.FeatureManagement (Microsoft.FeatureManagement.AspNetCore) is free, built into ASP.NET Core, and stores flags in appsettings.json. It works well for simple on/off toggles and does not require an external service. OpenFeature .NET (OpenFeature NuGet) is the CNCF vendor-neutral standard — you pick a provider and the evaluation code stays the same if you switch later. A managed service like Rollgate gives you a dashboard, targeting rules, gradual rollouts, and audit logs without building any infrastructure.
The decision tree: simple toggles with no runtime changes needed → Microsoft.FeatureManagement. Planning to evaluate multiple providers or want CNCF alignment → OpenFeature. Need a dashboard, real-time changes, and user targeting → managed service.
Does the Rollgate SDK work with older .NET versions?
The Rollgate.SDK NuGet package targets net8.0. For older runtimes (.NET 6, .NET Framework 4.8), contact us — we can advise on compatibility or provide a compatible build.
Can I use feature flags in .NET background services?
Yes. Register RollgateClient as a singleton and inject it into your BackgroundService. Evaluate flags inside ExecuteAsync using the same IsEnabled API. Polling keeps flags fresh without blocking the background thread.
public class ReportService : BackgroundService
{
private readonly RollgateClient _flags;
public ReportService(RollgateClient flags) => _flags = flags;
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
if (_flags.IsEnabled("new-report-engine", false))
await RunNewReportEngine(ct);
else
await RunLegacyReportEngine(ct);
await Task.Delay(TimeSpan.FromHours(1), ct);
}
}
}
How do feature flags interact with EF Core migrations?
Do not use feature flags inside migrations. Migrations must be deterministic and repeatable — a flag that is enabled today might not be enabled when someone runs the migration on a fresh database tomorrow. Use flags in application-layer code, not in the persistence layer.
What happens if InitializeAsync fails at startup?
If the initial fetch fails and there is no cached state, InitializeAsync throws. You can catch this and proceed with all flags at their defaultValue, or let the exception surface and prevent startup (recommended — better to fail fast than to serve an unknown state).
Next Steps
Ready to add feature flags to your .NET 8 application?
- Create a free Rollgate account — no credit card required
- Read the .NET SDK docs — full API reference and advanced configuration
- Learn about gradual rollouts — the safest pattern for shipping production features
Feature flags in .NET give you the control to ship confidently — deploy on your schedule, release on your terms, and roll back in seconds if anything goes wrong.