<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Scott</title>
    <description>The latest articles on DEV Community by Scott (@chaotic_neutral_dev).</description>
    <link>https://dev.to/chaotic_neutral_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3995414%2F4be6ca29-b507-462e-ab2d-c072e82b87f2.jpg</url>
      <title>DEV Community: Scott</title>
      <link>https://dev.to/chaotic_neutral_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chaotic_neutral_dev"/>
    <language>en</language>
    <item>
      <title>Compile-time feature flags in C# using IL weaving and a Roslyn analyzer</title>
      <dc:creator>Scott</dc:creator>
      <pubDate>Tue, 23 Jun 2026 18:26:49 +0000</pubDate>
      <link>https://dev.to/chaotic_neutral_dev/compile-time-feature-flags-in-c-using-il-weaving-and-a-roslyn-analyzer-1i55</link>
      <guid>https://dev.to/chaotic_neutral_dev/compile-time-feature-flags-in-c-using-il-weaving-and-a-roslyn-analyzer-1i55</guid>
      <description>&lt;p&gt;I got fed up of writing this everywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_featureManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsEnabledAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SendWelcomeEmail"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;SendWelcomeEmailAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not because it's hard. Because it's noise. It's the same pattern, over and over, scattered through every service, every controller, every background job. The feature logic and the gating logic tangled together forever. Magic strings that can drift. Call sites to hunt down when you finally retire the flag. It's boring and it's everywhere and I decided to do something about it that probably shouldn't work but does.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem: two things that shouldn't be your problem
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The if statement noise
&lt;/h3&gt;

&lt;p&gt;Every feature flag adds ceremony at every call site. Multiply that across a real codebase and you've got if statements in places they have no business being. When the flag is retired you have to find every single one manually. And if you mistype the string key? Silent failure. Great.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. No compile-time safety
&lt;/h3&gt;

&lt;p&gt;String keys are the lingua franca of feature flag libraries. &lt;code&gt;"SendWelcomeEmail"&lt;/code&gt;, &lt;code&gt;"new-checkout-flow"&lt;/code&gt;, &lt;code&gt;"payment_v2"&lt;/code&gt;: magic strings the compiler can't validate, that can be mistyped, that go stale when you rename a method, that give you absolutely no indication at build time that anything is wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architectural decision 1: the method IS the feature
&lt;/h2&gt;

&lt;p&gt;The first thing I decided: &lt;code&gt;[Toggle]&lt;/code&gt; should be the only thing you need to add to ship a feature dark, and the only thing you need to remove to retire it.&lt;/p&gt;

&lt;p&gt;So instead of wrapping every call site in an if block, you decorate the method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Toggle&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// send the email&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then call it normally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// only executes if "SendWelcomeEmail": true in appsettings.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No if. No injected service at the call site. No magic string. The method name is the toggle key.&lt;/p&gt;

&lt;p&gt;When you want to retire the flag, you remove &lt;code&gt;[Toggle]&lt;/code&gt;. That's it. There are no call sites to update because there were never any call sites wrapping the feature. The method goes back to being a normal method.&lt;/p&gt;

&lt;p&gt;One attribute to ship dark. One attribute removal to retire. Nothing else changes.&lt;/p&gt;

&lt;p&gt;For async methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ToggleAsync&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SendWelcomeEmailAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_emailClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;SendWelcomeEmailAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// safely awaitable whether on or off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the toggle is off, &lt;code&gt;[ToggleAsync]&lt;/code&gt; returns &lt;code&gt;Task.CompletedTask&lt;/code&gt;; always safely awaitable, no null reference risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architectural decision 2: radical ownership
&lt;/h2&gt;

&lt;p&gt;The second decision: your flag data lives with your app.&lt;/p&gt;

&lt;p&gt;FtrIO is built around you owning your flag state. Not as a fallback, not as an offline mode: as the primary design. Your flag state lives in &lt;code&gt;appsettings.json&lt;/code&gt;, alongside your application code, on your server, in your repository. It is a first-class citizen of your app, not a remote dependency.&lt;/p&gt;

&lt;p&gt;If you use a remote provider, it runs in the background, polls for changes, and writes the updated state into your local file via an atomic buffer. The read path always reads from your file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Remote provider         ToggleProviderBuffer        appsettings.json
(HTTP / Azure /    →    (stages changes,        →   (source of truth,
 env vars)              flushes atomically)          always yours)
                                                          ↓
                                                     [Toggle] method
                                                     (reads at call time)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your flag state is a text file sitting next to your code. Open it and you can see exactly what every toggle is set to. Edit it in an emergency and &lt;code&gt;ReloadOnChange&lt;/code&gt; picks it up with no restart. Diff it in git, deploy it with your app, take it with you if you ever change tooling.&lt;/p&gt;

&lt;p&gt;Remote providers are optional, not foundational. Remove them and &lt;code&gt;appsettings.json&lt;/code&gt; is still your fully functional flag store. The call sites don't change either way.&lt;/p&gt;




&lt;h2&gt;
  
  
  How IL weaving makes decision 1 possible
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[Toggle]&lt;/code&gt; is an aspect powered by &lt;a href="https://github.com/pamidur/aspect-injector" rel="noopener noreferrer"&gt;AspectInjector&lt;/a&gt;, a compile-time IL weaving library. When you build your project, AspectInjector walks the compiled IL, finds every method decorated with &lt;code&gt;[Toggle]&lt;/code&gt;, and weaves a gate check directly into the method's IL, before the method body executes.&lt;/p&gt;

&lt;p&gt;The gate reads the toggle state from &lt;code&gt;ToggleParser&lt;/code&gt;, which reads from &lt;code&gt;appsettings.json&lt;/code&gt;. If the toggle is off, the method returns immediately. If it's on, the method body executes as normal.&lt;/p&gt;

&lt;p&gt;The weaving happens at compile time. At runtime there is no reflection, no proxy, no dynamic invocation; just a direct IL call. The method behaves identically to a method that was always written with the gate logic inline, except you didn't have to write it.&lt;/p&gt;

&lt;p&gt;You can see exactly what AspectInjector wove into your method using Rider's IL Viewer — Tools → IL Viewer. It's not magic. It's just IL. And it's right there for you to inspect.&lt;/p&gt;

&lt;p&gt;This is also why removing the attribute is a clean operation. There is no call site to update. The aspect is gone from the IL, the gate is gone, and the method is back to normal behaviour.&lt;/p&gt;




&lt;h2&gt;
  
  
  Roslyn closes the loop
&lt;/h2&gt;

&lt;p&gt;IL weaving handles the syntax. But what about the case where you decorate a method with &lt;code&gt;[Toggle]&lt;/code&gt; and forget to add the corresponding entry to &lt;code&gt;appsettings.json&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Without anything else, that's a silent runtime failure; the toggle evaluates as missing and the feature quietly does not run. That's not good enough.&lt;/p&gt;

&lt;p&gt;So I built a Roslyn analyzer, &lt;code&gt;FTRIO001&lt;/code&gt;, that catches this at build time.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;appsettings.json&lt;/code&gt; as an &lt;code&gt;AdditionalFile&lt;/code&gt; in your &lt;code&gt;.csproj&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;AdditionalFiles&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"appsettings.json"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now if you write this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Toggle&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;NewCheckoutFlow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without adding &lt;code&gt;"NewCheckoutFlow"&lt;/code&gt; to your &lt;code&gt;Toggles&lt;/code&gt; section, the build fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error FTRIO001: 'NewCheckoutFlow' is decorated with [Toggle]
but has no corresponding entry in the Toggles config section.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler validates the contract between your code and your config. Magic string drift is caught before it ever reaches a running application. Rider surfaces &lt;code&gt;FTRIO001&lt;/code&gt; inline as you type — no plugin required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Strategies: honestly an afterthought
&lt;/h2&gt;

&lt;p&gt;Honestly? This whole section was an afterthought.&lt;/p&gt;

&lt;p&gt;When I first started this project years ago, I only ever expected features to run or not run. On or off. That was the whole mental model. I put it down for a long time, picked it up again with fresh eyes, and realised the world is a bit more nuanced than that. Percentage rollouts. Blue-green deployments. A/B tests. Per-user targeting. These are real things real teams need.&lt;/p&gt;

&lt;p&gt;So I added them. But I want to be clear about what they are: add-ons that all ultimately boil down to the same question. Should this method execute or not? The strategies are just different ways of answering it. The call site never changes.&lt;/p&gt;

&lt;p&gt;I'm genuinely excited about where this goes next. The foundation is solid and there's a lot of interesting territory still to explore. But the core idea — one attribute, run or not run — is still what drives everything.&lt;/p&gt;

&lt;p&gt;FtrIO's &lt;code&gt;StrategyToggleParser&lt;/code&gt; routes raw config values through a chain of &lt;code&gt;IToggleDecisionStrategy&lt;/code&gt; implementations, with &lt;code&gt;BooleanStrategy&lt;/code&gt; always appended as the final fallback.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Toggles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"NewCheckoutFlow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"20%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PaymentV2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="s2"&gt;"blue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"BetaFeature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="s2"&gt;"users:alice,bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PremiumDashboard"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"attribute:plan equals premium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Experiment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;"ab:50"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ToggleParserProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StrategyToggleParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserTargetingStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AttributeRuleStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ABTestStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PercentageRolloutStrategy&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BlueGreenStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"blue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"blue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"green"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each strategy implements &lt;code&gt;CanHandle(string rawValue)&lt;/code&gt; and &lt;code&gt;ShouldExecute(string key, string rawValue)&lt;/code&gt;. The parser tries each in order; first match wins, &lt;code&gt;BooleanStrategy&lt;/code&gt; catches everything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;PercentageRolloutStrategy&lt;/code&gt;&lt;/strong&gt; handles &lt;code&gt;"20%"&lt;/code&gt;: probabilistic, runs approximately 1 in 5 calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;BlueGreenStrategy&lt;/code&gt;&lt;/strong&gt; handles &lt;code&gt;"blue"&lt;/code&gt; / &lt;code&gt;"green"&lt;/code&gt;: compares the config value to the current deployment slot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;UserTargetingStrategy&lt;/code&gt;&lt;/strong&gt; handles &lt;code&gt;"users:alice,bob"&lt;/code&gt;: checks the current user ID against a comma-separated allow list via &lt;code&gt;IFtrIOContextAccessor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;AttributeRuleStrategy&lt;/code&gt;&lt;/strong&gt; handles &lt;code&gt;"attribute:plan equals premium"&lt;/code&gt;: evaluates an attribute rule against the current user context. Supports &lt;code&gt;equals&lt;/code&gt;, &lt;code&gt;notEquals&lt;/code&gt;, &lt;code&gt;startsWith&lt;/code&gt;, &lt;code&gt;endsWith&lt;/code&gt;, &lt;code&gt;contains&lt;/code&gt;, &lt;code&gt;in&lt;/code&gt;, &lt;code&gt;notIn&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ABTestStrategy&lt;/code&gt;&lt;/strong&gt; handles &lt;code&gt;"ab:50"&lt;/code&gt;: deterministic bucketing via SHA-256 of the user ID and toggle key, mod 100. The same user always gets the same result for a given toggle. Salt support (&lt;code&gt;"ab:50:round2"&lt;/code&gt;) allows population reassignment without changing the toggle key.&lt;/p&gt;

&lt;p&gt;All of this is driven by the value in &lt;code&gt;appsettings.json&lt;/code&gt;. The call site is always the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Toggle&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;NewCheckoutFlow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The strategy chain is wired up once at startup. The config value determines which strategy fires. The &lt;code&gt;[Toggle]&lt;/code&gt; attribute knows nothing about strategies.&lt;/p&gt;




&lt;h2&gt;
  
  
  The rest of the ecosystem
&lt;/h2&gt;

&lt;p&gt;The library is the core but it's not the whole thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FtrIO.Toaster&lt;/strong&gt; is a self-hosted Docker web UI for managing toggles live. It implements FtrIO's own &lt;code&gt;ToggleProviderBuffer&lt;/code&gt; internally; changes flush atomically to &lt;code&gt;appsettings.json&lt;/code&gt; and your app picks them up via &lt;code&gt;ReloadOnChange&lt;/code&gt; with no restart. Boolean, percentage, blue-green, per-user targeting, A/B test salts, per-user overrides — all manageable from the UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FtrIO.onetwo&lt;/strong&gt; is a .NET CLI audit tool. Scans your source tree, cross-references against &lt;code&gt;appsettings.json&lt;/code&gt;, and reports the state of each toggle: &lt;code&gt;ON&lt;/code&gt;, &lt;code&gt;OFF&lt;/code&gt;, &lt;code&gt;20%&lt;/code&gt;, &lt;code&gt;BLUE&lt;/code&gt;, &lt;code&gt;AB-TEST(50%)&lt;/code&gt;, &lt;code&gt;TARGETED(alice,bob)&lt;/code&gt;, &lt;code&gt;MISSING&lt;/code&gt;, with file and line number. Also has experimental subcommands for importing flag state from LaunchDarkly, Flagsmith, and Microsoft.FeatureManagement, generating migration reports, and ejecting back out to another system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;export-manifest-action and release-check-action&lt;/strong&gt; are GitHub Actions that together gate deployments on missing toggle config. The full safety net:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;What catches it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Write &lt;code&gt;[Toggle]&lt;/code&gt; without config&lt;/td&gt;
&lt;td&gt;Roslyn analyzer: compile time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy with key missing from target env&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;release-check-action&lt;/code&gt;: deploy time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package FtrIO
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"AspectInjector"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.9.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"FtrIO"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ReloadOnChange"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Toggles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SendWelcomeEmail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"NewCheckoutFlow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Toggle&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// runs only if "SendWelcomeEmail": true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else is opt-in. Toaster when you want a UI. onetwo when you want audit tooling. Strategies when you need targeting. GitHub Actions when you want deployment safety. You never need all of it.&lt;/p&gt;

&lt;p&gt;There's a lot more you can do with IL weaving and Roslyn than people realise. I built a feature flag library with it. What are you going to build?&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub org: &lt;a href="https://github.com/FtrOnOff" rel="noopener noreferrer"&gt;https://github.com/FtrOnOff&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://docs.ftrio.dev" rel="noopener noreferrer"&gt;https://docs.ftrio.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NuGet: &lt;a href="https://www.nuget.org/packages/FtrIO" rel="noopener noreferrer"&gt;https://www.nuget.org/packages/FtrIO&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>architecture</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Already using LaunchDarkly or Flagsmith? Here's how to try FtrIO on a single flag</title>
      <dc:creator>Scott</dc:creator>
      <pubDate>Sun, 21 Jun 2026 18:44:13 +0000</pubDate>
      <link>https://dev.to/chaotic_neutral_dev/already-using-launchdarkly-or-flagsmith-heres-how-to-try-ftrio-on-a-single-flag-54no</link>
      <guid>https://dev.to/chaotic_neutral_dev/already-using-launchdarkly-or-flagsmith-heres-how-to-try-ftrio-on-a-single-flag-54no</guid>
      <description>&lt;h1&gt;
  
  
  Thinking about trying FtrIO? The new CLI makes it easy to start with just one feature flag
&lt;/h1&gt;

&lt;p&gt;If you've been curious about FtrIO (the .NET feature toggle library that replaces &lt;code&gt;if (featureFlags.IsEnabled(...))&lt;/code&gt; with a &lt;code&gt;[Toggle]&lt;/code&gt; attribute woven directly into your compiled IL) but weren't sure where to start, the experimental release of FtrIO.onetwo just made that first step a lot smaller.&lt;/p&gt;

&lt;p&gt;You don't need to commit to a full migration. You don't need to rip out your existing flag library. You just need one method and 20 minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  What FtrIO.onetwo does
&lt;/h2&gt;

&lt;p&gt;FtrIO.onetwo is a .NET CLI audit tool. Its default mode scans your source tree, finds every FtrIO toggle reference, and tells you exactly what's live right now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt; FtrIO.onetwo &lt;span class="nt"&gt;--version&lt;/span&gt; 1.1.1-experimental
ftrio.onetwo &lt;span class="nt"&gt;--source&lt;/span&gt; C:&lt;span class="se"&gt;\P&lt;/span&gt;rojects&lt;span class="se"&gt;\M&lt;/span&gt;yApp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But in this experimental release it does two new things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ftrio.onetwo import&lt;/code&gt;&lt;/strong&gt;: pulls your current flag state from LaunchDarkly, Flagsmith, flagd, environment variables, or an HTTP endpoint directly into &lt;code&gt;appsettings.json&lt;/code&gt;. Your existing flag library keeps working unchanged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ftrio.onetwo migrate&lt;/code&gt;&lt;/strong&gt;: scans your &lt;code&gt;.cs&lt;/code&gt; files for LaunchDarkly or Flagsmith SDK call patterns using Roslyn, cross-references them against your live flag state, and generates a report showing exactly what each flag would look like in FtrIO and how to migrate it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The "try it on one flag" workflow
&lt;/h2&gt;

&lt;p&gt;The migrate report categorises every flag it finds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Ready to migrate&lt;/strong&gt;: boolean flag, no targeting rules, straightforward &lt;code&gt;[Toggle]&lt;/code&gt; replacement&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;Needs review&lt;/strong&gt;: targeting rules, number flags, needs a decision&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Cannot migrate&lt;/strong&gt;: JSON flags, recommend moving to &lt;code&gt;IConfiguration&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For every ready flag it shows the suggested refactor. Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;NewCheckoutFlow&lt;/span&gt;
&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="m"&gt;42&lt;/span&gt;

&lt;span class="n"&gt;Current&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BoolVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"new-checkout-flow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;ValidateCart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;ApplyDiscounts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;ProcessPayment&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;Suggested&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;Extract&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="n"&gt;named&lt;/span&gt; &lt;span class="n"&gt;NewCheckoutFlow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="n"&gt;decorate&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Toggle&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt; &lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt; &lt;span class="n"&gt;remain&lt;/span&gt; &lt;span class="n"&gt;unchanged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Toggle&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;NewCheckoutFlow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;ValidateCart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;ApplyDiscounts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;ProcessPayment&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Add&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;appsettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;"NewCheckoutFlow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick one of those ready flags. Do the refactor. Run the app. That's it: you've got FtrIO running in your codebase alongside your existing flag library, on exactly one feature, with everything else completely unchanged.&lt;/p&gt;

&lt;p&gt;If you like how it feels, pick another. If you don't, remove the &lt;code&gt;[Toggle]&lt;/code&gt; attribute and you're back to where you started.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the parent method suggestion matters
&lt;/h2&gt;

&lt;p&gt;The report nudges toward a pattern that's worth calling out explicitly: &lt;strong&gt;one named parent method per toggled feature&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BoolVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"send-welcome-email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;ValidateEmailAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;BuildEmailTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;SendViaSmtp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;LogEmailSent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FtrIO works best like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Toggle&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;ValidateEmailAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;BuildEmailTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;SendViaSmtp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;LogEmailSent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The feature has a single, named, discoverable entry point. The toggle is a gate on the feature, not scattered through its implementation. The inner methods have no idea a toggle exists. This is better code regardless of FtrIO; the migration report teaches the pattern rather than just doing a mechanical find-and-replace.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running the migration report
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# With live flag state from LaunchDarkly&lt;/span&gt;
ftrio.onetwo migrate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from&lt;/span&gt; launchdarkly &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api-key&lt;/span&gt; sdk-xxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt; my-project &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env&lt;/span&gt; production &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; C:&lt;span class="se"&gt;\P&lt;/span&gt;rojects&lt;span class="se"&gt;\M&lt;/span&gt;yApp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--markdown&lt;/span&gt; plan.md

&lt;span class="c"&gt;# Code scan only, no API key needed&lt;/span&gt;
ftrio.onetwo migrate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from&lt;/span&gt; launchdarkly &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; C:&lt;span class="se"&gt;\P&lt;/span&gt;rojects&lt;span class="se"&gt;\M&lt;/span&gt;yApp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--api-key&lt;/code&gt; is optional; without it the report still finds all your call sites and shows the suggested refactors, just with placeholder values in the &lt;code&gt;appsettings.json&lt;/code&gt; snippets instead of live flag states.&lt;/p&gt;




&lt;h2&gt;
  
  
  This is experimental
&lt;/h2&gt;

&lt;p&gt;Both &lt;code&gt;import&lt;/code&gt; and &lt;code&gt;migrate&lt;/code&gt; are in an experimental branch. To try it you need to explicitly request the experimental version — the default install will give you the stable release which doesn't include these commands yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt; FtrIO.onetwo &lt;span class="nt"&gt;--version&lt;/span&gt; 1.1.1-experimental
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Functional but not yet on a stable release. Feedback is genuinely welcome, especially on the migration report format and the flag mapping logic.&lt;/p&gt;

&lt;p&gt;The full FtrIO ecosystem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FtrIO&lt;/strong&gt;: the core library: &lt;code&gt;dotnet add package FtrIO&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FtrIO.Toaster&lt;/strong&gt;: self-hosted Docker UI for managing toggles live&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FtrIO.onetwo&lt;/strong&gt;: this CLI: &lt;code&gt;dotnet tool install -g FtrIO.onetwo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs&lt;/strong&gt;: &lt;a href="https://ftronoff.github.io/FtrIO" rel="noopener noreferrer"&gt;ftronoff.github.io/FtrIO&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/FtrOnOff" rel="noopener noreferrer"&gt;github.com/FtrOnOff&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it on a flag or two I'd love to hear how it goes; drop a comment or open an issue.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>featureflags</category>
      <category>csharp</category>
      <category>migration</category>
    </item>
    <item>
      <title>I got tired of writing ifs everywhere — so I built an ecosystem</title>
      <dc:creator>Scott</dc:creator>
      <pubDate>Sun, 21 Jun 2026 14:46:02 +0000</pubDate>
      <link>https://dev.to/chaotic_neutral_dev/i-got-tired-of-writing-ifs-everywhere-so-i-built-an-ecosystem-1koi</link>
      <guid>https://dev.to/chaotic_neutral_dev/i-got-tired-of-writing-ifs-everywhere-so-i-built-an-ecosystem-1koi</guid>
      <description>&lt;h1&gt;
  
  
  I got tired of writing &lt;code&gt;if (featureFlags.IsEnabled(...))&lt;/code&gt; everywhere — so I built an ecosystem
&lt;/h1&gt;

&lt;p&gt;I've been writing C# for over a decade and feature flags have always annoyed me in the same way.&lt;/p&gt;

&lt;p&gt;Not the concept — the concept is great. The ability to ship code dark, roll out to a percentage of traffic, or flip a switch without a redeploy is genuinely powerful. What annoyed me was the noise. Every time I added a feature flag I'd end up with this scattered through the codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;featureFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SendWelcomeEmail"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multiply that across a real codebase and you've got &lt;code&gt;if&lt;/code&gt; statements everywhere, magic strings that drift out of sync with config, and no easy way to know what's actually live at any given moment without opening files manually.&lt;/p&gt;

&lt;p&gt;So I built something about it. Then I built something else. Then I hadn't slept and had consumed several root beers and I had an ecosystem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with feature flags in .NET
&lt;/h2&gt;

&lt;p&gt;Most feature flag libraries for .NET — including Microsoft's own &lt;code&gt;Microsoft.FeatureManagement&lt;/code&gt; — solve the storage and evaluation problem well. But they don't solve the noise problem. You still have to wrap every call site, inject a service everywhere, and remember to clean up the &lt;code&gt;if&lt;/code&gt; statements when a flag is retired.&lt;/p&gt;

&lt;p&gt;LaunchDarkly and Flagsmith go further with SaaS dashboards and targeting rules, but now you've got a vendor dependency, a network call on your hot path, and a subscription fee.&lt;/p&gt;

&lt;p&gt;What I wanted was something that felt like a natural part of the language — where toggling a feature on or off was as simple as adding or removing an attribute, and the checking happened automatically without touching every call site.&lt;/p&gt;




&lt;h2&gt;
  
  
  FtrIO — the core library
&lt;/h2&gt;

&lt;p&gt;The idea behind FtrIO is simple: decorate a method with &lt;code&gt;[Toggle]&lt;/code&gt; and it becomes config-gated by its own name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Toggle&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// send the email&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// only runs if "SendWelcomeEmail": true in appsettings.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No &lt;code&gt;if&lt;/code&gt;, no injected service, no wrapper. The method calls exactly like a normal method. If the toggle is off, it just doesn't run.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it actually works
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;[Toggle]&lt;/code&gt; is an &lt;a href="https://github.com/pamidur/aspect-injector" rel="noopener noreferrer"&gt;AspectInjector&lt;/a&gt; aspect. At compile time, AspectInjector weaves the gating check directly into the method's IL. The gate is applied at the IL level, so it works regardless of how the method is called — direct call, reflection, delegate, anything.&lt;/p&gt;

&lt;p&gt;Config lives in &lt;code&gt;appsettings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Toggles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SendWelcomeEmail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"NewCheckoutFlow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Compile-time validation
&lt;/h3&gt;

&lt;p&gt;The thing I'm most proud of: FtrIO ships a Roslyn analyzer that catches missing config entries &lt;strong&gt;at build time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you decorate a method with &lt;code&gt;[Toggle]&lt;/code&gt; but forget to add the matching entry to &lt;code&gt;appsettings.json&lt;/code&gt;, the build fails with &lt;code&gt;FTRIO001&lt;/code&gt; — not a silent runtime failure, not a logged warning, a proper compiler error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error FTRIO001: 'NewCheckoutFlow' is decorated with [Toggle] but has no entry in Toggles
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No other feature flag library I know of does this, at any price.&lt;/p&gt;

&lt;h3&gt;
  
  
  Async support
&lt;/h3&gt;

&lt;p&gt;For async methods, &lt;code&gt;[ToggleAsync]&lt;/code&gt; returns &lt;code&gt;Task.CompletedTask&lt;/code&gt; or &lt;code&gt;Task.FromResult(default)&lt;/code&gt; when the toggle is off — always safely awaitable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ToggleAsync&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SendWelcomeEmailAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;emailClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;SendWelcomeEmailAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// safely awaitable whether the toggle is on or off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Beyond true/false — strategy-based decisions
&lt;/h3&gt;

&lt;p&gt;FtrIO isn't limited to boolean toggles. &lt;code&gt;StrategyToggleParser&lt;/code&gt; routes raw config values through a chain of strategies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Toggles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"NewCheckout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"20%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PaymentV2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"blue"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ToggleParserProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StrategyToggleParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PercentageRolloutStrategy&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;      &lt;span class="c1"&gt;// "20%" runs ~1 in 5 calls&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BlueGreenStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"blue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"blue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"green"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// runs only on the blue slot&lt;/span&gt;
&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Percentage rollouts, blue-green deployment switching, or any custom logic you implement via &lt;code&gt;IToggleDecisionStrategy&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic providers
&lt;/h3&gt;

&lt;p&gt;For toggle state driven by external sources — HTTP endpoints, Azure App Config, environment variables — FtrIO uses a provider pipeline that writes into &lt;code&gt;appsettings.json&lt;/code&gt; in the background. The read path always comes from the file.&lt;/p&gt;

&lt;p&gt;This means if a remote provider goes offline, the last known state serves automatically from disk. No fallback code, no circuit breaker, no stale-cache TTL to configure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package FtrIO.Providers.Http
dotnet add package FtrIO.Providers.AzureAppConfig
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package FtrIO
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Targets .NET 6, 8, and 10.&lt;/p&gt;




&lt;h2&gt;
  
  
  FtrIO.Toaster — the management UI
&lt;/h2&gt;

&lt;p&gt;Once FtrIO was in use, I had a new problem: flipping toggles meant editing &lt;code&gt;appsettings.json&lt;/code&gt; by hand. Across multiple environments. That got old quickly.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;FtrIO.Toaster&lt;/strong&gt; — a lightweight Dockerized web UI for managing toggles live.&lt;/p&gt;

&lt;p&gt;The name has two origins: toast is binary (toasted or not, much like a feature toggle), and it's a nod to the Dungeon Master who runs our D&amp;amp;D sessions. Every good campaign needs someone deciding what's enabled and what isn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it does
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Boolean&lt;/strong&gt; on/off toggles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Percentage rollout&lt;/strong&gt; — slider and number input in sync&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blue/green&lt;/strong&gt; deployment switching&lt;/li&gt;
&lt;li&gt;Change toggle type at any time&lt;/li&gt;
&lt;li&gt;Add and delete toggles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-environment support&lt;/strong&gt; — manage any number of environments from a single UI instance via a dropdown&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit log&lt;/strong&gt; — every change recorded with timestamp, environment, toggle key, old value, new value, and the acting user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Basic Auth&lt;/strong&gt; built in, with an OAuth2 Proxy sidecar option for SSO (Google, GitHub, Microsoft, GitLab, OIDC)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How it fits with FtrIO core
&lt;/h3&gt;

&lt;p&gt;Toaster implements FtrIO's own &lt;code&gt;ToggleProviderBuffer&lt;/code&gt; internally. Changes are staged in memory and flushed atomically to &lt;code&gt;appsettings.json&lt;/code&gt; on the configured interval — exactly as a native FtrIO provider would. Your running app picks up changes via &lt;code&gt;ReloadOnChange&lt;/code&gt; with no restart.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting started
&lt;/h3&gt;

&lt;p&gt;No clone required — pull from Docker Hub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;toaster&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;thescottbot/ftrio:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000:8000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;APP_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;My&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Application"&lt;/span&gt;
      &lt;span class="na"&gt;APPSETTINGS_PATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/data/appsettings.json&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bind&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/path/to/your/appsettings.json&lt;/span&gt;
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/data/appsettings.json&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:8000&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  FtrIO.onetwo — the audit CLI
&lt;/h2&gt;

&lt;p&gt;The third problem: as the codebase grew, there was no easy way to know what was actually live right now without opening &lt;code&gt;appsettings.json&lt;/code&gt; and cross-referencing it with the source code manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FtrIO.onetwo&lt;/strong&gt; is a .NET global tool that does that cross-referencing for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; FtrIO.onetwo
ftrio.onetwo &lt;span class="nt"&gt;--source&lt;/span&gt; C:&lt;span class="se"&gt;\P&lt;/span&gt;rojects&lt;span class="se"&gt;\M&lt;/span&gt;yApp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It walks your source tree, finds every &lt;code&gt;[Toggle]&lt;/code&gt;, &lt;code&gt;[ToggleAsync]&lt;/code&gt;, and manual call, and outputs a table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;── Staging C:\Projects\MyApp\appsettings.Staging.json
╭──────────────────┬──────────────────┬────────────┬─────────┬───────────────────┬──────╮
│ Toggle Key       │ Method           │ Source     │  State  │ File              │ Line │
├──────────────────┼──────────────────┼────────────┼─────────┼───────────────────┼──────┤
│ NewCheckoutFlow  │ NewCheckoutFlow  │ [Toggle]   │   50%   │ Services\Order.cs │    9 │
│ PaymentV2        │ PaymentV2        │ [Toggle]   │  BLUE   │ Services\Pay.cs   │    6 │
│ SendWelcomeEmail │ SendWelcomeEmail │ [Toggle]   │   ON    │ Services\Email.cs │   22 │
│ UnknownFeature   │ UnknownFeature   │ ManualCall │ MISSING │ Controllers\Ho... │   42 │
╰──────────────────┴──────────────────┴────────────┴─────────┴───────────────────┴──────╯
4 toggle(s). 1 ON, 0 OFF, 1 PERCENTAGE, 1 BLUE/GREEN, 1 MISSING.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;MISSING&lt;/code&gt; state is the one I find most useful — it catches toggles that exist in code but have no config entry, across every environment, before they cause a silent runtime failure. No other feature flag tooling I know of does this.&lt;/p&gt;

&lt;p&gt;Supports &lt;code&gt;--env Staging&lt;/code&gt; to target a specific environment overlay, and &lt;code&gt;--markdown&lt;/code&gt; to write the output to a markdown file for documentation or CI artefacts.&lt;/p&gt;




&lt;h2&gt;
  
  
  How they fit together
&lt;/h2&gt;

&lt;p&gt;All three tools share &lt;code&gt;appsettings.json&lt;/code&gt; as the single source of truth. No coupling between them — use any combination without changing your call sites:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────┐
│  Your code                                          │
│  [Toggle] public void SendWelcomeEmail() { ... }    │
└───────────────────┬─────────────────────────────────┘
                    │ compile-time weaving
                    ▼
┌─────────────────────────────────────────────────────┐
│  FtrIO core                                         │
│  gates method execution at runtime                  │
└───────────────────┬─────────────────────────────────┘
                    │ reads
                    ▼
┌─────────────────────────────────────────────────────┐
│  appsettings.json  — source of truth                │
└──────────┬──────────────────────────┬───────────────┘
           │ writes live              │ reads &amp;amp; audits
           ▼                          ▼
  FtrIO.Toaster                 FtrIO.onetwo
  (web UI — manage toggles)     (CLI — audit state)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How it compares
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;FtrIO&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;LaunchDarkly&lt;/th&gt;
&lt;th&gt;Microsoft.FeatureManagement&lt;/th&gt;
&lt;th&gt;Flagsmith&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Call-site syntax&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;[Toggle]&lt;/code&gt; attribute, zero noise&lt;/td&gt;
&lt;td&gt;SDK call at every site&lt;/td&gt;
&lt;td&gt;&lt;code&gt;if (await _fm.IsEnabledAsync(...))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SDK call at every site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Works offline&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ always (file-backed)&lt;/td&gt;
&lt;td&gt;❌ needs SDK fallback config&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ needs SDK fallback config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compile-time validation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Roslyn analyzer&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Codebase audit / drift detection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ onetwo CLI&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Management UI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Toaster, self-hosted&lt;/td&gt;
&lt;td&gt;✅ SaaS dashboard&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ SaaS dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Percentage rollout&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Self-hosted / no vendor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ paid SaaS&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅ (or SaaS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free, OSS&lt;/td&gt;
&lt;td&gt;Paid SaaS&lt;/td&gt;
&lt;td&gt;Free, OSS&lt;/td&gt;
&lt;td&gt;Free tier / paid SaaS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Where to find it
&lt;/h2&gt;

&lt;p&gt;Everything lives under the &lt;a href="https://github.com/FtrOnOff" rel="noopener noreferrer"&gt;FtrOnOff org on GitHub&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FtrIO&lt;/strong&gt; (core library) — &lt;code&gt;dotnet add package FtrIO&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FtrIO.Toaster&lt;/strong&gt; (Docker UI) — &lt;code&gt;docker compose up -d&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FtrIO.onetwo&lt;/strong&gt; (audit CLI) — &lt;code&gt;dotnet tool install -g FtrIO.onetwo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full docs&lt;/strong&gt; — &lt;a href="https://ftronoff.github.io/FtrIO" rel="noopener noreferrer"&gt;ftronoff.github.io/FtrIO&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All free, all open source, all built on no sleep and root beer. 🍺&lt;/p&gt;

&lt;p&gt;If you try it I'd love to know what you think — drop a comment or open an issue.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>featureflags</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
