Feature Flags¶
This guide explains how to add feature flags to your application using the SAIF platform.
๐ Overview¶
Feature flags let you enable or disable functionality at runtime without redeploying your application. The platform supports two modes:
| Mode | Flag source | Refresh | Use case |
|---|---|---|---|
| Local / no App Config | appsettings.json / appsettings.Development.json |
Hot-reload on file change | Local development, apps without App Configuration |
| Azure App Configuration | Azure App Configuration store | Request-driven refresh (checks every 5 min) | Deployed environments |
Both modes use the same IFeatureManager interface from Microsoft.FeatureManagement โ your application code doesn't change between them. When AppConfigurationEndpoint is not configured, the platform falls back to reading flags from appsettings.json automatically.
Feature flags are an API-side concern โ the AppHost and frontend projects do not require any changes.
โ๏ธ Setup¶
1. Registration¶
AddAzureDefaults() (from SAIF.Platform.Azure) registers everything you need:
AddFeatureManagement()โ the .NET feature management system- Azure App Configuration provider (when
AppConfigurationEndpointis configured)
No additional service registration is needed in Program.cs.
2. Enable refresh middleware¶
Call UseAzureDefaults() after building the app to enable automatic refresh of feature flags from Azure App Configuration:
var builder = WebApplication.CreateBuilder(args);
builder
.AddAzureDefaults()
.AddServiceDefaults();
var app = builder.Build();
app.UseAzureDefaults(); // Enables App Configuration refresh middleware
This middleware calls TryRefreshAsync() on each HTTP request, which re-fetches flags from App Configuration when the refresh interval has elapsed. Without it, flag changes in App Configuration are never picked up.
UseAzureDefaults() is safe to call in all environments โ it's a no-op when AppConfigurationEndpoint is not configured.
3. Customize refresh interval (optional)¶
The default refresh interval is 5 minutes. Override it via AddAzureDefaults:
4. No AppHost changes required¶
Feature flag management is handled entirely within the API project. The Aspire AppHost and any frontend projects do not need modifications โ the AppHost orchestrates the application as usual, and the frontend consumes flag-gated API endpoints like any other endpoint.
๐ฉ Defining flags¶
Flag naming convention¶
For naming conventions, scoping rules, and lifecycle guidance, see the ARB Feature Flag Standard (PROP0007).
Separator character
The ARB standard uses : as the separator (e.g., claims-api:payments:new-processor), but Azure App Configuration feature flag keys do not support :. Use . as a substitute when following the standard naming format.
Project isolation in Azure App Configuration is handled via labels rather than key prefixes:
| Label | Purpose |
|---|---|
{projectId} (e.g., my-app) |
Base defaults for this project |
{projectId}/{environment} (e.g., my-app/test) |
Environment-specific overrides |
This scoping prevents collisions across applications that share the same App Configuration store.
Flag lifecycle¶
The ARB standard defines three flag categories with different lifecycle expectations:
| Category | Lifetime | Example | Cleanup |
|---|---|---|---|
| Release flags | Short (days to weeks) | ExpressCheckout |
Remove after 100% rollout |
| Experiment flags | Medium (weeks to months) | BlueVsGreenButton |
Remove after experiment concludes |
| Operational flags | Permanent | PaymentProcessingEnabled |
Maintain indefinitely (circuit breakers, kill switches) |
Expiration policy: All release and experiment flags must have expiration dates (default: 90 days). Review active flags quarterly to prevent stale flag accumulation.
Removal process:
- Disable the flag โ set to its final intended state
- Remove flag checks from code
- Deploy code changes
- Delete the flag from App Configuration after deployment completes
Don't delete config before code
Do not delete flags from App Configuration before removing the code that references them. Older code versions may still reference the flag, causing runtime errors.
Local development (appsettings.json)¶
Add flags to appsettings.json or appsettings.Development.json using the v2 feature management schema:
{
"feature_management": {
"feature_flags": [
{ "id": "ExampleFeature", "enabled": true },
{ "id": "GatedEndpoint", "enabled": false }
]
}
}
Changes to these files are hot-reloaded automatically โ toggle a flag value, save, and the next request picks up the change without restarting the application.
This also works in deployed containers that don't use App Configuration โ flags in appsettings.json are always available as a fallback.
Azure App Configuration¶
In deployed environments with AppConfigurationEndpoint configured, flags are sourced from Azure App Configuration. The platform loads flags using label-based precedence:
- Label
{projectId}โ base defaults (loaded first) - Label
{projectId}/{environment}(e.g.,my-app/test) โ environment-specific overrides
For example, a flag ExampleFeature with label my-app set to true can be overridden to false for the test environment by creating the same flag with label my-app/test.
Refresh latency
Flag changes in App Configuration are not instant. The middleware checks for updates on a configurable interval (default: 5 minutes, can be overridden in .AddAzureDefaults()). When the interval expires, the next incoming request triggers a background refresh but still serves cached values โ the request after that sees the updated values. This is a known characteristic of the Azure App Configuration middleware's non-blocking refresh model.
Infrastructure setup for App Configuration is covered in the deployment pipeline configuration.
๐ ๏ธ Using flags in endpoints¶
Inject IFeatureManager into your endpoint handlers.
IsEnabledAsync โ inline check¶
Inject IFeatureManager and check the flag manually. Use this when you need to branch logic inside a handler based on flag state:
app.MapGet("/api/features", async (IFeatureManager featureManager) =>
{
var enabled = await featureManager.IsEnabledAsync("ExampleFeature");
return new { ExampleFeature = enabled };
});
This gives you full control โ you can return different responses, log the flag state, or combine multiple flags to decide what to do.
Conditional endpoint gating¶
Use IsEnabledAsync to make an endpoint return 404 Not Found when a flag is off:
app.MapGet("/api/hello/beta", async (IFeatureManager featureManager) =>
{
if (!await featureManager.IsEnabledAsync("GatedEndpoint"))
return Results.NotFound();
return Results.Ok(new { Message = "Hello from the beta endpoint!" });
});
IFeatureManager vs IVariantFeatureManager¶
| Interface | When to use |
|---|---|
IFeatureManager |
Recommended for most apps. Simple IsEnabledAsync(name) checks |
IVariantFeatureManager |
When you need variant/targeting feature management from Microsoft.FeatureManagement |
๐ฅ๏ธ Frontend considerations¶
The frontend does not need a feature management library. Instead, it consumes flag state through your API:
- Create an API endpoint that returns flag states using
IFeatureManager.IsEnabledAsync - Call that endpoint from the frontend
- Use the response to conditionally render UI elements
app.MapGet("/api/features", async (IFeatureManager featureManager) =>
{
return new
{
GatedEndpoint = await featureManager.IsEnabledAsync("GatedEndpoint"),
NewDashboard = await featureManager.IsEnabledAsync("NewDashboard")
};
});
The frontend fetches /api/features and uses the boolean values to show or hide UI. This keeps flag management centralized in the API and avoids duplicating flag logic in the frontend.
useFeatureFlags hook (React / TypeScript)¶
The useFeatureFlags hook is available in @saif/platform-react. Projects consuming it from the Azure Artifacts feed use:
Then import the hook, as it encapsulates the logic for fetching and exposing flag states from the API:
Usage in a component:
import { useFeatureFlags } from "@saif/platform-react";
const Home = () => {
const apiUrl = import.meta.env.VITE_BACKEND_URL || "";
const { loading, error, isEnabled } = useFeatureFlags(apiUrl);
if (loading) return <p>Loading flags...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
{isEnabled("newDashboard") && <NewDashboard />}
<p>Gated Endpoint: {isEnabled("gatedEndpoint") ? "ON" : "OFF"}</p>
</div>
);
};
The hook returns false for unknown flag names, so components degrade gracefully when a flag hasn't been defined yet.
๐งช Testing¶
.NET unit tests¶
Test flag-dependent logic by mocking IFeatureManager with NSubstitute:
[Fact]
public async Task IsEnabledAsync_ReturnsTrue_WhenFlagIsEnabled()
{
var featureManager = Substitute.For<IFeatureManager>();
featureManager.IsEnabledAsync("ExampleFeature").Returns(true);
var result = await featureManager.IsEnabledAsync("ExampleFeature");
result.Should().BeTrue();
}
For integration tests, configure flags in-memory โ flag names work directly with IFeatureManager when no AppConfigurationEndpoint is set:
[Fact]
public async Task FeatureManager_ReadsFromInMemoryConfiguration()
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["feature_management:feature_flags:0:id"] = "ExampleFeature",
["feature_management:feature_flags:0:enabled"] = "true",
["feature_management:feature_flags:1:id"] = "GatedEndpoint",
["feature_management:feature_flags:1:enabled"] = "false",
})
.Build();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(config);
services.AddFeatureManagement();
var provider = services.BuildServiceProvider();
var featureManager = provider.GetRequiredService<IFeatureManager>();
(await featureManager.IsEnabledAsync("ExampleFeature")).Should().BeTrue();
(await featureManager.IsEnabledAsync("GatedEndpoint")).Should().BeFalse();
}
Frontend tests (Vitest)¶
Test the useFeatureFlags hook by mocking fetch:
import { renderHook, waitFor } from "@testing-library/react";
import { useFeatureFlags } from "@saif/platform-react";
it("returns flags from the API", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({
ok: true,
json: async () => ({ newDashboard: true, gatedEndpoint: false }),
} as Response);
const { result } = renderHook(() =>
useFeatureFlags("http://localhost:5000")
);
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.isEnabled("newDashboard")).toBe(true);
expect(result.current.isEnabled("gatedEndpoint")).toBe(false);
expect(result.current.isEnabled("unknownFlag")).toBe(false);
});
๐ก Tips¶
- Always call
UseAzureDefaults(): Without it, flag changes in App Configuration are never refreshed. It's safe to call even without App Configuration. - Labels handle isolation: In App Configuration, project and environment scoping is done via labels (
{projectId},{projectId}/{environment}), not key names. - Track flag lifecycle: Assign a category (release, experiment, operational), set expiration dates for temporary flags, and remove flags after full rollout.
- Hot reload: During local development, flag changes in
appsettings.jsontake effect on the next request โ no restart needed. - appsettings.json fallback: Apps without App Configuration (no
AppConfigurationEndpointset) automatically read flags fromappsettings.json. This works in both local development and deployed containers. - Feature filters: For more advanced scenarios (percentage rollout, time windows, targeting), see the feature filters documentation.
- Keep flags short-lived: Feature flags are meant for rollout control, not permanent configuration. Remove flags and their gating code once a feature is fully rolled out.
๐ Foundry example¶
A complete working example is available in the Forge foundry:
This includes:
- API โ
IFeatureManagerusage withIsEnabledAsyncinProgram.cs,UseAzureDefaults()middleware - Frontend โ
useFeatureFlagshook from@saif/platform-reactwith React usage inHome.tsx - .NET unit tests โ mock and in-memory configuration tests in
feature-flags.UnitTests/ - Frontend tests โ Vitest specs for
useFeatureFlagsin the@saif/platform-reactpackage
Run it with aspire run to see feature flags in action with a React frontend that displays flag state and calls gated endpoints. Flags are read from appsettings.json in local development.
๐ Resources¶
- ARB Feature Flag Standard (PROP0007) โ naming conventions, lifecycle management, governance, and compliance requirements
- Microsoft Feature Management v2 schema โ top-level
feature_management.feature_flagsdocument schema - Feature Flag v2 schema โ individual flag schema (
id,enabled,conditions,variants,allocation,telemetry) - Microsoft.FeatureManagement overview
- Azure App Configuration โ feature flag management
- Feature filters (percentage, time window, targeting)
- Azure App Configuration refresh
- Import/export App Configuration data โ bulk import used by the sync pipeline