DEV Community

Cover image for I got tired of writing ifs everywhere — so I built an ecosystem
Scott Tippett
Scott Tippett

Posted on

I got tired of writing ifs everywhere — so I built an ecosystem

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();
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode
ToggleParserProvider.Configure(new StrategyToggleParser(
    new PercentageRolloutStrategy(),      // "20%" runs ~1 in 5 calls
    new BlueGreenStrategy("blue", "blue", "green")  // runs only on the blue slot
));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Installation

dotnet add package FtrIO
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
docker compose up -d
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 docsftronoff.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)