Feature Flags in .NET with Microsoft.FeatureManagement
Feature flags let you deploy code without releasing functionality. You merge to main, deploy to production, and the new feature stays hidden behind a flag until you're ready to turn it on. No long-lived feature branches, no release-day surprises. Microsoft provides a first-party library for this: Microsoft.FeatureManagement.
Getting Started
Install the package:
dotnet add package Microsoft.FeatureManagement.AspNetCore
Register in Program.cs:
builder.Services.AddFeatureManagement();
Define flags in appsettings.json:
{
"FeatureManagement": {
"NewCheckoutFlow": true,
"BetaDashboard": false,
"AdvancedReporting": {
"EnabledFor": [
{
"Name": "Percentage",
"Parameters": {
"Value": 25
}
}
]
}
}
}
Defining Feature Names
Use a static class to avoid magic strings:
public static class FeatureFlags
{
public const string NewCheckoutFlow = "NewCheckoutFlow";
public const string BetaDashboard = "BetaDashboard";
public const string AdvancedReporting = "AdvancedReporting";
}
Checking Flags in Code
Inject IFeatureManager to check flags programmatically:
public class CheckoutService
{
private readonly IFeatureManager _featureManager;
public CheckoutService(IFeatureManager featureManager) =>
_featureManager = featureManager;
public async Task<CheckoutResult> ProcessAsync(Cart cart)
{
if (await _featureManager.IsEnabledAsync(FeatureFlags.NewCheckoutFlow))
{
return await ProcessWithNewFlow(cart);
}
return await ProcessWithLegacyFlow(cart);
}
}
Feature-Gated Endpoints
Gate entire endpoints with the FeatureGate attribute:
app.MapGet("/api/dashboard/beta", async (DashboardService service) =>
{
var data = await service.GetBetaDashboardAsync();
return Results.Ok(data);
})
.WithMetadata(new FeatureGateAttribute(FeatureFlags.BetaDashboard));
Or with controllers:
[FeatureGate(FeatureFlags.BetaDashboard)]
[ApiController]
[Route("api/[controller]")]
public class BetaDashboardController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> Get()
{
// Only accessible when BetaDashboard is enabled
return Ok(await _service.GetDashboardAsync());
}
}
When the flag is off, the endpoint returns 404 — the feature simply doesn't exist from the client's perspective.
Built-in Feature Filters
The library includes several filters out of the box:
Percentage filter — enables the feature for a percentage of requests:
{
"FeatureManagement": {
"ExperimentalSearch": {
"EnabledFor": [
{
"Name": "Percentage",
"Parameters": { "Value": 10 }
}
]
}
}
}
Time window filter — enables the feature during a specific period:
{
"FeatureManagement": {
"HolidayBanner": {
"EnabledFor": [
{
"Name": "TimeWindow",
"Parameters": {
"Start": "2025-12-20T00:00:00Z",
"End": "2025-12-27T00:00:00Z"
}
}
]
}
}
}
Custom Feature Filters
Build your own filters for specific targeting logic. Here's one that enables features for specific users:
[FilterAlias("UserList")]
public class UserListFilter : IFeatureFilter
{
private readonly IHttpContextAccessor _httpContextAccessor;
public UserListFilter(IHttpContextAccessor httpContextAccessor) =>
_httpContextAccessor = httpContextAccessor;
public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
{
var settings = context.Parameters.Get<UserListFilterSettings>();
var userId = _httpContextAccessor.HttpContext?.User
?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var enabled = settings?.AllowedUsers?.Contains(userId ?? "") ?? false;
return Task.FromResult(enabled);
}
}
public class UserListFilterSettings
{
public List<string> AllowedUsers { get; set; } = [];
}
Configuration:
{
"FeatureManagement": {
"AdminTools": {
"EnabledFor": [
{
"Name": "UserList",
"Parameters": {
"AllowedUsers": ["user-001", "user-002"]
}
}
]
}
}
}
Feature Flags in DI
Conditionally register services based on feature flags:
builder.Services.AddScoped<ISearchService>(sp =>
{
var featureManager = sp.GetRequiredService<IFeatureManager>();
return featureManager.IsEnabledAsync(FeatureFlags.ExperimentalSearch).Result
? sp.GetRequiredService<ElasticSearchService>()
: sp.GetRequiredService<SqlSearchService>();
});
Best Practices
Clean up old flags. Feature flags are temporary by nature. Once a feature is fully rolled out, remove the flag and the branching code. Stale flags accumulate technical debt.
Keep flag logic at the edges. Check flags in endpoints, middleware, or service entry points — not deep inside domain logic. The domain shouldn't know about deployment strategies.
Use configuration providers for dynamic flags. With Azure App Configuration or a custom provider, you can toggle flags without redeploying. The library supports hot-reloading through IConfiguration.
Feature flags are a deployment strategy, not an architecture pattern. Use them to decouple deployment from release, enable safe experimentation, and gradually roll out changes to production.