Status: Authoring guide. A practical, step-by-step recipe for building a new PowerCSharp Feature, using the Cache family as the canonical worked example. Read
PowerCSharp.Features.Architecture.mdfirst for the conceptual model.
Use this decision tree before writing anything.
Does the feature pull a third-party library OR carry significant/complex implementation?
│
├── No → Built-in Feature (Group 1)
│ Lives in PowerCSharp.BuiltInFeatures. Runtime-flag toggled only.
│
└── Yes → Pluggable Feature (Group 2)
Its own package (or family). Two-layer gated (package + flag).
Third-party deps isolated in the package.
│
└── Are there swappable backends (e.g. BitFaster vs native)?
│
├── No → single package: PowerCSharp.Feature.<Name>
└── Yes → family:
PowerCSharp.Feature.<Name> (contracts + module)
PowerCSharp.Feature.<Name>.<Provider> (each implementation)
| Question | Built-in (G1) | Pluggable (G2) |
|---|---|---|
| Third-party dependency? | No | Yes (isolated) |
| Implementation complexity | Low | Medium/High |
| Packaging | Shared bundle | Own package/family |
| Gating | Flag only | Package + flag |
| Examples | CORS, correlation ID, security headers, sanitization, JWT wiring | Cache, Sitecore, Sentry, OpenTelemetry, AWS Secrets |
Every feature, regardless of tier, provides:
- A stable
FeatureKey— e.g."Cache". Used in config (PowerFeatures:Cache), flags, env vars, and diagnostics. - Contracts — the interfaces the host/other features depend on (e.g.
ICacheService). - Options — a
FeatureOptionsBasesubclass bound from the feature's config section. - A registration mechanism — an
IFeatureModule(auto-discovery) and/or an explicitAdd<Name>Feature()extension. - A safe-off behavior — what happens when the flag is off (skip, or register a NoOp).
- (Optional) Pipeline wiring — middleware via
ConfigurePipeline(mostly Group 1).
The Cache feature is packaged as a family to demonstrate the full framework: contracts are split into a zero-dependency abstractions package, the module/options live in the ASP.NET Core package, and each backend is isolated in its own provider package.
PowerCSharp.Feature.Cache.Abstractions → contracts + NoOp floors (NO third-party deps, netstandard2.0 + net8.0)
PowerCSharp.Feature.Cache → module + options + AddCacheFeature (ASP.NET Core, net8.0)
PowerCSharp.Feature.Cache.BitFaster → BitFaster-backed implementation (isolates BitFaster.Caching)
PowerCSharp.Feature.Cache.Disk → disk-backed LRU implementation (no third-party deps)
Dependencies: Microsoft.Extensions.Logging.Abstractions only.
Namespaces:
PowerCSharp.Feature.Cache.Abstractions—ICacheService,IDiskCacheService,CacheResult<T>, metadata types.PowerCSharp.Feature.Cache.Abstractions.Enums—CacheProvider,CacheResultReason,CacheEntryPriority.PowerCSharp.Feature.Cache.Abstractions.NoOp—NoOpCacheService,NoOpDiskCacheService.
Contracts (modeled on the source project's Infrastructure/Services/Cache/*):
using PowerCSharp.Feature.Cache.Abstractions;
public interface ICacheService
{
bool TryGet<T>(string key, out T value);
void Set<T>(string key, T value, TimeSpan? ttl = null);
void Remove(string key);
}
public interface IDiskCacheService
{
ValueTask<T?> GetAsync<T>(string key, CancellationToken ct = default);
ValueTask SetAsync<T>(string key, T value, CancellationToken ct = default);
}Options — bound from PowerFeatures:Cache:
using PowerCSharp.Feature.Cache;
using PowerCSharp.Feature.Cache.Abstractions.Enums;
public sealed class CacheFeatureOptions : FeatureOptionsBase
{
public CacheProvider Provider { get; set; } = CacheProvider.None; // variant flag drives selection
public int Capacity { get; set; } = 1000;
}
// Defined in PowerCSharp.Feature.Cache.Abstractions.Enums
public enum CacheProvider { None, BitFaster, Disk, Memory }Dependencies: PowerCSharp.Features.Abstractions + PowerCSharp.Feature.Cache.Abstractions.
Module — supports BOTH auto-discovery and explicit registration:
using PowerCSharp.Feature.Cache.Abstractions;
using PowerCSharp.Feature.Cache.Abstractions.NoOp;
using PowerCSharp.Features.Abstractions;
public sealed class CacheFeatureModule : IFeatureModule
{
public string FeatureKey => "Cache";
public int Order => 100;
public void ConfigureServices(IFeatureRegistrationContext context)
{
// Bind + validate options
var options = context.Configuration
.GetSection($"PowerFeatures:{FeatureKey}")
.Get<CacheFeatureOptions>() ?? new CacheFeatureOptions();
// Layer 2 (flag off): register NoOp so dependents always resolve safely.
if (!context.Flags.IsEnabled(FeatureKey))
{
context.Services.AddSingleton<ICacheService, NoOpCacheService>();
context.Services.AddSingleton<IDiskCacheService, NoOpDiskCacheService>();
return;
}
// Provider selection via the variant flag/option.
// NOTE: concrete provider registration lives in the provider package
// (e.g. AddCacheBitFaster). This module only wires options and the NoOp floor.
}
public void ConfigurePipeline(IFeaturePipelineContext context) { /* no middleware */ }
}Explicit extension (optional convenience):
using PowerCSharp.Feature.Cache;
public static class CacheFeatureExtensions
{
public static IServiceCollection AddCacheFeature(this IServiceCollection services, IConfiguration configuration)
=> services.Configure<CacheFeatureOptions>(configuration.GetSection("PowerFeatures:Cache"));
}Dependencies: PowerCSharp.Feature.Cache.Abstractions + BitFaster.Caching (the isolated third-party).
using PowerCSharp.Feature.Cache.Abstractions;
using PowerCSharp.Feature.Cache.BitFaster;
public static class CacheBitFasterExtensions
{
// Called by the host when it chooses the BitFaster provider, or by the module
// when Provider == BitFaster. BitFaster types are ONLY referenced here.
public static IServiceCollection AddCacheBitFaster(this IServiceCollection services, IConfiguration configuration)
{
var options = configuration.GetSection("PowerFeatures:Cache").Get<CacheFeatureOptions>()!;
services.AddLru<string, object>(b => b.WithCapacity(options.Capacity).Build());
services.AddSingleton<ICacheService, BitFasterCacheService>();
return services;
}
}The BitFaster reference exists only in this package. An app that does not reference
PowerCSharp.Feature.Cache.BitFasternever pullsBitFaster.Caching— Layer 1 isolation in action.
| Mechanism | How |
|---|---|
| Layer 1 isolation | Don't reference .BitFaster → BitFaster.Caching absent from the dependency tree. |
| Layer 2 flag | Reference it but flag off → NoOp* registered (mirrors source NoOpDiskCacheService). |
| Non-boolean flag | Provider variant (BitFaster/Disk/Memory) selects the implementation. |
| Swappable backend | A future PowerCSharp.Feature.Cache.Memory drops in without touching contracts. |
| Hybrid registration | CacheFeatureModule (auto) + AddCacheFeature/AddCacheBitFaster/AddCacheDisk (explicit). |
When a feature has dependents that always resolve a contract, register a NoOp implementation in the flag-off path so the container never fails. NoOps should:
- Implement the full contract with inert behavior (cache misses, no-ops, empty results).
- Log once at
Information/Warningthat the feature is disabled. - Live in the contracts package (no third-party deps).
If a contract is only resolved by the feature's own active code, you may skip the NoOp and simply not register anything when the flag is off.
- Add an
IFeatureModuletoPowerCSharp.BuiltInFeatureswith a uniqueFeatureKey. - Add a
FeatureOptionsBasesubclass bound fromPowerFeatures:<Key>. - Implement
ConfigureServices(andConfigurePipelineif middleware). - Honor the flag: skip or register inert behavior when disabled.
- Set a sensible
Order(middleware ordering matters). - Add XML docs on public types; add a section to the bundle README.
- Create
PowerCSharp.Feature.<Name>.Abstractions(contracts + NoOp), depending only on frameworkAbstractions(and minimal third-party deps such as logging). - Create
PowerCSharp.Feature.<Name>(module + options + registration extensions), depending onPowerCSharp.Features.Abstractions+PowerCSharp.Feature.<Name>.Abstractions. - If swappable backends: create
PowerCSharp.Feature.<Name>.<Provider>for each implementation; isolate third-party deps there. Each provider depends only onPowerCSharp.Feature.<Name>.Abstractions, not on the module package. - Provide both an
IFeatureModule(in the module package) and explicitAdd<Name>Feature()/Add<Name><Provider>()extensions. - Implement Layer 2 flag-off behavior (NoOp where needed).
- Add a per-feature version variable to
Directory.Build.props(e.g.PowerCSharpFeature<Name>Version). Share it acrossAbstractions, module, and all providers in the family. - Add a
README.mdto each package; add the feature to the catalog. - Add tests; validate isolation by building a consumer that does NOT reference the provider package.
- FeatureKey — PascalCase, stable, matches the config section (
PowerFeatures:<Key>). - Options — always extend
FeatureOptionsBase; never read raw config strings outside binding. - No magic strings — keep keys/section names as constants.
- Async —
CancellationTokenlast;ConfigureAwait(false)in library code. - Logging — typed
ILogger<T>; neverConsole.Write. - Third-party references — only ever inside a
Feature.<Name>.<Provider>package, never in contracts or the bundle. - XML docs — on all public contracts and registration extensions.
Because dependency isolation is the headline benefit, prove it:
- In the
PowerCSharp.CleanArchitecturetemplate (or a scratch consumer), reference onlyPowerCSharp.Feature.Cache+PowerCSharp.Feature.Cache.Abstractions(not.BitFaster). - Build and inspect the dependency tree (
dotnet list package --include-transitive). - Confirm
BitFaster.Cachingis absent. - Add
PowerCSharp.Feature.Cache.BitFaster, rebuild, confirm it now appears — and that toggling the flag off swaps in the NoOp at runtime. - (Optional) Prove providers can be used without the ASP.NET Core module: reference only
PowerCSharp.Feature.Cache.Abstractions+PowerCSharp.Feature.Cache.BitFasterin a console app and confirmPowerCSharp.Feature.Cache(andMicrosoft.AspNetCore.App) is not required.
PowerCSharp.Features.Architecture.md— conceptual model, package topology, gating, lifecycle.PowerCSharp.Features.FlagReference.md— flag schema, variants, provider precedence, diagnostics.