After 20+ years building enterprise C# systems, one pattern kept showing up as a pain point: you ship a package, and every consumer of that package inherits every behavior — enabled or not, needed or not, tested or not.
I wanted to flip that. So I built the PowerCSharp Features engine.
The Problem With Traditional Extension Methods
// Everyone gets CORS configured. No opt-out. No flag. No context.
services.AddCorsPowerCSharp();
The library decides.
The consumer accepts.
A Better Model: Module + Flag + Context
// The engine discovers this automatically from opted-in assemblies.
public class CorsFeatureModule : IFeatureModule
{
public string FeatureKey => "Cors";
public int Order => 10;
public void ConfigureServices(IFeatureRegistrationContext context)
{
if (!context.Flags.IsEnabled(FeatureKey))
{
return; // safe-off: do nothing, or register a NoOp
}
context.Services.Configure<CorsFeatureOptions>(
context.Configuration.GetSection("PowerFeatures:Cors"));
// register active CORS policy
}
public void ConfigurePipeline(IFeaturePipelineContext context)
{
context.App.UseCors("PowerCSharpCors");
}
}
The module is self-contained.
The engine calls it.
The consumer configures a flag.
The behavior follows the flag.
How Flag Resolution Works
The engine composes a composite resolver from multiple providers:
Code override → Custom IFeatureFlagProvider → Environment variables → appsettings → Feature default
This means:
-
appsettings.json→ controlled by ops - Environment variables → controlled by infrastructure/K8s
- Code override → controlled by engineering for testing
- Custom provider → controlled by your Azure App Config / AWS SSM / database
The same binary ships to all environments. Only flags differ.
What the Engine Does for You
-
Discovery — scans opted-in assemblies for
IFeatureModuleimplementations (reflection, opt-in only — no surprise scanning) - Flag resolution — composites all providers in precedence order
-
DI orchestration — calls
ConfigureServiceson every module; modules self-gate - Registry — records the resolved state of every feature (key, tier, enabled, source, version)
-
Pipeline application — calls
ConfigurePipelineinOrderfor enabled, middleware-bearing modules - Diagnostics — structured startup log + opt-in HTTP endpoint
Host Integration (The Full Picture)
// Program.cs
builder.Services.AddPowerFeatures(builder.Configuration, options =>
{
options.AddBuiltInFeatures(); // Group 1: bundled capabilities
options.ScanAssemblies(typeof(CacheFeatureModule).Assembly); // Group 2: pluggable features
options.Override("Cache", true); // engineering override
options.EnableDiagnosticsEndpoint(); // GET /power-features
});
var app = builder.Build();
app.UsePowerFeatures(); // applies enabled middleware; logs resolved matrix
Why I Chose This Design
- No reflection surprises. Nothing is scanned unless you explicitly opt-in an assembly.
-
ASP.NET Core conventions.
AddXxx/UseXxx— developers already know this pattern. - Safe-off by default. Every module registers a NoOp when disabled. Dependents always resolve.
-
Zero third-party abstractions.
PowerCSharp.Features.Abstractionshas no NuGet dependencies.
The Contracts Are Open
// Implement this to build any feature
public interface IFeatureModule
{
string FeatureKey { get; }
int Order { get; }
void ConfigureServices(IFeatureRegistrationContext context);
void ConfigurePipeline(IFeaturePipelineContext context);
}
// Implement this to build any flag source
public interface IFeatureFlagProvider
{
bool IsEnabled(string featureKey);
FeatureFlagValue GetValue(string featureKey);
}
Fork the repo, implement the interface, ship your own feature package. The engine handles the rest.
GitHub: https://github.com/marioarce/PowerCSharp
NuGet: PowerCSharp.Features / PowerCSharp.Features.Abstractions

Top comments (0)