Skip to content

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 AppConfigurationEndpoint is 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:

Program.cs
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:

Program.cs
builder.AddAzureDefaults(o => o.FeatureFlagRefreshInterval = TimeSpan.FromSeconds(30));

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:

  1. Disable the flag โ€” set to its final intended state
  2. Remove flag checks from code
  3. Deploy code changes
  4. 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:

appsettings.json
{
  "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:

  1. Label {projectId} โ€” base defaults (loaded first)
  2. 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:

Program.cs
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:

Program.cs
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:

  1. Create an API endpoint that returns flag states using IFeatureManager.IsEnabledAsync
  2. Call that endpoint from the frontend
  3. Use the response to conditionally render UI elements
Program.cs โ€” API endpoint exposing flag state
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:

package.json
{
  "dependencies": {
    "@saif/platform-react": "^3.0.0"
  }
}

Then import the hook, as it encapsulates the logic for fetching and exposing flag states from the API:

import { useFeatureFlags } from '@saif/platform-react';

Usage in a component:

pages/Home.tsx
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:

FeatureFlagTests.cs
[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:

FeatureFlagTests.cs
[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:

src/typescript/packages/saif-platform-react/test/useFeatureFlags.test.ts
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.json take effect on the next request โ€” no restart needed.
  • appsettings.json fallback: Apps without App Configuration (no AppConfigurationEndpoint set) automatically read flags from appsettings.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:

foundry/dotnet/feature-flags/

This includes:

  • API โ€” IFeatureManager usage with IsEnabledAsync in Program.cs, UseAzureDefaults() middleware
  • Frontend โ€” useFeatureFlags hook from @saif/platform-react with React usage in Home.tsx
  • .NET unit tests โ€” mock and in-memory configuration tests in feature-flags.UnitTests/
  • Frontend tests โ€” Vitest specs for useFeatureFlags in the @saif/platform-react package

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