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.
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?
-
Zero call-site noise.
[Toggle]is woven directly into the method's IL at compile time. A toggled method looks and calls like any normal method โ noif (flags.IsEnabled(...)), no injected service, no wrapper at every call site. Remove the attribute and the method is back to normal. -
appsettings.jsonas a read-through cache. Other libraries make your app depend on an external flag service being online. FtrIO flips this: providers run in the background and write their state intoappsettings.json;ToggleParseralways reads from the file. If the remote source goes offline, the last known state is served automatically from disk โ no fallback code, no circuit breaker, no TTL to configure. -
Escape hatch built in. The same
appsettings.jsonyou already deploy works as a fully functional toggle store without any provider. Swap from a static file to Azure App Config โ or back โ without touching a single call site.
โ๏ธ 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.
<<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:
๐ 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
[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.
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.
| Scenario | What 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 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.
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();
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
}
}
| Key | Default | Description |
|---|---|---|
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:
| Exception | When 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
}
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.
| Tool | Role | Reads | Writes |
|---|---|---|---|
| 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 |
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
- Boolean on/off toggles
- Percentage rollout controls
- Blue/green deployment switching
- Multi-environment support
- Audit log with timestamps, acting user, old value, and new value
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:
| Variable | Default | Description |
|---|---|---|
APPSETTINGS_PATH | /data/appsettings.json | Path 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:
- HTTP Basic Auth โ set
AUTH_USERNAMEandAUTH_PASSWORD. Suitable for development or internal tooling. - OAuth2 Proxy โ place an OAuth2 Proxy in front of the container for SSO with Google, Microsoft, GitHub, GitLab, or any OIDC provider. The acting user's identity is then captured in the audit log.
Audit log
Every change is recorded to /log/changes.log as JSONL (one JSON object per line). Each entry captures:
- Timestamp
- Environment
- Toggle key
- Old value โ new value
- Acting user (from Basic Auth or OAuth2 Proxy identity)
How it integrates with FtrIO
Toaster writes toggle values through ToggleProviderBuffer โ the same class providers use. This means:
- Writes are atomic (tmp file โ replace) โ a crash mid-write never corrupts
appsettings.json - Your running app picks up changes via
ReloadOnChangewith no restart - The base
appsettings.jsonremains the fallback if Toaster is offline โ last known state persists
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>]
| Argument | Description |
|---|---|
--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 / -h | Show usage. |
--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
| Pattern | Use 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
| State | Meaning |
|---|---|
ON | Toggle is true or 1 in config |
OFF | Toggle is false or 0 in config |
20% | Percentage rollout โ raw value shown directly |
BLUE / GREEN | Blue-green deployment slot โ shown in uppercase |
MISSING | Key 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.
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.