Microsoft.FeatureManagement.AspNetCore is the kit’s feature-flag system. The kit ships a custom TenantFeatureFilter so flags can be enabled for specific tenants — useful for staged rollouts, beta cohorts, kill switches per customer, and per-environment toggles without code changes.
Opt in
builder.AddHeroPlatform(o => o.EnableFeatureFlags = true);AddHeroPlatform registers Microsoft.FeatureManagement services plus TenantFeatureFilter so per-tenant evaluation works. Feature flags are off by default — the shipped host (FSH.Starter.Api/Program.cs) doesn’t enable them, so flip the toggle before declaring any flags.
Declaring flags
Flags live in appsettings.json under FeatureManagement. Three example shapes:
{ "FeatureManagement": { // 1. simple on/off "BetaCheckoutFlow": true,
// 2. tenant-scoped — only enabled for listed tenants "BetaProductImagesV2": { "EnabledFor": [ { "Name": "Tenant", "Parameters": { "AllowedTenants": ["acme", "globex"] } } ] },
// 3. multi-filter (tenant + percentage rollout) "NewSearchUi": { "EnabledFor": [ { "Name": "Tenant", "Parameters": { "AllowedTenants": ["acme"] } }, { "Name": "Microsoft.Percentage", "Parameters": { "Value": 10 } } ] } }}The Tenant filter is the kit’s TenantFeatureFilter (alias Tenant, parameter AllowedTenants, case-insensitive tenant-id match); the percentage filter is built into Microsoft.FeatureManagement. You can stack filters — they OR together by default (any filter passing enables the flag).
Checking a flag
Inject IFeatureManager and call IsEnabledAsync:
public sealed class GetProductByIdQueryHandler( ICatalogDbContext db, IFeatureManager features, IImageEnricher images) : IQueryHandler<GetProductByIdQuery, ProductResponse>{ public async ValueTask<ProductResponse> Handle(GetProductByIdQuery q, CancellationToken ct) { var product = await db.Products.FindAsync([q.ProductId], ct).ConfigureAwait(false);
if (await features.IsEnabledAsync("BetaProductImagesV2").ConfigureAwait(false)) return await images.EnrichV2Async(product, ct).ConfigureAwait(false);
return ProductResponse.From(product); }}For tenant-scoped flags, TenantFeatureFilter resolves the tenant from the Finbuckle multi-tenant context (falling back to the tenant request header if the context isn’t resolved yet) and matches it against AllowedTenants. No tenant resolvable → the filter says no. No flag definition at all → IsEnabledAsync returns false.
Gating endpoints
For all-or-nothing endpoint gating, use the kit’s .RequireFeature(...) extension, which attaches FeatureGateEndpointFilter:
endpoints.MapGet("/catalog/products/recommended", handler) .RequirePermission(perm) .RequireFeature("RecommendationsEndpoint");When the flag is off, the endpoint returns 404 Not Found — to callers, a gated-off endpoint doesn’t exist. Use this for shipping new endpoints behind a flag without exposing them broadly.
Common patterns
Staged rollout
Enable for one tenant first, then add more once you’re confident:
{ "FeatureManagement": { "NewBilling": { "EnabledFor": [ { "Name": "Tenant", "Parameters": { "AllowedTenants": ["test-tenant"] } } ] } }}Re-deploy with more tenants in the array as confidence grows; eventually drop the filter and set the flag to true globally.
Kill switch
{ "FeatureManagement": { "ChatRealtimeEnabled": true } }Set to false in an incident to instantly disable the SignalR push path without redeploying. Have the handler fall back to polling, or return a graceful “feature temporarily unavailable” response.
Per-environment
appsettings.Production.json overrides appsettings.json. Set flags to false for production until they ship:
{ "FeatureManagement": { "BetaCheckoutFlow": false }}Where flag state lives
In configuration. The kit doesn’t ship a database-backed flag store. That’s deliberate: editing JSON config is cheap, auditable through git history, and doesn’t require a separate management UI.
Related but separate: billing/quota plans carry an ActiveFeatureFlags limit (QuotaResource.ActiveFeatureFlags — see QuotaOptions:Plans and the admin plan form). That’s a metering dimension for plan design; nothing currently enforces it against your FeatureManagement definitions.
If you outgrow JSON — you need runtime flag toggles without redeploy, or per-user (not per-tenant) flags — wire a custom IFeatureDefinitionProvider against your store of choice (Redis, EF Core, ConfigCat, LaunchDarkly). The kit’s TenantFeatureFilter pattern is the template for custom filters.
Removing a flag
When a flag has been on globally for a while and the alternative is gone:
- Delete the flag definition from
appsettings.json. - Remove the
IFeatureManager.IsEnabledAsynccall in the handler — collapse the conditional to the always-on branch. - Delete the dead alternative path.
Don’t skip step 3. Lingering “old” branches are technical debt and a security risk (un-tested code that might still be reachable).
Related
- Multitenancy module — the tenant resolution that
TenantFeatureFilterreads. - Production checklist — flag hygiene before a major release.
- Web building block —
EnableFeatureFlagstoggle.