惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
DataBreaches.Net
N
Netflix TechBlog - Medium
P
Proofpoint News Feed
D
Docker
J
Java Code Geeks
L
LangChain Blog
Microsoft Security Blog
Microsoft Security Blog
The GitHub Blog
The GitHub Blog
I
InfoQ
Stack Overflow Blog
Stack Overflow Blog
云风的 BLOG
云风的 BLOG
Engineering at Meta
Engineering at Meta
MongoDB | Blog
MongoDB | Blog
月光博客
月光博客
T
Tailwind CSS Blog
M
MIT News - Artificial intelligence
Blog — PlanetScale
Blog — PlanetScale
Google DeepMind News
Google DeepMind News
腾讯CDC
罗磊的独立博客
U
Unit 42
爱范儿
爱范儿
Vercel News
Vercel News
MyScale Blog
MyScale Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Compile-time feature flags in C# using IL weaving and a R...
Scott Tippett · 2026-06-24 · via DEV Community

Compile-time feature flags in C# using IL weaving and a Roslyn analyzer

Feature flags are one of those ideas that sound simple until you actually implement them at scale. The concept is clean: gate a feature behind a condition, flip the condition remotely, ship dark. The reality is messier.

After a few years of writing the same pattern everywhere, I decided to do something about it. What I ended up building changed how I think about feature flags entirely: not just the syntax, but the fundamental architecture.


The problem: two things that shouldn't be your problem

1. The if statement noise

Every feature flag in a traditional library adds ceremony at every call site:

if (await _featureManager.IsEnabledAsync("SendWelcomeEmail"))
{
    await SendWelcomeEmailAsync(user);
}

Multiply that across a real codebase and you have if statements in your service layer, your controllers, your background jobs; everywhere. The feature logic and the gating logic are tangled together. When the flag is retired you have to hunt down every call site manually. And the string "SendWelcomeEmail" can drift out of sync with the actual method name silently.

2. No compile-time safety

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


Architectural decision 1: the method IS the feature

The first design decision in FtrIO is that [Toggle] 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.

Instead of wrapping every call site in an if block, you decorate the method itself:

[Toggle]
public void SendWelcomeEmail(User user)
{
    // send the email
}

Then call it normally:

SendWelcomeEmail(user); // only executes if "SendWelcomeEmail": true in appsettings.json

No if. No injected service at the call site. No magic string. The method name is the toggle key.

When you are ready to retire the flag, you remove [Toggle]. That is the entire change. There are no call sites to update because there were never any call sites wrapping the feature. The feature logic and the gating logic were never tangled together. The method goes back to being a normal method.

This is the design goal: a feature toggle should be a single attribute on a single method. Everything else should be unchanged.

For async methods:

[ToggleAsync]
public async Task SendWelcomeEmailAsync(User user)
{
    await _emailClient.SendAsync(user.Email);
}

await SendWelcomeEmailAsync(user); // safely awaitable whether on or off

When the toggle is off, [ToggleAsync] returns Task.CompletedTask; always safely awaitable, no null reference risk.


Architectural decision 2: radical ownership

The second design decision is about where your flag data lives.

FtrIO is built from the ground up around you owning your flag data. Not as a fallback, not as an offline mode, not as an escape hatch: as the primary design. Your flag state lives in appsettings.json, alongside your application code, on your server, in your repository. It is a first-class citizen of your app, not a remote dependency.

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.

Remote provider         ToggleProviderBuffer        appsettings.json
(HTTP / Azure /    →    (stages changes,        →   (source of truth,
 env vars)              flushes atomically)          always yours)
                                                          ↓
                                                     [Toggle] method
                                                     (reads at call time)

The consequences of this are significant:

Your flag state is inspectable. Open appsettings.json and you can see exactly what every toggle is set to. No dashboard login, no API call, no SDK required. It's a text file sitting next to your code.

Your flag state is portable. It's JSON in a file you control. Copy it, back it up, diff it in git, deploy it with your app. It moves with your codebase because it lives in your codebase.

Your flag state is editable directly. In an emergency, edit appsettings.json and the change is picked up via ReloadOnChange with no restart. No external access required.

Fresh start resilience is free. On a cold start FtrIO reads whatever is in appsettings.json immediately. No connection to establish, no initialisation to wait for.

Remote providers are optional, not foundational. Use HTTP, Azure App Config, or env vars if you want remote control. Remove them and appsettings.json is still your fully functional flag store. The call sites don't change either way.

This is what radical ownership means in practice. The data lives with your app. You control it completely.


How IL weaving makes decision 1 possible

[Toggle] is not magic. It is an aspect powered by AspectInjector, a compile-time IL weaving library.

When you build your project, AspectInjector walks the compiled IL, finds every method decorated with [Toggle], and weaves a gate check directly into the method's IL, before the method body executes. The gate reads the toggle state from ToggleParser, which reads from appsettings.json. If the toggle is off, the method returns immediately. If it is on, the method body executes as normal.

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.

This is 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.


Roslyn closes the loop

The IL weaving solves the syntax problem. But there is still the question of what happens if you decorate a method with [Toggle] and forget to add the corresponding entry to appsettings.json.

Without any additional tooling, this is a silent runtime failure; the toggle evaluates as missing and the feature quietly does not run.

FtrIO ships a Roslyn analyzer, FTRIO001, that catches this at build time.

Add appsettings.json as an AdditionalFile in your .csproj:

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

Now if you write this:

[Toggle]
public void NewCheckoutFlow() { ... }

Without adding "NewCheckoutFlow" to your Toggles section in appsettings.json, the build fails:

error FTRIO001: 'NewCheckoutFlow' is decorated with [Toggle]
but has no corresponding entry in the Toggles config section.

This closes the last gap in the safety net. 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 FTRIO001 as a standard compiler error in the editor; you see it inline as you type, in the Problems view, and in the build output. No plugin required.


Strategy-based decisions beyond true/false

Honestly? This was an afterthought.

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.

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

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.

Not every feature flag is boolean. FtrIO's StrategyToggleParser routes raw config values through a chain of IToggleDecisionStrategy implementations, with BooleanStrategy always appended as the final fallback.

{
  "Toggles": {
    "NewCheckoutFlow":   "20%",
    "PaymentV2":         "blue",
    "BetaFeature":       "users:alice,bob",
    "PremiumDashboard":  "attribute:plan equals premium",
    "Experiment":        "ab:50"
  }
}

ToggleParserProvider.Configure(new StrategyToggleParser(
    new UserTargetingStrategy(accessor),
    new AttributeRuleStrategy(accessor),
    new ABTestStrategy(accessor),
    new PercentageRolloutStrategy(),
    new BlueGreenStrategy("blue", "blue", "green")
));

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

PercentageRolloutStrategy handles "20%": probabilistic, runs approximately 1 in 5 calls.

BlueGreenStrategy handles "blue" / "green": compares the config value to the current deployment slot.

UserTargetingStrategy handles "users:alice,bob": checks the current user ID against a comma-separated allow list via IFtrIOContextAccessor.

AttributeRuleStrategy handles "attribute:plan equals premium": evaluates an attribute rule against the current user context. Supports equals, notEquals, startsWith, endsWith, contains, in, notIn.

ABTestStrategy handles "ab:50": 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 ("ab:50:round2") allows population reassignment without changing the toggle key.

All of this is driven by the value in appsettings.json. The call site is always the same:

[Toggle]
public void NewCheckoutFlow() { ... }

The strategy chain is wired up once at startup. The config value determines which strategy fires. The [Toggle] attribute knows nothing about strategies.


The full picture

FtrIO is not just a library. It is an ecosystem built around the two architectural decisions above.

FtrIO.Toaster is a self-hosted Docker web UI for managing toggles live without editing files. It implements FtrIO's own ToggleProviderBuffer internally; changes are staged and flushed atomically to appsettings.json, picked up via ReloadOnChange with no restart.

FtrIO.onetwo is a .NET CLI audit tool. It scans your source tree for every [Toggle] reference, cross-references against appsettings.json, and reports the state of each toggle: ON, OFF, 20%, BLUE, AB-TEST(50%), TARGETED(alice,bob), MISSING, with file and line number. It also has experimental subcommands for importing flag state from LaunchDarkly, Flagsmith, and Microsoft.FeatureManagement, generating migration reports, and ejecting back out to another system with a structured report.

export-manifest-action and release-check-action are GitHub Actions that together gate deployments on missing toggle config. The export action scans the source tree and uploads a manifest of required toggle keys as a build artifact. The release-check action downloads the manifest and validates it against the target environment's config before deploying, blocking the deploy if any keys are missing.

The full safety net:

Stage What catches it
Write [Toggle] without config Roslyn analyzer: compile time
Deploy with key missing from target env release-check-action: deploy time

Getting started

dotnet add package FtrIO

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

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

[Toggle]
public void SendWelcomeEmail() { ... }

SendWelcomeEmail(); // runs only if "SendWelcomeEmail": true

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.


Links