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 โŒ โŒ โŒ
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.

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 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 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()
));
Don't need automated sync? If you just want a UI to change toggle values on demand without wiring up a background provider, FtrIO.Toaster covers that. It writes directly through 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:

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
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
No coupling between tools. FtrIO.Toaster, FtrIO.onetwo, and the CI/CD actions all work against the same 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

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.

๐Ÿ”ฌ Experimental: Migration tooling

v1.1.2-experimental โ€” extends the migration commands from v1.1.1 with Microsoft.FeatureManagement support and a new 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
TargetConvention
LaunchDarklySendWelcomeEmail โ†’ send-welcome-email
FlagsmithSendWelcomeEmail โ†’ send_welcome_email
Microsoft.FeatureManagementSendWelcomeEmail โ†’ SendWelcomeEmail (unchanged)
UnleashSendWelcomeEmail โ†’ 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:

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

StageWhat catches it
Write [Toggle] without a config entryRoslyn analyzer โ€” compile time
Release with a key missing from the target envrelease-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

InputDefaultDescription
source.Directory to scan for .cs files
outputtoggles.manifest.jsonPath to write the manifest JSON
upload-artifacttrueUpload the manifest as a GitHub Actions artifact
artifact-nametoggle-manifestName of the uploaded artifact
artifact-retention-days30Days to retain the artifact
versionlatestPin a specific version of FtrIO.onetwo

Outputs

OutputDescription
manifest-pathPath to the generated manifest file
toggle-countNumber of toggle keys found
artifact-nameName 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

InputDefaultDescription
artifact-nameโ€”Name of the build artifact to download. If set, downloads it automatically.
manifesttoggles.manifest.jsonPath 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-nameProductionDisplay name for the target environment, shown in the report and annotations.
fail-on-missingtrueFail the workflow if any keys are missing.
warn-onlyfalseEmit warnings but always exit 0. Overrides fail-on-missing. Useful when introducing the check without immediately blocking deploys.
markdownrelease-check-report.mdPath to write a markdown report. Uploaded as an artifact automatically.
versionlatestPin a specific version of FtrIO.onetwo.

Outputs

OutputDescription
missing-countNumber of toggle keys missing from the target config
present-countNumber of toggle keys present in the target config
passedtrue if all keys are present, false otherwise
report-pathPath 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.