The Features Framework
A feature flag library answers one question: is this feature enabled? It doesn't answer the question that actually matters in a real application: what happens to dependency injection when the feature is *not* enabled? That gap is where `PowerCSharp.Features` came from.

Problem
A feature flag library answers one question: is this feature enabled? It doesn't answer the question that actually matters in a real application: what happens to dependency injection when the feature is not enabled? That gap is where PowerCSharp.Features lives.
Why the obvious solutions fail
I ran into this three years ago building a CORS policy module for a library that other teams would consume without my involvement. The infrastructure team wanted to control activation through environment variables. Engineering wanted to override flags in automated tests. I wanted the same compiled binary to behave correctly in all three cases without a code change. Three reasonable requirements, and none of the standard answers covered all of them.
A boolean in an options object (AddSomething(configure => configure.EnableCors = true)) is compile-time only. Changing it means recompiling and redeploying — exactly the friction the infrastructure team was trying to avoid.
Microsoft.FeatureManagement is a real, well-built library, and it solves a narrower problem than the one I had. It gives you IsEnabledAsync("FeatureX"). It does not wire anything for you — every consumer still writes if (await featureManager.IsEnabledAsync("Cors")) { app.UseCors(); } by hand. The library provides the check; you provide the wiring. That wiring is exactly the boilerplate I was trying to eliminate, not preserve under a new name.
Conditional registration on raw configuration (if (configuration.GetValue<bool>("Features:Cors:Enabled")) services.AddCors(...)) works until you need more than one flag source with a defined precedence, a way to see what actually resolved at startup, and — critically — a guarantee that disabling a feature doesn't take its dependents down with it.
The design decision
Four things had to be true simultaneously, and none of the existing options gave me all four: a module abstraction that owns both service registration and pipeline configuration, not just a boolean; a composite flag resolver so a value can come from code, a custom provider, an environment variable, or configuration, in a defined order; a safe-off floor so a disabled feature's dependents still resolve something inert instead of null; and startup transparency — a way to see what was enabled, and why, without attaching a debugger.
That became IFeatureModule:
public interface IFeatureModule
{
string FeatureKey { get; }
int Order { get; }
void ConfigureServices(IFeatureRegistrationContext context);
void ConfigurePipeline(IFeaturePipelineContext context);
}
And a flag-resolution chain with an explicit precedence, highest first: an explicit code override, a custom IFeatureFlagProvider, an environment variable (POWERFEATURES__<KEY>__ENABLED), appsettings.json (PowerFeatures:<Key>:Enabled), and finally the feature's own default. The CompositeFeatureFlagProvider walks that chain and returns the first source that has an opinion.
Implementation
builder.Services.AddPowerFeatures(builder.Configuration, options =>
{
options.AddBuiltInFeatures();
options.ScanAssemblies(typeof(CacheFeatureModule).Assembly);
options.Override("Cache", true);
options.EnableDiagnosticsEndpoint();
});
var app = builder.Build();
app.UsePowerFeatures();
And the self-gating pattern every module follows — the engine discovers the module either way; the module decides what "off" means for its own domain:
public void ConfigureServices(IFeatureRegistrationContext context)
{
context.Services.Configure<CorsOptions>(
context.Configuration.GetSection("PowerFeatures:Cors"));
if (!context.Flags.IsEnabled(FeatureKey))
return; // safe-off: no CORS, dependents still resolve
context.Services.AddCors(options => { /* ... */ });
}
Module discovery is opt-in per assembly — nothing gets scanned unless you pass it to ScanAssemblies — and at runtime, every discovered module shows up in a structured startup log line (PowerFeature Cache [Pluggable] enabled=True source=Configuration order=100) and, if you opt in, an HTTP endpoint (/power-features, off by default) that returns the same matrix as JSON.
Trade-offs
I deliberately chose what the docs call "Model A": every discovered module's ConfigureServices runs regardless of whether the feature is enabled, and the module itself decides what to do when it isn't. The alternative — the engine skips disabled modules entirely — sounds cleaner, and it isn't, because the engine has no way to know what a module's safe-off behavior should be without understanding that module's domain. The cost of Model A is real: you can't eliminate the cost of configuration binding just because a feature is disabled. The mitigation is to defer anything actually expensive to ConfigurePipeline, which only runs for enabled features.
Reflection-based discovery also isn't free. On a cold start with fewer than five scanned assemblies, it's sub-millisecond; past twenty, it becomes measurable. If startup time matters more than convenience, options.AddModule(new MyFeatureModule()) bypasses discovery entirely for that module.
And a quieter trade-off: a module with a broken constructor doesn't crash the app — Activator.CreateInstance failing means the module is silently skipped. That's safer for production, and it means the only way to notice a misconfigured module is to actually read the startup log, not to trust that a missing feature will throw.
Production considerations
Two modules registered with the same FeatureKey resolve last-registration-wins, by design — that's the exact mechanism that makes "replace a built-in feature with your own" work (see the BuiltInFeatures article). It also means registration order matters in a way that isn't obvious from the type system; get it backwards and your custom module silently loses to the built-in one, not the other way around.
Conclusion
The honest version of this story isn't a production incident — nothing caught fire. It's three reasonable, conflicting requirements from three different teams, and the realization that every tool I reached for solved one of them at a time. Where's the line, for you, between "a boolean flag is enough" and "this needs its own engine"?
Visit the Official Website https://powercsharp.net/
NuGet: https://www.nuget.org/packages/PowerCSharp.Core
and GitHub: https://github.com/marioarce/PowerCSharp





