Feature Toggles for .NET

Gate method execution via appsettings.json โ€” one attribute, no if statements, no boilerplate.

NuGet version CI License
dotnet add package FtrIO
Get started View on GitHub
๐ŸŽ›๏ธ

Attribute-driven

Decorate any method with [Toggle] and it becomes config-gated automatically.

๐Ÿ”

Compile-time safety

The bundled Roslyn analyzer catches missing config entries at build time, not at runtime.

๐Ÿ“ฆ

Zero boilerplate

No wrapper classes, no service registrations, no if statements around every call site.

โšก

Async support

[ToggleAsync] and ExecuteMethodIfToggleOnAsync gate async methods safely without null Task pitfalls.

๐Ÿ”Œ

DI / custom parser

Swap in any IToggleParser via ToggleParserProvider.Configure() at startup โ€” including parsers resolved from your DI container.

๐ŸŽฒ

Strategy decisions

Percentage rollouts, blue-green slots, and custom decision logic via StrategyToggleParser โ€” no call-site changes required.

๐ŸŒ

Dynamic providers

HTTP endpoints, Azure App Config, and env vars push updates to appsettings.json via a buffered pipeline. The file is always the source of truth.

๐ŸŽฏ

Multi-target

Ships TFMs for .NET 6, 7, 8, 9 and 10 in one package.

Two paths, same call sites. Use the simple path โ€” appsettings.json + [Toggle] โ€” when toggle state is managed at deploy time. Switch to dynamic providers (HTTP, Azure App Config, env vars) when you need toggle state to change at runtime without a redeploy. Either way, [Toggle], [ToggleAsync], and ExecuteMethodIfToggleOn work identically โ€” no call-site changes needed when switching between paths.

๐Ÿ’ก Why FtrIO?

โš–๏ธ 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 โœ… FtrIO.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

Simple path โ€” appsettings.json

๐Ÿš€ Quick start

1. Install the package

dotnet add package FtrIO

2. Add AspectInjector to your consuming project

AspectInjector weaves [Toggle] IL at compile time, per project. Any project that decorates its own methods needs this:

<PackageReference Include="AspectInjector" Version="2.9.0" />

3. Create appsettings.json

{
  "Toggles": {
    "SendWelcomeEmail": true,
    "NewCheckoutFlow": false
  }
}

4. Copy it to the build output

<ItemGroup>
  <None Update="appsettings.json">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </None>
</ItemGroup>

5. Decorate and call

using FtrIO;

public class EmailService
{
    [Toggle]
    public void SendWelcomeEmail()
    {
        // runs only when "SendWelcomeEmail": true in config
    }
}

Call it like any normal method โ€” FtrIO intercepts the call via IL weaving and checks the config before the body runs. No if, no wrapper, nothing extra at the call site.

Local functions won't work. The compiler name-mangles them (e.g. <<Main>$>g__MyFunc|0_0), so name-based config resolution fails. Use real named methods on a class instead.

๐ŸŽฏ Target frameworks

One NuGet package, five targets:

.NET 6 .NET 7 .NET 8 .NET 9 .NET 10

๐Ÿ” Compile-time validation

FtrIO ships a Roslyn analyzer (FTRIO001) that catches missing config entries at build time. If you register your appsettings.json as an AdditionalFile, any [Toggle]-decorated method whose name has no matching entry in Toggles produces a compiler error โ€” the build fails rather than the method misbehaving silently at runtime.

Opt in

<ItemGroup>
  <AdditionalFiles Include="appsettings.json" />
</ItemGroup>

Without this line the analyzer is silent โ€” runtime behaviour is unchanged. The analyzer is included automatically with the NuGet package; no separate install is needed.

Example

<!-- appsettings.json has Toggles.SendWelcomeEmail but not Toggles.NewCheckoutFlow -->

[Toggle] public void SendWelcomeEmail() {}  // โœ“ fine
[Toggle] public void NewCheckoutFlow()  {}  // โœ— FTRIO001: 'NewCheckoutFlow' has no entry in Toggles

โšก Async support

[ToggleAsync] attribute

For methods that return Task or Task<T>, use [ToggleAsync] instead of [Toggle]. It gates the method by its own name against config, but handles the "off" path correctly โ€” returning Task.CompletedTask or Task.FromResult(default) rather than null, so the result is always safely awaitable.

[ToggleAsync]
public async Task SendWelcomeEmailAsync()
{
    await emailClient.SendAsync(...);
}

await SendWelcomeEmailAsync();  // safely awaitable whether the toggle is on or off
Use [ToggleAsync] for async, [Toggle] for sync. Using [Toggle] on an async method compiles fine but risks a NullReferenceException on await when the toggle is off.

ExecuteMethodIfToggleOnAsync

The manual-control equivalent for async. Accepts Func<Task> and Func<Task<TResult>> and always returns an awaitable result:

var featureToggle = new FeatureToggle<bool>();

// Gate a Task-returning method
await featureToggle.ExecuteMethodIfToggleOnAsync(
    () => emailClient.SendAsync(), "SendWelcomeEmail");

// Gate a Task<T>-returning method
var result = await featureToggle.ExecuteMethodIfToggleOnAsync(
    () => orderService.PlaceOrderAsync(), "NewCheckoutFlow");

๐Ÿ”„ Hot-reload

By default, ToggleParser reads appsettings.json once at startup. Set ReloadOnChange: true in the FtrIO section to pick up file changes on disk without a restart.

This is strongly recommended for all applications. If you are using any dynamic provider it is mandatory โ€” without it, ToggleParser reads the file once and never sees the values providers flush to it.

{
  "FtrIO": {
    "ReloadOnChange": true
  },
  "Toggles": {
    "SendWelcomeEmail": true
  }
}

When set, ToggleParser attaches a file watcher via Microsoft.Extensions.Configuration. The next call to any [Toggle]-decorated method after the file changes will reflect the updated values โ€” no restart required.

๐ŸŒ Multi-environment support

FtrIO supports unlimited environments. The right approach depends on your infrastructure:

Separate servers per environment

This is the common case and requires no special FtrIO configuration. Each server has its own appsettings.json โ€” they are completely independent. Prod server has prod toggles, staging server has staging toggles. There is no upper limit on how many environments or servers you run.

prod-server/appsettings.json    โ† production toggle state
staging-server/appsettings.json โ† staging toggle state
dev-machine/appsettings.json    โ† dev toggle state

Each deployment is fully self-contained. [Toggle] call sites read from whichever appsettings.json is local to that server โ€” nothing else to configure.

Single machine, multiple environments

When you want to share a base config and override specific keys per environment on one machine, set FtrIO:Environment in appsettings.json to activate an overlay file. ToggleParser layers appsettings.{env}.json on top โ€” env-specific values win, the base fills the gaps.

// appsettings.json
{
  "FtrIO": { "ReloadOnChange": true, "Environment": "Staging" },
  "Toggles": { "SendWelcomeEmail": true, "NewCheckout": false }
}

// appsettings.Staging.json โ€” only what differs from the base
{
  "Toggles": { "NewCheckout": "50%" }
}

When FtrIO:Environment is set, ToggleProviderBuffer also writes provider flushes to the env file โ€” the base file is never modified by providers in this mode.

Important: FtrIO deliberately ignores ASPNETCORE_ENVIRONMENT and DOTNET_ENVIRONMENT when deciding where the buffer writes. A server's own appsettings.json is its environment โ€” writing to a different file because an env var happens to be set would break single-server deployments. Only FtrIO:Environment in config triggers env-file writes.

Remote config sources

For toggle state that lives on a remote server, use a provider. It pulls from the remote source and flushes to the local appsettings.json โ€” works across any number of environments with no file management:

// Azure App Config โ€” one store, one label per environment
new AzureAppConfigToggleParser(connectionString, buffer, label: "staging");

// HTTP config server
new HttpToggleParser("https://config.internal/toggles/staging", buffer);

If the remote source goes offline, the last flushed state in appsettings.json persists automatically โ€” no fallback code needed.

๐Ÿ”Œ Custom parser / Dependency Injection

By default, [Toggle], [ToggleAsync], and ExecuteMethodIfToggleOn all use the built-in ToggleParser which reads from appsettings.json. To swap in a custom IToggleParser โ€” one that reads from a database, a feature-flag service, or a DI container โ€” call ToggleParserProvider.Configure once at application startup before any toggled methods run:

using FtrIO;
using FtrIO.Classes;

// Manual โ€” use default ToggleParser at a custom path
ToggleParserProvider.Configure(new ToggleParser());

// With Microsoft.Extensions.DependencyInjection
ToggleParserProvider.Configure(
    host.Services.GetRequiredService<IToggleParser>());

If Configure is never called, the default ToggleParser is used automatically โ€” existing consumers don't need to change anything.

ExecuteMethodIfToggleOn and ExecuteMethodIfToggleOnAsync also accept an IToggleParser directly for per-call control:

await featureToggle.ExecuteMethodIfToggleOnAsync(
    () => SendAsync(), myCustomParser, "SendWelcomeEmail");

Analyzer behaviour with a custom parser

The Roslyn analyzer checks [Toggle]-decorated methods against appsettings.json at build time. If your custom parser does not use appsettings.json, do not register it as an AdditionalFiles entry โ€” doing so will produce false FTRIO001 errors for keys that exist in your custom source but not in the file.

ScenarioWhat to do
Using the default ToggleParser with appsettings.json Add the AdditionalFiles entry to enable the analyzer
Using a custom IToggleParser Omit the AdditionalFiles entry โ€” analyzer stays silent

To silence FTRIO001 entirely regardless of parser:

<PropertyGroup>
  <NoWarn>FTRIO001</NoWarn>
</PropertyGroup>

Dynamic providers โ€” runtime toggle state

๐ŸŽฒ Strategy-based decisions

StrategyToggleParser is a drop-in replacement for ToggleParser that routes raw config values through a chain of IToggleDecisionStrategy implementations. BooleanStrategy is always appended as the final fallback so existing true/false values continue to work with no changes.

Percentage rollout

Any value ending in % is handled by PercentageRolloutStrategy. The check is probabilistic per-call โ€” a 20% rollout means roughly 1 in 5 calls execute the method body:

// appsettings.json
{ "Toggles": { "NewCheckout": "20%" } }

// startup
ToggleParserProvider.Configure(new StrategyToggleParser(new PercentageRolloutStrategy()));

[Toggle]
public void NewCheckout() { ... }  // runs ~20% of the time

Blue-green deployment

BlueGreenStrategy routes by named deployment slot. The current slot is declared at startup; the config value is the slot that should be active:

// appsettings.json
{ "Toggles": { "PaymentV2": "blue" } }

// startup โ€” currentSlot, knownSlots...
ToggleParserProvider.Configure(new StrategyToggleParser(
    new BlueGreenStrategy("blue", "blue", "green")));

[Toggle]
public void PaymentV2() { ... }  // runs only on the "blue" slot

Combining strategies

Strategies are tried in registration order โ€” the first whose CanHandle returns true wins. BooleanStrategy is always appended automatically as the final fallback.

ToggleParserProvider.Configure(new StrategyToggleParser(
    new PercentageRolloutStrategy(),
    new BlueGreenStrategy("blue", "blue", "green")
    // BooleanStrategy auto-appended โ€” handles true/false/1/0
));

Custom strategies

Implement IToggleDecisionStrategy to add any decision logic your application needs:

public class TimeWindowStrategy : IToggleDecisionStrategy
{
    public bool CanHandle(string rawValue) => rawValue.Contains(".."); // e.g. "09:00..17:00"
    public bool ShouldExecute(string key, string rawValue)
    {
        // parse the window and check the current time
    }
}

๐ŸŒ Dynamic providers

appsettings.json is always the source of truth. Providers push toggle values into a buffer; the buffer flushes to appsettings.json on a configurable interval; ToggleParser reads from appsettings.json as normal. If a provider goes offline, the last flushed state in the file persists automatically โ€” no fallback logic is needed at call sites.
PROVIDERS HttpToggleParser AzureAppConfig ToggleParser EnvironmentVariable ToggleParser Stage() ToggleProvider Buffer flush every N s appsettings.json โ€” source of truth โ€” Reload OnChange ToggleParser [Toggle] call site READ PATH provider offline โ†’ last state persists

ToggleProviderBuffer โ€” the one required piece

Everything in the provider pipeline flows through one object. Create it once at startup and pass it to every provider you use:

var buffer = new ToggleProviderBuffer();
This single line is what enables real-time toggle updates. The buffer is the bridge between every provider (which writes) and appsettings.json (which ToggleParser reads). Without it, toggle state is fixed at whatever was in the file at startup. With it, any provider can push updates that your running app sees on the next flush โ€” no restart, no redeploy.

The buffer reads FlushInterval from appsettings.json automatically, serialises all file writes so providers never race each other, and performs atomic replacement (tmp file โ†’ replace) so a crash mid-write can never corrupt the file. Call buffer.Dispose() at shutdown to flush any remaining staged changes before the process exits.

Write storms: staging uses a ConcurrentDictionary โ€” rapid successive updates to the same key collapse to the last value before flush. If a write is in progress when the timer fires, that tick is skipped; staged values accumulate for the next tick and are never dropped.

Configuration

{
  "FtrIO": {
    "ReloadOnChange": true,   // mandatory when using providers
    "FlushInterval": 5         // seconds between buffer flushes, default 5
  },
  "Toggles": {
    "SendWelcomeEmail": true
  }
}
KeyDefaultDescription
ReloadOnChange false Mandatory when using providers. Without it, ToggleParser reads the file once at startup and will never see buffer flushes.
FlushInterval 5 Seconds between buffer flushes to appsettings.json.

HTTP provider

dotnet add package FtrIO.Providers.Http

The endpoint must return a Toggles object at the root โ€” the same shape as appsettings.json:

{ "Toggles": { "SendWelcomeEmail": "true", "NewCheckout": "50%" } }
var buffer = new ToggleProviderBuffer();
new HttpToggleParser("https://flags.example.com/toggles", buffer,
    pollInterval: TimeSpan.FromSeconds(30));

ToggleParserProvider.Configure(new StrategyToggleParser(new PercentageRolloutStrategy()));

Azure App Config provider

dotnet add package FtrIO.Providers.AzureAppConfig

Keys in App Config should be prefixed with FtrIO:Toggles: so FtrIO:Toggles:SendWelcomeEmail maps to toggle key SendWelcomeEmail.

var buffer = new ToggleProviderBuffer();

// Connection string
new AzureAppConfigToggleParser("Endpoint=https://...;Id=...;Secret=...", buffer);

// Managed Identity / DefaultAzureCredential
new AzureAppConfigToggleParser(
    new Uri("https://myconfig.azconfig.io"), new DefaultAzureCredential(), buffer);

// With label filter (e.g. separate staging vs. production values)
new AzureAppConfigToggleParser(connectionString, buffer, label: "production");

Environment variable provider

Set env vars with the default FTRIO__Toggles__ prefix (double-underscore follows .NET config hierarchy conventions):

FTRIO__Toggles__SendWelcomeEmail=true
FTRIO__Toggles__NewCheckout=50%
var buffer = new ToggleProviderBuffer();
new EnvironmentVariableToggleParser(buffer);  // snapshot at startup

// Or re-snapshot periodically (e.g. Docker secrets volumes)
new EnvironmentVariableToggleParser(buffer, pollInterval: TimeSpan.FromMinutes(5));

Full wiring example

// 1. Create the buffer โ€” reads FlushInterval from appsettings.json
var buffer = new ToggleProviderBuffer();

// 2. Start providers โ€” push updates to the buffer in the background
new HttpToggleParser("https://flags.example.com/toggles", buffer);
new EnvironmentVariableToggleParser(buffer);

// 3. Configure the reader โ€” always reads from appsettings.json
ToggleParserProvider.Configure(new StrategyToggleParser(
    new PercentageRolloutStrategy(),
    new BlueGreenStrategy("blue", "blue", "green")
));

// 4. Call sites are completely unchanged
emailService.SendWelcomeEmail();

// 5. Flush remaining staged changes on shutdown
buffer.Dispose();

Multiple providers

Multiple providers writing to the same buffer work independently. Each polls its own source and stages its keys. If two providers update the same key before a flush, the last staged value wins โ€” no coordination required.

CompositeToggleParser

For cases where you want one source to override another at read time without the buffer, CompositeToggleParser chains parsers with first-wins fallthrough:

// Env var overrides appsettings.json โ€” no buffer, direct read fallthrough
ToggleParserProvider.Configure(new CompositeToggleParser(
    new EnvironmentVariableToggleParser(),  // standalone mode โ€” reads on demand
    new ToggleParser()
));

Reference

๐ŸŽฎ Manual control

ExecuteMethodIfToggleOn is available when you want explicit control โ€” passing a key name override, gating a lambda, or gating a method you can't decorate:

var toggle = new FeatureToggle<EmailService>(new EmailService());

// Key resolved from method name via [Toggle] attribute
toggle.ExecuteMethodIfToggleOn(svc => svc.SendWelcomeEmail());

// Explicit key override
toggle.ExecuteMethodIfToggleOn(svc => svc.SendWelcomeEmail(), "MyCustomKey");

โš ๏ธ Exceptions

All exceptions live in the ToggleExceptions namespace:

ExceptionWhen it's thrown
ToggleDoesNotExistException appsettings.json exists but has no entry for the requested key in the Toggles section.
ToggleParsedOutOfRangeException A Toggles entry exists but its value isn't parseable as a boolean (true / false / 1 / 0).
ToggleAttributeMissingException ExecuteMethodIfToggleOn is called without an explicit key and the method has no [Toggle] attribute to fall back on.
using ToggleExceptions;

try
{
    emailService.SendWelcomeEmail();
}
catch (ToggleDoesNotExistException)
{
    // "SendWelcomeEmail" key is missing from appsettings.json
}
catch (ToggleParsedOutOfRangeException)
{
    // The value isn't true/false/1/0
}
Async paths: all three exceptions propagate synchronously โ€” thrown before any Task is created, not wrapped in a faulted Task. A standard try/catch block catches them identically on sync and async call sites.

The FtrIO ecosystem

๐Ÿงฉ How the three tools work together

FtrIO is three tools with a single shared contract: appsettings.json is always the source of truth. Each tool has a distinct role โ€” write, gate, audit โ€” and they compose without any coupling between them.

FtrIO.Toaster web UI โ€” manage toggles ToggleProviderBuffer Stage() flush appsettings.json โ€” source of truth โ€” Your code [Toggle] gates execution ReloadOnChange FtrIO.onetwo CLI โ€” audit toggle state Source tree [Toggle] references scans reads config
ToolRoleReadsWrites
FtrIO (core) Gates method execution at runtime via compile-time IL weaving appsettings.json (via ToggleParser) โ€”
FtrIO.Toaster Web UI for managing toggle values live without file editing appsettings.json (to show current state) appsettings.json (via ToggleProviderBuffer)
FtrIO.onetwo CLI audit: cross-references code toggle usage against config Source tree + appsettings*.json Optional --markdown report
No coupling between tools. FtrIO.Toaster and FtrIO.onetwo both work against the same appsettings.json contract โ€” you can use either, both, or neither without changing your FtrIO core setup.

๐Ÿž FtrIO.Toaster

FtrIO.Toaster is a lightweight Docker-hosted web UI for managing FtrIO feature toggles. It lets you view, edit, add, and delete toggles without touching appsettings.json directly. Changes are written through ToggleProviderBuffer โ€” the same flush pipeline your app uses โ€” so updates land in appsettings.json on the next flush interval and are picked up live via ReloadOnChange.

Capabilities

Quick start

The image is published to Docker Hub. Create a compose.yml and run docker compose up -d:

services:
  toaster:
    image: thescottbot/ftrio:latest
    ports:
      - "8000:8000"
    environment:
      APPSETTINGS_PATH: /data/appsettings.json
      APP_NAME: MyApp
      # AUTH_USERNAME: admin
      # AUTH_PASSWORD: secret
    volumes:
      - /path/to/your/appsettings.json:/data/appsettings.json
      - toaster-logs:/log
volumes:
  toaster-logs:

Then open http://localhost:8000. Point the volume mount at the same appsettings.json your app reads โ€” Toaster and your app will stay in sync automatically.

To clone the repo instead (for contributors or self-building the image): git clone https://github.com/FtrOnOff/FtrIO.Toaster && cd FtrIO.Toaster && docker compose up -d.

Configuration

Toaster is configured entirely via environment variables:

VariableDefaultDescription
APPSETTINGS_PATH/data/appsettings.jsonPath to the appsettings.json file Toaster manages. Mount this from your app's volume.
APP_NAMEโ€”Display name shown in the UI header.
AUTH_USERNAMEโ€”HTTP Basic Auth username. Set both username and password to enable Basic Auth.
AUTH_PASSWORDโ€”HTTP Basic Auth password.

Authentication

Toaster supports two authentication modes:

Audit log

Every change is recorded to /log/changes.log as JSONL (one JSON object per line). Each entry captures:

How it integrates with FtrIO

Toaster writes toggle values through ToggleProviderBuffer โ€” the same class providers use. This means:

Point APPSETTINGS_PATH at the same file your app reads โ€” typically via a shared Docker volume โ€” and Toaster and your app stay automatically in sync.

1๏ธโƒฃ2๏ธโƒฃ FtrIO.onetwo

FtrIO.onetwo is a .NET CLI audit tool. It walks your project's source tree, finds every FtrIO toggle reference, cross-references each against appsettings.json, and outputs a table showing the current state โ€” file and line number included. Because FtrIO always resolves toggle state from appsettings.json at runtime, the tool gives you an instant at-a-glance view of exactly what is enabled or disabled right now without opening a single source file manually.

Installation

dotnet tool install -g FtrIO.onetwo

Available on NuGet.

Usage

FtrIO.onetwo [--source <path>] [--config <path>] [--env <name>] [--markdown <output.md>]
ArgumentDescription
--source <path>Directory to scan for toggle usage in .cs files. Defaults to the current directory.
--config <path>Directory to search for appsettings*.json files. Defaults to --source when not specified โ€” so source and config can live in entirely different locations.
--env <name>Show a single environment using the base+overlay model. Omit to show all appsettings files as separate tables.
--markdown <file>Also write the results to a markdown file at the given path.
--help / -hShow usage.
Positional shorthand: --source and --config can also be passed as positional arguments โ€” the first positional value is the source path, the second is the config path. E.g. FtrIO.onetwo "C:\Projects\MyApp" "C:\Server\configs".

What it detects

PatternUse case
[Toggle]Synchronous method gated by its own name
[ToggleAsync]Task-returning method gated by its own name
ExecuteMethodIfToggleOn(action, "key")Manual synchronous gating with an explicit key
ExecuteMethodIfToggleOnAsync(func, "key")Manual async gating with an explicit key

Toggle states

StateMeaning
ONToggle is true or 1 in config
OFFToggle is false or 0 in config
20%Percentage rollout โ€” raw value shown directly
BLUE / GREENBlue-green deployment slot โ€” shown in uppercase
MISSINGKey used in code but absent from all appsettings*.json files

Example output

Without --env, each appsettings*.json found is shown as a separate table:

Scanning C:\Projects\MyApp...

โ”€โ”€ appsettings.json
โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚ Toggle Key       โ”‚ Method           โ”‚ Source   โ”‚ State โ”‚ File              โ”‚ Line โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ NewCheckoutFlow  โ”‚ NewCheckoutFlow  โ”‚ [Toggle] โ”‚  OFF  โ”‚ Services\Order.cs โ”‚    9 โ”‚
โ”‚ SendWelcomeEmail โ”‚ SendWelcomeEmail โ”‚ [Toggle] โ”‚  ON   โ”‚ Services\Email.cs โ”‚   22 โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
2 toggle(s). 1 ON, 1 OFF, 0 PERCENTAGE, 0 BLUE/GREEN, 0 MISSING.

โ”€โ”€ Staging  (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.

With --env Staging, a single merged table is shown applying the overlay:

FtrIO.onetwo --source C:\Projects\MyApp --env Staging

Multi-environment support

Without --env, FtrIO.onetwo finds every appsettings*.json in the --config directory (defaulting to --source) and renders a separate table for each. The environment name is derived from the filename โ€” appsettings.Staging.json โ†’ Staging. Each table header includes the full path so there is no ambiguity.

With --env, the tool applies FtrIO's overlay model: the environment-specific file's values win, and the base appsettings.json fills any gaps โ€” the same resolution logic your app uses at runtime.

Note: FtrIO.onetwo deliberately ignores ASPNETCORE_ENVIRONMENT, matching FtrIO's own behaviour. Use --env on the command line to target a specific environment.

Separating source and config paths

In real-world deployments the source tree and the live config files often live in different places โ€” e.g. source on a dev machine and appsettings.json on a build output or server share. Use --config to point at the config location independently:

# Source and config in the same place (default)
FtrIO.onetwo --source C:\Projects\MyApp

# Config lives in the build output, not alongside source
FtrIO.onetwo --source C:\Projects\MyApp --config C:\Projects\MyApp\bin\Debug\net10.0

# Config on a remote share or separate server path
FtrIO.onetwo --source C:\Projects\MyApp --config C:\Server\configs --env Production

Generating a markdown report

FtrIO.onetwo --source C:\Projects\MyApp --config C:\Server\configs --env Production --markdown toggles.md

Writes the same table output to a .md file โ€” useful for including toggle state snapshots in PRs or release notes.

๐Ÿ“„ Optional config

Without providers: appsettings.json is entirely optional. If the file is absent, every toggle is treated as on โ€” nothing is gated off. You can ship without a config file and nothing breaks.

With providers: appsettings.json becomes the persistent store that ToggleProviderBuffer writes to and ToggleParser reads from. If the file doesn't exist when the first flush fires, the buffer creates it automatically โ€” so you still don't need to create it manually, but it will exist after the first provider poll. ReloadOnChange: true is mandatory in this mode so ToggleParser picks up each flush.

In both modes: if the file exists but is missing a Toggles key for a specific method, that throws ToggleDoesNotExistException โ€” a present-but-incomplete config is treated as a mistake worth surfacing.