I got tired of writing if (featureFlags.IsEnabled(...)) everywhere — so I built an ecosystem
I've been writing C# for over a decade and feature flags have always annoyed me in the same way.
Not the concept — the concept is great. The ability to ship code dark, roll out to a percentage of traffic, or flip a switch without a redeploy is genuinely powerful. What annoyed me was the noise. Every time I added a feature flag I'd end up with this scattered through the codebase:
if (featureFlags.IsEnabled("SendWelcomeEmail"))
{
SendWelcomeEmail();
}
Multiply that across a real codebase and you've got if statements everywhere, magic strings that drift out of sync with config, and no easy way to know what's actually live at any given moment without opening files manually.
So I built something about it. Then I built something else. Then I hadn't slept and had consumed several root beers and I had an ecosystem.
The problem with feature flags in .NET
Most feature flag libraries for .NET — including Microsoft's own Microsoft.FeatureManagement — solve the storage and evaluation problem well. But they don't solve the noise problem. You still have to wrap every call site, inject a service everywhere, and remember to clean up the if statements when a flag is retired.
LaunchDarkly and Flagsmith go further with SaaS dashboards and targeting rules, but now you've got a vendor dependency, a network call on your hot path, and a subscription fee.
What I wanted was something that felt like a natural part of the language — where toggling a feature on or off was as simple as adding or removing an attribute, and the checking happened automatically without touching every call site.
FtrIO — the core library
The idea behind FtrIO is simple: decorate a method with [Toggle] and it becomes config-gated by its own name.
[Toggle]
public void SendWelcomeEmail()
{
// send the email
}
SendWelcomeEmail(); // only runs if "SendWelcomeEmail": true in appsettings.json
That's it. No if, no injected service, no wrapper. The method calls exactly like a normal method. If the toggle is off, it just doesn't run.
How it actually works
[Toggle] is an AspectInjector aspect. At compile time, AspectInjector weaves the gating check directly into the method's IL. The gate is applied at the IL level, so it works regardless of how the method is called — direct call, reflection, delegate, anything.
Config lives in appsettings.json:
{
"Toggles": {
"SendWelcomeEmail": true,
"NewCheckoutFlow": false
}
}
Compile-time validation
The thing I'm most proud of: FtrIO ships a Roslyn analyzer that catches missing config entries at build time.
If you decorate a method with [Toggle] but forget to add the matching entry to appsettings.json, the build fails with FTRIO001 — not a silent runtime failure, not a logged warning, a proper compiler error:
error FTRIO001: 'NewCheckoutFlow' is decorated with [Toggle] but has no entry in Toggles
No other feature flag library I know of does this, at any price.
Async support
For async methods, [ToggleAsync] returns Task.CompletedTask or Task.FromResult(default) when the toggle is off — always safely awaitable:
[ToggleAsync]
public async Task SendWelcomeEmailAsync()
{
await emailClient.SendAsync(...);
}
await SendWelcomeEmailAsync(); // safely awaitable whether the toggle is on or off
Beyond true/false — strategy-based decisions
FtrIO isn't limited to boolean toggles. StrategyToggleParser routes raw config values through a chain of strategies:
{
"Toggles": {
"NewCheckout": "20%",
"PaymentV2": "blue"
}
}
ToggleParserProvider.Configure(new StrategyToggleParser(
new PercentageRolloutStrategy(), // "20%" runs ~1 in 5 calls
new BlueGreenStrategy("blue", "blue", "green") // runs only on the blue slot
));
Percentage rollouts, blue-green deployment switching, or any custom logic you implement via IToggleDecisionStrategy.
Dynamic providers
For toggle state driven by external sources — HTTP endpoints, Azure App Config, environment variables — FtrIO uses a provider pipeline that writes into appsettings.json in the background. The read path always comes from the file.
This means if a remote provider goes offline, the last known state serves automatically from disk. No fallback code, no circuit breaker, no stale-cache TTL to configure.
dotnet add package FtrIO.Providers.Http
dotnet add package FtrIO.Providers.AzureAppConfig
Installation
dotnet add package FtrIO
Targets .NET 6, 8, and 10.
FtrIO.Toaster — the management UI
Once FtrIO was in use, I had a new problem: flipping toggles meant editing appsettings.json by hand. Across multiple environments. That got old quickly.
So I built FtrIO.Toaster — a lightweight Dockerized web UI for managing toggles live.
The name has two origins: toast is binary (toasted or not, much like a feature toggle), and it's a nod to the Dungeon Master who runs our D&D sessions. Every good campaign needs someone deciding what's enabled and what isn't.
What it does
- Boolean on/off toggles
- Percentage rollout — slider and number input in sync
- Blue/green deployment switching
- Change toggle type at any time
- Add and delete toggles
- Multi-environment support — manage any number of environments from a single UI instance via a dropdown
- Audit log — every change recorded with timestamp, environment, toggle key, old value, new value, and the acting user
- Basic Auth built in, with an OAuth2 Proxy sidecar option for SSO (Google, GitHub, Microsoft, GitLab, OIDC)
How it fits with FtrIO core
Toaster implements FtrIO's own ToggleProviderBuffer internally. Changes are staged in memory and flushed atomically to appsettings.json on the configured interval — exactly as a native FtrIO provider would. Your running app picks up changes via ReloadOnChange with no restart.
Getting started
No clone required — pull from Docker Hub:
services:
toaster:
image: thescottbot/ftrio:latest
ports:
- "8000:8000"
environment:
APP_NAME: "My Application"
APPSETTINGS_PATH: /data/appsettings.json
volumes:
- type: bind
source: /path/to/your/appsettings.json
target: /data/appsettings.json
restart: unless-stopped
docker compose up -d
Open http://localhost:8000.
FtrIO.onetwo — the audit CLI
The third problem: as the codebase grew, there was no easy way to know what was actually live right now without opening appsettings.json and cross-referencing it with the source code manually.
FtrIO.onetwo is a .NET global tool that does that cross-referencing for you:
dotnet tool install -g FtrIO.onetwo
ftrio.onetwo --source C:\Projects\MyApp
It walks your source tree, finds every [Toggle], [ToggleAsync], and manual call, and outputs a table:
── Staging C:\Projects\MyApp\appsettings.Staging.json
╭──────────────────┬──────────────────┬────────────┬─────────┬───────────────────┬──────╮
│ Toggle Key │ Method │ Source │ State │ File │ Line │
├──────────────────┼──────────────────┼────────────┼─────────┼───────────────────┼──────┤
│ NewCheckoutFlow │ NewCheckoutFlow │ [Toggle] │ 50% │ Services\Order.cs │ 9 │
│ PaymentV2 │ PaymentV2 │ [Toggle] │ BLUE │ Services\Pay.cs │ 6 │
│ SendWelcomeEmail │ SendWelcomeEmail │ [Toggle] │ ON │ Services\Email.cs │ 22 │
│ UnknownFeature │ UnknownFeature │ ManualCall │ MISSING │ Controllers\Ho... │ 42 │
╰──────────────────┴──────────────────┴────────────┴─────────┴───────────────────┴──────╯
4 toggle(s). 1 ON, 0 OFF, 1 PERCENTAGE, 1 BLUE/GREEN, 1 MISSING.
The MISSING state is the one I find most useful — it catches toggles that exist in code but have no config entry, across every environment, before they cause a silent runtime failure. No other feature flag tooling I know of does this.
Supports --env Staging to target a specific environment overlay, and --markdown to write the output to a markdown file for documentation or CI artefacts.
How they fit together
All three tools share appsettings.json as the single source of truth. No coupling between them — use any combination without changing your call sites:
┌─────────────────────────────────────────────────────┐
│ Your code │
│ [Toggle] public void SendWelcomeEmail() { ... } │
└───────────────────┬─────────────────────────────────┘
│ compile-time weaving
▼
┌─────────────────────────────────────────────────────┐
│ FtrIO core │
│ gates method execution at runtime │
└───────────────────┬─────────────────────────────────┘
│ reads
▼
┌─────────────────────────────────────────────────────┐
│ appsettings.json — source of truth │
└──────────┬──────────────────────────┬───────────────┘
│ writes live │ reads & audits
▼ ▼
FtrIO.Toaster FtrIO.onetwo
(web UI — manage toggles) (CLI — audit state)
How it compares
| FtrIO | LaunchDarkly | Microsoft.FeatureManagement | Flagsmith | |
|---|---|---|---|---|
| Call-site syntax |
[Toggle] attribute, zero noise |
SDK call at every site | if (await _fm.IsEnabledAsync(...)) |
SDK call at every site |
| Works offline | ✅ always (file-backed) | ❌ needs SDK fallback config | ✅ | ❌ needs SDK fallback config |
| Compile-time validation | ✅ Roslyn analyzer | ❌ | ❌ | ❌ |
| Codebase audit / drift detection | ✅ onetwo CLI | ❌ | ❌ | ❌ |
| Management UI | ✅ Toaster, self-hosted | ✅ SaaS dashboard | ❌ | ✅ SaaS dashboard |
| Percentage rollout | ✅ | ✅ | ✅ | ✅ |
| Self-hosted / no vendor | ✅ | ❌ paid SaaS | ✅ | ✅ (or SaaS) |
| Cost | Free, OSS | Paid SaaS | Free, OSS | Free tier / paid SaaS |
Where to find it
Everything lives under the FtrOnOff org on GitHub:
-
FtrIO (core library) —
dotnet add package FtrIO -
FtrIO.Toaster (Docker UI) —
docker compose up -d -
FtrIO.onetwo (audit CLI) —
dotnet tool install -g FtrIO.onetwo - Full docs — ftronoff.github.io/FtrIO
All free, all open source, all built on no sleep and root beer. 🍺
If you try it I'd love to know what you think — drop a comment or open an issue.
Top comments (0)