Feature Flags¶
This guide explains how to add feature flags to your application using Microsoft.FeatureManagement and the SAIF platform.
๐ Overview¶
Feature flags let you enable or disable functionality at runtime without redeploying your application. The platform supports two levels:
- Local development โ flags defined in
appsettings.Development.json, hot-reloaded on change - Deployed environments โ flags sourced from Azure App Configuration via
SAIF.Platform.Azure
Feature flags are an API-side concern โ the AppHost and frontend projects do not require any changes.
โ๏ธ Setup¶
1. Add global usings¶
Add the feature management namespaces to your GlobalUsings.cs:
global using Microsoft.FeatureManagement;
global using Microsoft.FeatureManagement.AspNetCore;
Microsoft.FeatureManagement.AspNetCore must be referenced directly in your ASP.NET Core project โ SAIF.Platform.Azure uses the core Microsoft.FeatureManagement package to stay compatible with non-web runtimes (workers, console apps).
2. Registration¶
AddAzureDefaults() (from SAIF.Platform.Azure) already calls AddFeatureManagement(), so no additional service registration is needed in Program.cs.
3. 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¶
Flags follow the {ProjectId}.{FlagName} convention, where ProjectId matches your application's project ID. This scoping prevents collisions across applications that share the same Azure App Configuration store.
โน๏ธ Note: Flag names in the
FeatureManagementconfiguration section use PascalCase (e.g.,my-app.ExampleFeature), following .NET configuration conventions. When exposing flag state through REST endpoints, use camelCase in the JSON response (e.g.,exampleFeature) โ this happens automatically with the defaultSystem.Text.Jsonserializer.
Local development¶
Add flags to appsettings.Development.json under the FeatureManagement section:
{
"FeatureManagement": {
"my-app.ExampleFeature": true,
"my-app.BetaEndpoint": false
}
}
Changes to this file are hot-reloaded automatically โ toggle a flag value, save, and the next request picks up the change without restarting the application.
Deployed environments¶
In non-development environments, flags are sourced from Azure App Configuration. Infrastructure setup for App Configuration is covered separately in the infrastructure guide (see #263).
๐ ๏ธ Using flags in endpoints¶
Microsoft.FeatureManagement offers two patterns for gating endpoints. Choose based on how much control you need inside the handler.
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("my-app.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.
WithFeatureGate โ declarative endpoint gate¶
Use this when the entire endpoint should be unavailable while the flag is off. Returns 404 Not Found automatically โ no handler code runs:
app.MapGet("/api/hello/beta", () => new { Message = "Hello from the beta endpoint!" })
.WithFeatureGate("my-app.BetaEndpoint");
This is the minimal API equivalent of the [FeatureGate] attribute used on MVC controllers. The endpoint is fully invisible to callers when the flag is off.
Choosing between the two¶
| Approach | When to use |
|---|---|
IsEnabledAsync |
You need branching logic, gradual rollout behavior, or want to return different responses based on flag state |
WithFeatureGate |
The entire endpoint should be on or off โ no conditional logic needed |
You can combine both patterns in the same application.
๐ฅ๏ธ 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
IsEnabledAsync - Call that endpoint from the frontend
- Use the response to conditionally render UI elements
app.MapGet("/api/features", async (IFeatureManager featureManager) =>
{
return new
{
BetaEndpoint = await featureManager.IsEnabledAsync("my-app.BetaEndpoint"),
NewDashboard = await featureManager.IsEnabledAsync("my-app.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>Beta: {isEnabled("betaEndpoint") ? "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("my-app.ExampleFeature").Returns(true);
var result = await featureManager.IsEnabledAsync("my-app.ExampleFeature");
result.Should().BeTrue();
}
To test against a real IFeatureManager with in-memory configuration:
[Fact]
public async Task FeatureManager_ReadsFromInMemoryConfiguration()
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["FeatureManagement:my-app.ExampleFeature"] = "true",
["FeatureManagement:my-app.BetaEndpoint"] = "false",
})
.Build();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(config);
services.AddFeatureManagement(config.GetSection("FeatureManagement"));
var provider = services.BuildServiceProvider();
var featureManager = provider.GetRequiredService<IFeatureManager>();
(await featureManager.IsEnabledAsync("my-app.ExampleFeature")).Should().BeTrue();
(await featureManager.IsEnabledAsync("my-app.BetaEndpoint")).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, betaEndpoint: 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("betaEndpoint")).toBe(false);
expect(result.current.isEnabled("unknownFlag")).toBe(false);
});
๐ก Tips¶
- Start simple:
IsEnabledAsyncandWithFeatureGatecover the vast majority of use cases. - Consistent naming: Always prefix flags with your project ID (
{ProjectId}.{FlagName}) to avoid collisions in the shared App Configuration store. - Hot reload: During local development, flag changes in
appsettings.Development.jsontake effect on the next request โ no restart needed. - 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 โ
IsEnabledAsyncandWithFeatureGatepatterns inProgram.cs - 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 both flag patterns in action with a React frontend that displays flag state and calls gated endpoints.