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 the only thing at every call site โ all strategy wiring lives in one startup block, not spread across your codebase. 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 | โ | โ | โ |
| CI/CD deploy gate | โ blocks deploy if toggle keys missing from target config | โ | โ | โ |
| Per-user targeting | โ
UserTargetingStrategy |
โ | โ | โ |
| Attribute-based rules | โ
AttributeRuleStrategy |
โ | โ | โ |
| A/B test assignment | โ
ABTestStrategy (deterministic) |
โ | โ ๏ธ via Percentage filter | โ |
| 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 active slot and valid slot names are read from appsettings.json โ flip the slot live without a restart:
// appsettings.json โ edit CurrentSlot to switch slots, no restart needed
{
"FtrIO": {
"ReloadOnChange": true,
"BlueGreen": { "CurrentSlot": "blue", "KnownSlots": "blue,green" }
},
"Toggles": { "PaymentV2": "blue" }
}
// startup โ reads slot config automatically
ToggleParserProvider.Configure(new StrategyToggleParser(new BlueGreenStrategy()));
[Toggle]
public void PaymentV2() { ... } // runs only when CurrentSlot matches "blue"
To switch to green: set "CurrentSlot": "green" in appsettings.json. With ReloadOnChange: true the change takes effect immediately โ no redeploy, no restart.
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()
// 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
}
}
Per-user targeting
UserTargetingStrategy enables a toggle only for a named list of users. It requires an IFtrIOContextAccessor that supplies the current user ID โ fail-closed if no user context is present.
// appsettings.json
"Toggles": { "NewDashboard": "users:alice,bob" }
ToggleParserProvider.Configure(new StrategyToggleParser(
new UserTargetingStrategy(contextAccessor)
));
The user list is case-insensitive and comma-separated. Users not in the list get the toggle OFF; unauthenticated requests are also OFF.
Attribute-based rules
AttributeRuleStrategy evaluates toggle values of the form attribute:<name> <op> <value> against attributes on the current user. Supported operators: equals, notEquals, startsWith, endsWith, contains, in, notIn.
// appsettings.json
"Toggles": {
"PremiumFeature": "attribute:plan equals premium",
"EUOnlyFeature": "attribute:country in IE,GB,DE,FR"
}
ToggleParserProvider.Configure(new StrategyToggleParser(
new AttributeRuleStrategy(contextAccessor)
));
Attributes are supplied by your IFtrIOContextAccessor implementation. The strategy is fail-closed: if the attribute is missing or there is no user context, the toggle is OFF.
A/B test assignment
ABTestStrategy provides deterministic, per-user A/B bucketing. A user's bucket is computed with SHA-256 over userId:toggleKey โ the same user always lands in the same group for a given toggle, with no stored state.
// appsettings.json
"Toggles": { "NewCheckout": "ab:50" } // 50 % of users get the feature
To reassign the entire population without changing the percentage โ for example, to start a new experiment round โ append a salt:
"NewCheckout": "ab:50:round2" // independent bucket from "ab:50"
The salt makes the hash input userId:toggleKey:round2. The same user can be in different groups across experiments with different salts. Without a user context the strategy falls back to a random per-call check (equivalent to PercentageRolloutStrategy).
Per-user overrides
TogglesOverrides in appsettings.json lets you force a specific toggle ON or OFF for a named user, unconditionally before any strategy runs. Useful for QA, support escalations, or internal dogfooding.
"TogglesOverrides": {
"NewCheckout": {
"alice": true, // always ON for alice regardless of ab:50
"bob": false // always OFF for bob regardless of ab:50
}
}
Wire up OverrideResolver alongside your strategy pipeline:
var parser = new StrategyToggleParser(
new OverrideResolver(contextAccessor, new ToggleParser()),
new UserTargetingStrategy(contextAccessor),
new AttributeRuleStrategy(contextAccessor),
new ABTestStrategy(contextAccessor)
);
๐ 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()
));
ToggleProviderBuffer on each save โ no provider pipeline required, and your app picks up the change live via ReloadOnChange.
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 |
| export-manifest-action | GitHub Action: scans source for [Toggle] keys and uploads a manifest artifact |
Source tree | toggles.manifest.json artifact |
| release-check-action | GitHub Action: validates a target appsettings.json against the manifest before deploying |
Manifest artifact + target appsettings.json |
Markdown report artifact |
appsettings.json contract โ use any combination 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.
๐ฌ Experimental: Migration tooling
eject command. Install with:
dotnet tool install --global FtrIO.onetwo --version 1.1.2-experimental
These commands have not been tested against all configurations. If you try them please open an issue with what worked, what didn't, and what your setup looks like โ see areas of interest below.
ftrio.onetwo import
Pull flag state from LaunchDarkly, Flagsmith, flagd, Microsoft.FeatureManagement, env vars, or an HTTP endpoint directly into appsettings.json. Run once to snapshot your current flag state, then migrate call sites at your own pace.
For Microsoft.FeatureManagement, reads the FeatureManagement section from a local appsettings.json and writes values into Toggles. Simple booleans are mapped directly; EnabledFor: Percentage is mapped to FtrIO's "20%" format; complex feature filters produce a warning. No API key or network access required.
ftrio.onetwo import --source microsoft.featuremanagement --file appsettings.json --config appsettings.json
ftrio.onetwo migrate
Scan your .cs files for LaunchDarkly, Flagsmith, or Microsoft.FeatureManagement SDK call patterns, cross-reference against live flag state, and generate a migration report with suggested actions. Does not modify any code.
For Microsoft.FeatureManagement, detects [FeatureGate("key")] attributes, IsEnabled("key"), and IsEnabledAsync("key") via Roslyn. [FeatureGate] โ [Toggle] is surfaced as a direct 1:1 replacement in the report. No API key required โ flag values are read from local config.
ftrio.onetwo migrate --from microsoft.featuremanagement --source ./src --markdown plan.md
ftrio.onetwo eject
Generates a complete exit report from FtrIO back to another feature flag system. Scans source for toggle references, reads current values from appsettings.json, optionally creates flags in the target system via its API, and produces a per-flag report of the code changes required โ including key normalisation, suggested call site replacements, and a ready-to-paste config block.
Supported targets: LaunchDarkly, Flagsmith, Microsoft.FeatureManagement, Unleash
# Report only โ no API calls
ftrio.onetwo eject --to launchdarkly --source ./src
# Create flags and write a report
ftrio.onetwo eject --to launchdarkly --api-key sdk-xxx --project my-project --env production --source ./src --markdown eject-report.md
# Microsoft.FeatureManagement โ no API needed, lowest-friction exit
ftrio.onetwo eject --to microsoft.featuremanagement --source ./src --markdown eject-report.md
Key normalisation
| Target | Convention |
|---|---|
| LaunchDarkly | SendWelcomeEmail โ send-welcome-email |
| Flagsmith | SendWelcomeEmail โ send_welcome_email |
| Microsoft.FeatureManagement | SendWelcomeEmail โ SendWelcomeEmail (unchanged) |
| Unleash | SendWelcomeEmail โ send-welcome-email |
Supports --dry-run, --exclude, and --markdown. Exit codes: 0 all clean, 1 flags missing or failed, 2 source/config not found, 3 API unreachable.
Help wanted โ call for testers
The migrate, import, and eject commands are experimental. The Microsoft.FeatureManagement path in particular has not been tested against a live account. If you try any of these commands please open an issue with what worked, what didn't, and what your setup looks like.
Areas of particular interest:
- Flag value edge cases โ targeting rules, multi-variate flags, percentage rollouts
- Microsoft.FeatureManagement custom feature filters beyond
Percentage - API auth flows for LaunchDarkly, Flagsmith, and Unleash eject targets
- Eject round-trip accuracy against a real project
The core ftrio.onetwo scan command and the export-manifest / release-check actions are unchanged and stable.
๐ CI/CD deployment safety
FtrIO provides two GitHub Actions that close the gap between code and production config. A missing toggle key in production throws a ToggleDoesNotExistException at runtime โ these actions catch that before it happens.
App CI pipeline Deployment pipeline
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
export-manifest-action โ release-check-action
Scans source for [Toggle] keys Downloads manifest artifact
Uploads toggles.manifest.json Fetches target appsettings.json
Checks every key is present
Blocks deploy if any are missing
The full safety net
| Stage | What catches it |
|---|---|
Write [Toggle] without a config entry | Roslyn analyzer โ compile time |
| Release with a key missing from the target env | release-check-action โ deploy time |
export-manifest-action
export-manifest-action scans your .cs files for every [Toggle], [ToggleAsync], and manual toggle call, then writes a JSON manifest of required keys and uploads it as a build artifact. The manifest contains only key names and source locations โ no config values, no secrets.
Add it to your CI workflow after build and test:
jobs:
build:
steps:
- uses: actions/checkout@v4
- run: dotnet build
- run: dotnet test
- uses: FtrOnOff/export-manifest-action@v1
with:
source: ./src
artifact-name: toggle-manifest
Inputs
| Input | Default | Description |
|---|---|---|
source | . | Directory to scan for .cs files |
output | toggles.manifest.json | Path to write the manifest JSON |
upload-artifact | true | Upload the manifest as a GitHub Actions artifact |
artifact-name | toggle-manifest | Name of the uploaded artifact |
artifact-retention-days | 30 | Days to retain the artifact |
version | latest | Pin a specific version of FtrIO.onetwo |
Outputs
| Output | Description |
|---|---|
manifest-path | Path to the generated manifest file |
toggle-count | Number of toggle keys found |
artifact-name | Name of the uploaded artifact |
release-check-action
release-check-action downloads the manifest produced by export-manifest-action and checks every key against a target appsettings.json. If any keys are missing the action fails and the deploy is blocked.
- uses: FtrOnOff/release-check-action@v1
with:
artifact-name: toggle-manifest
config-url: ${{ secrets.PRODUCTION_CONFIG_URL }}
env-name: Production
fail-on-missing: true
Inputs
| Input | Default | Description |
|---|---|---|
artifact-name | โ | Name of the build artifact to download. If set, downloads it automatically. |
manifest | toggles.manifest.json | Path to the manifest on disk. Used when the manifest is already available locally. |
config | โ | Path to the target appsettings.json. Mutually exclusive with config-url. |
config-url | โ | URL to fetch the target appsettings.json from. Mutually exclusive with config. |
config-auth-header | โ | Authorization header for config-url (e.g. Bearer my-token). |
env-name | Production | Display name for the target environment, shown in the report and annotations. |
fail-on-missing | true | Fail the workflow if any keys are missing. |
warn-only | false | Emit warnings but always exit 0. Overrides fail-on-missing. Useful when introducing the check without immediately blocking deploys. |
markdown | release-check-report.md | Path to write a markdown report. Uploaded as an artifact automatically. |
version | latest | Pin a specific version of FtrIO.onetwo. |
Outputs
| Output | Description |
|---|---|
missing-count | Number of toggle keys missing from the target config |
present-count | Number of toggle keys present in the target config |
passed | true if all keys are present, false otherwise |
report-path | Path to the markdown report |
Full deployment pipeline example
# build.yml โ runs on every push/PR
jobs:
build:
steps:
- uses: actions/checkout@v4
- run: dotnet build
- run: dotnet test
- uses: FtrOnOff/export-manifest-action@v1
with:
source: ./src
artifact-name: toggle-manifest
# deploy.yml โ runs on release
jobs:
release-check:
steps:
- uses: FtrOnOff/release-check-action@v1
with:
artifact-name: toggle-manifest
config-url: ${{ secrets.PRODUCTION_CONFIG_URL }}
env-name: Production
fail-on-missing: true
deploy:
needs: release-check
steps:
- name: Deploy
run: echo "deploying..."
The needs: release-check ensures the deploy job only runs if every toggle key is present in the production config. Missing keys fail the check and block the deploy automatically.
What missing keys look like
Warning: FtrIO [Production]: PaymentV2 MISSING Services/PaymentService.cs:88
Warning: FtrIO [Production]: BetaSearch MISSING Controllers/SearchController.cs:23
Error: FtrIO release-check: 2 toggle key(s) missing from Production config.
Add the missing keys before deploying.
๐ 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.