Skip to content

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:

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 FeatureManagement configuration 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 default System.Text.Json serializer.

Local development

Add flags to appsettings.Development.json under the FeatureManagement section:

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

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

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

  1. Create an API endpoint that returns flag states using 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
    {
        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:

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

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

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

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, 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: IsEnabledAsync and WithFeatureGate cover 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.json take 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:

foundry/dotnet/feature-flags/

This includes:

  • API โ€” IsEnabledAsync and WithFeatureGate patterns in Program.cs
  • 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 both flag patterns in action with a React frontend that displays flag state and calls gated endpoints.


๐Ÿ“š Resources