Implement Feature Management in Blazor ASP.NET Core

Feature flags let you turn a piece of functionality on or off without touching code or redeploying the whole application. In a plain ASP.NET Core API this is a five minute job. In a hosted Blazor WebAssembly application it gets a bit more interesting, because you effectively have two runtimes, the server and the browser sandboxed client, and both need to agree on whether a feature is switched on.

This article walks through a working setup that uses Microsoft’s own Microsoft.FeatureManagement package on both sides of a Blazor hosted application secured with Azure AD. The server gates an API endpoint and a scoped service registration, and the WASM client hides a navigation link and skips an API call, all driven from the same feature name.

The Feature X API page in the sample app, reachable only when the FeatureX flag evaluates to true on both the server and the client.
The Feature X API page in the sample app, reachable only when the FeatureX flag evaluates to true on both the server and the client.

Why use a package instead of a raw config flag

You could read a boolean out of appsettings.json yourself and scatter if statements around the codebase. That works for a single on or off switch, but it does not scale once you need percentage rollouts, time windows, or per user targeting. Microsoft.FeatureManagement gives you one interface, IFeatureManager, that you call the same way regardless of how complicated the underlying rule becomes.

The bigger reason to standardise on it in a Blazor hosted app is consistency. The server project and the WASM client project are two separate processes with two separate dependency injection containers. Using the same package and the same feature names in both means a developer reading the client code recognises the pattern immediately from having seen it on the server.

Project setup

The sample is a Blazor WebAssembly hosted application, Server plus Client plus a Shared project, using Azure AD with a BFF cookie based authentication flow. Because the WASM client executes entirely inside the browser, it cannot share the server’s DI container or its configuration. Each project needs its own reference to a feature management package and its own appsettings entry for the flag.

The Server project references Microsoft.FeatureManagement.AspNetCore. The Client project references Microsoft.FeatureManagement, the base package without the ASP.NET Core specific middleware helpers, since there is no request pipeline to hook into inside a browser process.

Wiring up feature management on the server

On the server, AddFeatureManagement registers IFeatureManager in the DI container. In this sample the flag is also read directly from configuration at startup, outside of IFeatureManager, to decide whether a whole scoped service should be registered at all.

services.AddFeatureManagement();
 
var featureXEnabled = configuration.GetValue<bool>("FeatureManagement:FeatureX");
 
if(featureXEnabled)
{
    services.AddScoped<FeatureXService>();
}

This runs once at application startup, before any request arrives, so there is no per request context to evaluate against and a plain configuration read is the right tool here rather than IFeatureManager. The payoff is that if FeatureX is off, FeatureXService is never registered in the container at all. Any code that accidentally tries to resolve it will fail loudly at startup or first use instead of quietly running a half finished feature in production. The trade off is that flipping this particular flag needs an application restart, because DI registrations are fixed once the container is built.

Inside a request, the check looks different. The controller that exposes the feature’s data injects IFeatureManager directly and calls IsEnabledAsync per request.

private IFeatureManager _featureManager;
private readonly FeatureXService _featureXService;
 
public FeatureXApiController(IFeatureManager featureManager, 
	FeatureXService featureXService)
{
	_featureManager = featureManager;
	_featureXService = featureXService;
}
 
[HttpGet]
public async Task<IActionResult> GetAsync()
{
	var featureX = await _featureManager
		.IsEnabledAsync(Features.FEATUREX);
 
	if(featureX)
	{
		return Ok(new List<string> { 
			"some data", 
			"more data", 
			_featureXService.GetFeatureString() 
		});
	}
 
	return NotFound();
}

IsEnabledAsync is async even for a plain on or off flag because the underlying feature filter might need to look something up externally, for example a percentage rollout filter that hashes a user id, or a targeting filter that checks group membership. A GET request against this endpoint returns 200 with the data when the flag is on and 404 when it is off. Returning 404 rather than 403 is a deliberate choice here, it avoids confirming to a caller that the endpoint exists at all when the feature is disabled. If you need callers to distinguish between not found and forbidden for their own error handling, you would pick 403 instead, but that does leak the existence of the route.

One common mistake with this pattern is assuming the flag check on the controller is enough on its own. It only protects the API. If the Blazor UI still renders a link to the page that calls this endpoint, a user can click through, see a loading state, and get stuck on an API call that always 404s. The client needs its own copy of the same check, not just the server.

The FeatureManagement section in the Server’s appsettings.json is the source of truth for the flag itself.

{
  "FeatureManagement": {
    "FeatureX": true
  }
}

The key under FeatureManagement, FeatureX in this case, must match the string constant referenced in code, here Features.FEATUREX. Get the casing or the section path wrong and the flag silently evaluates to false rather than throwing an exception, since Microsoft.FeatureManagement treats an unrecognised feature name as disabled by default. That default is sensible from a fail safe point of view, but it also means a typo can sit unnoticed for a while, since nothing crashes.

Wiring up feature management on the WASM client

The client’s Program.cs makes the same AddFeatureManagement call, just against the WASM host builder’s service collection instead of the server’s.

builder.Services.AddFeatureManagement();

A page that depends on the feature checks it in OnInitializedAsync, before it ever calls the API.

@page "/featurexapi"
@inject IAntiforgeryHttpClientFactory httpClientFactory
@inject IJSRuntime JSRuntime
@using Microsoft.FeatureManagement
@using AspNetCoreFeatures.Toggles.Shared;
@inject IFeatureManager _featureManager
 
<h1>Data from Feature X API</h1>
 
@if (apiData == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Data</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var data in apiData)
            {
                <tr>
                    <td>@data</td>
                </tr>
            }
        </tbody>
    </table>
}
 
@code {
    private string[]? apiData;
 
    public bool FeatureXEnabled { get; set; }
 
    protected override async Task OnInitializedAsync()
    {
        var featureX = await _featureManager.IsEnabledAsync(Features.FEATUREX);
 
        if (featureX)
        {
            var client = await httpClientFactory.CreateClientAsync();
            apiData = await client.GetFromJsonAsync<string[]>("api/FeatureXApi");
        }
    }
}

Checking the flag before calling the API avoids firing off a request that the server will reject anyway. It also means apiData stays null when the feature is off, and the page keeps showing Loading forever rather than an error. That is a rough edge in this particular sample. In a production app you would set an explicit FeatureXEnabled bool and render a proper unavailable message instead of leaving the user staring at a spinner that never resolves.

The WASM client needs its own appsettings.json in wwwroot with the same flag structure.

{
  "FeatureManagement": {
    "FeatureX": true
  }
}

This file is bundled into the client at build and publish time. That has a real consequence, flipping a feature for the WASM client normally means a new build and a new deployment, not just an appsettings edit on a running server. If you need to toggle client side features without redeploying the client, you have to fetch the flag state from the server at runtime instead of reading it from a file baked into the WASM payload, which is exactly the gap that a centrally managed configuration source like Azure App Configuration is built to close.

Hiding navigation based on the flag

The nav menu component follows the same OnInitializedAsync pattern to decide whether to render a link at all.

@using Microsoft.FeatureManagement
@using AspNetCoreFeatures.Toggles.Shared
@inject IFeatureManager _featureManager
 
<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">Blazor AAD BFF Cookies</a>
    <button class="navbar-toggler" @onclick="ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>
 
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <AuthorizeView>
            <Authorized>
 
                <li class="nav-item px-3">
                    <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                        <span class="oi oi-home" aria-hidden="true"></span> Home
                    </NavLink>
                </li>
 
                @if (FeatureXEnabled)
                {
                    <li class="nav-item px-3">
                        <NavLink class="nav-link" href="featurexapi">
                            <span class="oi oi-list-rich" aria-hidden="true"></span> Feature X API call
                        </NavLink>
                    </li>
                }
 
 
            </Authorized>
            <NotAuthorized>
                <li class="nav-item px-3">
                    <p class="whiteColor">no access</p>
                </li>
            </NotAuthorized>
        </AuthorizeView>
        
    </ul>
</div>
 
@code {
    private bool collapseNavMenu = true;
 
    public bool FeatureXEnabled { get; set; }
 
    protected override async Task OnInitializedAsync()
    {
        FeatureXEnabled = await _featureManager.IsEnabledAsync(Features.FEATUREX);
    }
   
    private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
 
    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

Hiding the link is purely cosmetic security. It stops a user from accidentally discovering the feature through the menu, nothing more. Anyone who types the route directly still lands on the page, and given the rough edge mentioned earlier, still gets stuck on an infinite Loading state if the flag is off. Treat the nav check as a UX nicety layered on top of the real gate, which is the server returning 404, never as the only line of defence.

Two runs of the sample app side by side. The Feature X API link only appears in the navigation when the FeatureX flag is switched on in configuration.
Two runs of the sample app side by side. The Feature X API link only appears in the navigation when the FeatureX flag is switched on in configuration.

Practical considerations before you use this in production

A plain appsettings based flag works well for a binary switch that is the same for every user in an environment, on in staging, off in production until you are ready. It falls apart the moment you need gradual rollout to a percentage of users, targeting by group, or a scheduled activation window. Microsoft.FeatureManagement supports all of that through IFeatureFilter implementations, percentage and time window filters ship out of the box, but you still have to write and register custom filters yourself for anything specific to your domain.

Refresh behaviour is worth planning for early. A flag baked into appsettings.json needs a server restart to change, and for the WASM client it needs an entirely new build and deploy, since the file ships inside the compiled client bundle. Azure App Configuration solves this with a sentinel key and a periodic polling refresh, so a flag flip on the server side can take effect without a restart. Getting the same live behaviour on the WASM client is harder, since the client would need to poll the server itself rather than reading a bundled file.

For testing, mock IFeatureManager rather than hard coding configuration values in your test setup. That lets you exercise both the enabled and disabled branches of a controller or a component in isolation, without maintaining two copies of appsettings.json for your test suite.

When this pattern is not enough

This approach has no dashboard and no audit trail. Toggling a flag means editing a JSON file and redeploying, which is fine for a two person team but awkward once a product manager or support engineer wants to flip a switch without going through a release. If that matters to you, look at Azure App Configuration’s feature flag UI, or a dedicated service like LaunchDarkly or the open source Unleash, both of which give non technical users a portal to manage flags and both of which plug into the same Microsoft.FeatureManagement abstraction through their own feature filter providers.

The pattern shown here is a solid starting point for a small number of flags in a hosted Blazor app. As the number of flags grows, or as you need per tenant or per user targeting, the maintenance cost of scattering IsEnabledAsync calls across both projects by hand starts to add up, and that is the point where a managed configuration source pays for itself.

Leave a Reply

Discover more from Behind the Stack

Subscribe now to keep reading and get access to the full archive.

Continue reading