Kanject.Core.Features
One source tree, several product artifacts — Free, Pro, Enterprise, a customer build, an offline variant. Declare the lock once, on the capability, and Features wires the edition constants, the service registrations, the runtime gates, and the publish checks. Edition locks decide what ships. Runtime flags decide what runs. The runtime and generator are free under the Community licence.
Install
dotnet add package Kanject.Core.Featuresdotnet add package Kanject.Core.Features.Annotations <ItemGroup> <PackageReference Include="Kanject.Core.Features" /> <PackageReference Include="Kanject.Core.Features.Annotations" PrivateAssets="all" /></ItemGroup> Define the ladder, lock a service
Mark one enum as the edition ladder, then put [LockedBehind] on anything that should only exist from a given edition upward:
using Kanject.Core.Features.Annotations.Attributes;// Declaration order is unlock order: Pro includes everything in Free.[EditionLadder]public enum Edition{ Free, Pro, Enterprise,}// A whole capability that only ships from Pro upward.[LockedBehind(Edition.Pro)]public sealed class AdvancedExportEngine : IExportEngine{ public ExportResult Export(Project project) => /* premium implementation */;} // Generated. Registers the services this edition unlocks; the rest are omitted.services.AddLockedFeatures(); In a Pro build, AdvancedExportEngine is registered as itself and as IExportEngine. In a Free build those registration lines are not emitted at all.
Publish an edition
dotnet publish -p:KanjectEdition=Free -p:PublishTrimmed=true -o ./dist/freedotnet publish -p:KanjectEdition=Pro -p:PublishTrimmed=true -o ./dist/pro The edition is a build-time fact. When KanjectEdition is not set the lowest edition is selected, so the safe default never unlocks anything by accident. Branch on the generated constants for edition-aware code that is not a service:
// Generated beside the enum. A false branch is dead code the trimmer removes.if (Editions.HasPro){ menu.Add(new AdvancedExportCommand());} Gate at runtime
For code that ships in every artifact but should not always run — a beta, a gradual rollout, a kill switch — lock a partial method behind a string flag. The generated guard lives in the method body, so every route into the method passes through it:
using Kanject.Core.Features.Annotations.Attributes;using Kanject.Core.Features.Interfaces;public sealed partial class Dashboard{ private readonly IFeatureGate _gate; public Dashboard(IFeatureGate gate) => _gate = gate; // A string means "decide at runtime". The real logic lives in {Method}Core. [LockedBehind("beta.export", Fallback = nameof(Locked))] public partial string Render(); private string RenderCore() => "high-res export"; private string Locked() => "Upgrade to export.";} Then register a gate. Each helper registers a singleton IFeatureGate, and the first registration wins:
using Kanject.Core.Features.Extensions;services.AddInMemoryFeatureGate("beta.export");// or: services.AddFeatureGate(key => myRemoteFlags.IsOn(key.Name));// or: services.AddFeatureGate<MyEntitlementGate>(); With the feature off and no Fallback declared, the guard throws FeatureLockedException.
Which lock to reach for
- A whole capability with its own service —
[LockedBehind(Edition.Pro)]on the class, thenAddLockedFeatures() - A small premium contribution inside shared code — an edition-locked
partial voidmethod the compiler elides in lower editions - The strongest boundary — move the code to its own project and include it with an edition-conditional
<LockedReference> - Shipped code that should not always run —
[LockedBehind("flag")]on apartialmethod, backed byIFeatureGate - Present in every edition but gated —
[LockedBehind(Edition.Pro, Binding = FeatureBinding.Runtime)], for an in-app upsell - Proof that code is absent —
[PrunedBelow(…)]with<VerifyEditionPruning>checking the published artifact
What ships with it
[EditionLadder]and theKanjectEditionbuild property — one ladder, one artifact per edition- Generated
Editions.Has…compile-time constants andAddLockedFeatures()service registration [LockedBehind]on classes (edition) andpartialmethods (edition or runtime flag), with an optionalFallbackIFeatureGatewith in-memory and delegate gates —AddInMemoryFeatureGate,AddFeatureGate<LockedReference>for edition-conditional project and package references[PrunedBelow]and post-publish pruning verification — absence as a CI-checkable property- Compile-time
KANFLdiagnostics, including a check for lower-edition code that injects a higher-edition service