Sitelet https://docs.useflagpole.dev/server/dotnet/
Skip to main content

.NET

A server-side .NET SDK for Flagpole feature flags — a polling client on HttpClient with zero dependencies, plus an ASP.NET Core integration package.

Updates are by polling, not streaming

Unlike the Node SDK, the .NET SDK does not hold a WebSocket. It refreshes the flag set by calling the REST API on an interval (FlagpoleOptions.PollInterval, default 30 seconds). A flag change therefore takes up to one poll interval to take effect.

Lower the interval for faster propagation, or call client.RefreshAsync() after a known change:

new FlagpoleOptions { PollInterval = TimeSpan.FromSeconds(5) };

Setting the interval to TimeSpan.Zero disables polling entirely — the flag set is then only what StartAsync() loaded plus any manual RefreshAsync().

Table of Contents​

Features​

  • 🟣 Zero dependencies in the core — HttpClient + System.Text.Json
  • ⚡ Non-blocking reads: after StartAsync(), IsEnabled(...) is a lock-free in-memory lookup
  • 🔁 Polling: configurable-interval REST refresh on a background task
  • 🌱 ASP.NET Core: DI registration, hosted startup, [RequireFeatureFlag], minimal-API endpoint filter
  • 🌍 Environment Targeting: load and evaluate flags per environment
  • 🔐 Fail-safe: unknown or unreachable flags return your configured fallback

Packages​

Package
Flagpolethe core client — usable from any .NET app (console, worker, …)
Flagpole.AspNetCoreDI, hosted startup, [RequireFeatureFlag], endpoint filter

Installation​

dotnet add package Flagpole              # core only
dotnet add package Flagpole.AspNetCore # + ASP.NET Core glue (pulls in Flagpole)

Requirements​

  • .NET 8+

Quick Start​

1. Create and start the client​

using Flagpole;

await using var client = new FlagpoleClient(new FlagpoleOptions
{
ApiKey = "fp_live_your_api_key",
Environments = ["production"],
Fallbacks = { ["new-checkout"] = false },
});

await client.StartAsync();

StartAsync performs the first fetch and then polls in the background. It throws a FlagpoleException if that first fetch fails, but the client stays usable and keeps polling — reads fall back until a later fetch succeeds.

2. Check flags​

if (client.IsEnabled("new-checkout"))
{
// ...
}

FeatureFlag? flag = client.Flag("new-checkout");
IReadOnlyDictionary<string, FeatureFlag> all = client.AllFlags;

3. Shut down​

await client.DisposeAsync();   // or `await using`, as above

API Reference​

FlagpoleClient​

MemberDescription
new FlagpoleClient(FlagpoleOptions) / (string apiKey, Action<FlagpoleOptions>?)Construct. Performs no I/O.
Task StartAsync(CancellationToken)Initial fetch, then start polling. Re-fetches if called again.
Task RefreshAsync(CancellationToken)Re-fetch every flag now
bool IsEnabled(string)Fallback for unknown / disabled / non-matching flags. Never throws.
FeatureFlag? Flag(string)The full flag, or null
IReadOnlyDictionary<string, FeatureFlag> AllFlagsA snapshot of every loaded flag
bool IsStartedWhether StartAsync has completed once
Task StopAsync()Stop polling
DisposeAsync() / Dispose()Stop polling and release the HttpClient

FlagpoleOptions​

PropertyDefaultDescription
ApiKey""API key from the dashboard
EnvironmentProductionWhich backend to talk to
Environmentsnull (all)Flag environments to load and evaluate against
PollInterval30sRefresh frequency. TimeSpan.Zero disables polling.
RequestTimeout10sPer-request timeout
Fallbacks{}Value for a flag that isn't loaded
BaseUrlOverridenullReplace the host derived from Environment
OnErrornullCallback for background poll failures

FeatureFlag​

A record: Id, Name, Description, IsEnabled (raw switch — prefer client.IsEnabled(name)), Project, Organization, Conditions (JsonElement), Environments (IReadOnlyList<string>, empty means "all"), CreatedAt / UpdatedAt (raw ISO-8601 strings).

FlagpoleException​

An abstract exception with three sealed subtypes: FlagpoleException.Api (has StatusCode, Body), FlagpoleException.InvalidResponse, FlagpoleException.Transport.

ASP.NET Core​

builder.Services.AddFlagpole(builder.Configuration.GetSection("Flagpole"));
// or: builder.Services.AddFlagpole(o => { o.ApiKey = "..."; o.Environments = ["production"]; });
{
"Flagpole": {
"ApiKey": "fp_live_your_api_key",
"Environments": ["production"],
"PollInterval": "00:00:30",
"RequestTimeout": "00:00:10",
"Fallbacks": { "new-checkout": false }
}
}

AddFlagpole registers a singleton FlagpoleClient (using IHttpClientFactory) and a hosted service that calls StartAsync at boot — a failed first fetch logs a warning and the app still starts.

Reading flags​

app.MapGet("/checkout", (FlagpoleClient flagpole) =>
flagpole.IsEnabled("new-checkout") ? "new" : "legacy");

Gating endpoints​

// minimal API
app.MapGet("/beta", () => "welcome").RequireFeatureFlag("beta-access");
app.MapGet("/early", () => "welcome").RequireFeatureFlag("early-access", disabledStatusCode: 403);

// MVC / controllers
[HttpGet("/beta")]
[RequireFeatureFlag("beta-access")] // 404 when off
public IActionResult Beta() => Ok("welcome");

[RequireFeatureFlag("early-access", DisabledStatusCode = 403)]
public IActionResult Early() => Ok("welcome");

[RequireFeatureFlag] works on a controller class too, gating every action.

Configuration​

Environments​

Environments is a filter — it controls which flags are loaded and how IsEnabled evaluates targeting. A flag with no environments always applies.

Backend host​

FlagpoleEnvironmentREST
Developmenthttp://localhost:5000
Staginghttps://api.staging.useflagpole.dev
Productionhttps://useflagpole-api.onrender.com

Error Handling​

IsEnabled(...) returns the configured fallback (default false) for any flag it can't resolve and never throws.

StartAsync and RefreshAsync throw a FlagpoleException:

try
{
await client.StartAsync();
}
catch (FlagpoleException.Api ex)
{
logger.LogWarning("Flagpole rejected the request: {Status} {Body}", ex.StatusCode, ex.Body);
// carry on — IsEnabled will use fallbacks
}

A failed background poll is passed to FlagpoleOptions.OnError (the ASP.NET Core package wires this to ILogger) and retried on the next tick.

Best Practices​

1. One client per process​

Register it as a singleton (AddFlagpole does), or create one and share it.

2. Start before serving traffic​

The hosted service does this. Outside ASP.NET Core, await client.StartAsync() in your startup path.

3. Tune the poll interval​

30s suits most rollouts. Drop it to a few seconds for kill-switch flags, or call RefreshAsync from an admin webhook.

4. Always set fallbacks for critical flags​

new FlagpoleOptions { Fallbacks = { ["payments-v2"] = false } };

Troubleshooting​

Flags are stale​

Expected up to one poll interval. Lower PollInterval or call RefreshAsync.

Flags are always the fallback value​

  • Wrong or expired API key
  • The flag targets environments not in your Environments list
  • The first fetch failed — check the logs / catch the FlagpoleException

AddFlagpole didn't seem to do anything​

Make sure something resolves FlagpoleClient (injecting it, or the hosted service running). The hosted service only starts with the app host.

Contributing​

The SDK lives in the flagpole-sdks monorepo under packages/server/dotnet.

git clone https://github.com/flagpole-corp/flagpole-sdks.git
cd flagpole-sdks/packages/server/dotnet

dotnet build
dotnet test
dotnet format --verify-no-changes

License​

MIT

Support​