Kanject.Core.Features
3.9.2
Prefix Reserved
See the version list below for details.
dotnet add package Kanject.Core.Features --version 3.9.2
NuGet\Install-Package Kanject.Core.Features -Version 3.9.2
<PackageReference Include="Kanject.Core.Features" Version="3.9.2" />
<PackageVersion Include="Kanject.Core.Features" Version="3.9.2" />
<PackageReference Include="Kanject.Core.Features" />
paket add Kanject.Core.Features --version 3.9.2
#r "nuget: Kanject.Core.Features, 3.9.2"
#:package Kanject.Core.Features@3.9.2
#addin nuget:?package=Kanject.Core.Features&version=3.9.2
#tool nuget:?package=Kanject.Core.Features&version=3.9.2
Kanject Features — editioning, pruning, and runtime gates
Kanject Features lets one source tree produce different product artifacts: Free, Pro, Enterprise, customer-specific, offline, regional, trial, internal, and white-label builds. You declare the lock once, on the capability, and Kanject wires the edition constants, service registrations, prune points, runtime gates, diagnostics, and publish checks.
The mental model is small:
Edition locks decide what ships. Runtime flags decide what runs.
An edition value such as [LockedBehind(Edition.Pro)] is a build-time fact, so Kanject
can omit registrations, elide prune points, omit locked references, and verify the lower
artifact does not contain code marked as pruned. A string flag such as
[LockedBehind("beta.export")] is a runtime fact, so Kanject routes the call through
IFeatureGate for betas, kill switches, and entitlements.
Boundary: this is distribution hygiene and consistency, not DRM. Compile-time locking protects code you did not ship; runtime checks in a shipped binary are still bypassable by whoever owns the machine.
Design rationale lives in the proposals:
0003-feature-locking and
0004-features-aot-prune-followups.
What can you build?
Use Features when the same codebase needs different capability sets:
- Free / Pro / Enterprise desktop apps
- CLI tools with paid commands
- open-core libraries and commercial SDKs
- customer-specific enterprise builds
- white-label partner builds
- offline or air-gapped variants
- region-specific compliance builds
- internal builds with diagnostics or experimental engines
- trial builds with shipped upsell UI but absent premium engines
- beta features, kill switches, and signed entitlements for code that must ship
For example, a desktop app can publish two artifacts from the same project:
dotnet publish -p:KanjectEdition=Free -p:PublishTrimmed=true -o ./dist/free
dotnet publish -p:KanjectEdition=Pro -p:PublishTrimmed=true -o ./dist/pro
The Free artifact can keep the shared app shell and upsell UI while omitting the Pro implementation. The Pro artifact includes and registers the Pro implementation.
Why not maintain separate Free and Pro codebases?
Separate source trees look simple at first, but they drift:
- bug fixes must be ported twice
- shared UI and domain behavior diverge
- tests and release pipelines split
- edition differences become tribal knowledge
- one product eventually lags behind the other
Features keeps one source tree as the source of truth. Shared code stays shared; premium behavior is isolated behind edition locks, prune points, locked references, or runtime gates.
Use separate codebases only when the editions are truly different products. If they are the same product with different capabilities, one codebase plus edition locks is usually the cleaner architecture.
Which lock should I use?
If you are unsure, start here:
- Use an edition service lock for whole capabilities with their own service.
- Use an edition partial prune point for small premium contributions inside shared code.
- Use a runtime partial method gate for shipped code that should run only when a flag, entitlement, or kill switch allows it.
- Use locked references for the strongest artifact boundary.
- Use interceptor mode only for existing non-partial code you cannot reshape yet.
| Need | Use |
|---|---|
| Build different SKUs from one source tree | <KanjectEdition> + [EditionLadder] |
| Omit a premium service | [LockedBehind(Edition.Pro)] on the service class |
| Prune woven contribution code | edition-locked elidable partial void method |
| Strongest module isolation | edition-conditional locked project/package reference |
| Gate a beta, rollout, or kill switch | [LockedBehind("flag")] + IFeatureGate |
| Ship a premium feature present but gated | [LockedBehind(Edition.Pro, Binding = FeatureBinding.Runtime)] |
| Gate code that must ship | an entitlement-backed IFeatureGate (Kanject.Core.Features.Entitlements) |
| Prove code is absent | [PrunedBelow(...)] + <VerifyEditionPruning> |
| Keep lower editions internally consistent | KANFL003 edition graph analysis |
Integration effort
Features can be introduced incrementally. You do not need to edition the whole product in one pass.
| Codebase shape | Typical effort | Recommended path |
|---|---|---|
| New code with clean service boundaries | Low | Define [EditionLadder], lock premium services, call AddLockedFeatures(), add two CI test passes. |
| Existing code with clear premium services | Low to Medium | Lock the service classes first, fix any KANFL003 dependency graph warnings, then add pruning verification for must-be-absent code. |
| Existing code with premium logic woven into shared methods | Medium | Extract premium contributions behind edition partial prune points; keep shared orchestration unchanged. |
| Existing shipped feature that must remain present but gated | Medium | Use runtime partial method gates or entitlement-backed IFeatureGate; add fallbacks for graceful disabled behavior. |
| Existing non-partial methods that are hard to reshape | Medium to High | Use interceptor mode as a migration tool, then move stable locks to the partial method form when practical. |
| Strong artifact isolation or customer-specific modules | Medium to High | Move premium/customer code into separate projects/packages and use LockedReference. |
For an AI agent or developer integrating this into an existing codebase, the safest workflow is:
- Inventory capabilities and choose the edition ladder.
- Classify each capability as absent below an edition, present but gated, or always present.
- Prefer service locks for whole capabilities.
- Use partial prune points for small premium contributions inside shared code.
- Use runtime partial gates for shipped code controlled by flags or entitlements.
- Use interceptor mode only as a migration bridge for existing non-partial methods.
- Run the generator/analyzer tests, fix
KANFL00xdiagnostics, then add lowest/highest edition CI passes. - Add
[PrunedBelow]and publish verification only after the code is isolated enough for absence to be meaningful.
Decision tree
Do you need the code absent from lower-edition artifacts?
├─ Yes
│ ├─ Is it a whole service/capability with a DI boundary?
│ │ └─ Use [LockedBehind(Edition.X)] on the service + services.AddLockedFeatures().
│ ├─ Is it a small contribution inside shared orchestration?
│ │ └─ Use an edition-locked elidable partial void + {Method}Core.
│ ├─ Is it a large module, customer integration, or sensitive implementation?
│ │ └─ Move it to a separate project/package and use LockedReference.
│ └─ Must CI prove it is absent after publish?
│ └─ Add [PrunedBelow(...)] + <VerifyEditionPruning>true</VerifyEditionPruning>.
│
└─ No, the code may ship but should not always run
├─ Can you shape the entry point as a partial method?
│ └─ Use [LockedBehind("flag")] partial method + {Method}Core + IFeatureGate.
├─ Is the gate tied to an edition entitlement but code must ship?
│ └─ Use [LockedBehind(Edition.X, Binding = FeatureBinding.Runtime)].
├─ Is the code already non-partial and expensive to reshape?
│ └─ Use interceptor mode temporarily; avoid generic/static/method-group/unsupported call forms.
└─ Does disabled behavior need to be graceful?
└─ Add Fallback = nameof(...) with a signature-compatible fallback method.
Using Features with Packages
Features and Packages solve different parts of the same product architecture problem:
Packages decide which provider handles a port.
Features decide whether that provider/capability ships or runs.
Use them together when a package provider is edition-, customer-, region-, rollout-, or entitlement-specific. Do not combine them just to add an on/off check around one provider; use Features alone for that. The value appears when the same port has multiple providers and not every provider should be available in every artifact or runtime context.
| Scenario | Packages provides | Features adds |
|---|---|---|
| Free/Pro providers | IPackageScope routes a port to basic, stripe, enterprise, etc. |
Lower editions omit or reject premium providers. |
| Customer-specific integrations | One package interface with customer/provider implementations. | Customer builds ship only the integrations they bought. |
| Regional or compliance variants | Per-request scope selects the regional provider. | Locked references keep disallowed regional providers out of other artifacts. |
| Provider beta rollout | Scope pins a provider key/version. | Runtime flags or entitlements decide whether the beta provider can run. |
| Enterprise connector marketplace | Packages expose a stable dispatch API. | Editions and entitlements control which connectors are installed, visible, or callable. |
| Startup health | ValidatePackageGraph() proves registered package providers resolve. |
Run validation per edition so pruned providers are not accidentally required. |
Example: route the payment port through Packages, but make the Stripe v2 provider a Pro capability:
[Package<IPaymentGateway>]
[PackageProvider(id: "stripe.payment.gateway", version: 2)]
[PackageResolverKey("stripe")]
[LockedBehind(Edition.Pro)]
public sealed partial class StripeV2PaymentGateway : IPaymentGateway
{
// Pro-only provider implementation
}
Then application code can choose the provider for a unit of work:
var packageScope = PackageScopeBuilder.Create()
.Use<IPaymentGateway>("stripe", version: 2)
.Build();
In a Pro build, that provider can be registered and selected. In a Free build, the provider registration is omitted; per-edition package graph validation should catch any scope or default that still points to it.
For code that must ship but should require a license, keep the provider present and gate the callable capability at runtime:
[LockedBehind(Edition.Pro, Binding = FeatureBinding.Runtime)]
public partial Task<Receipt> CaptureAsync(Order order);
Back the runtime gate with EntitlementFeatureGate when the provider is enabled by a
server-signed entitlement rather than by build edition.
Combined decision tree
Does this capability have multiple interchangeable implementations?
├─ No
│ └─ Use Features alone.
│
└─ Yes
├─ Should callers choose a provider per request, tenant, order, or command?
│ └─ Use Packages and IPackageScope.
│
├─ Are some providers unavailable in lower editions or customer builds?
│ └─ Add Features.
│ ├─ Provider must be absent? Use [LockedBehind(Edition.X)] or LockedReference.
│ ├─ Provider may ship but needs a license? Use runtime binding / IFeatureGate.
│ └─ Provider is in rollout? Use a runtime flag plus a package version pin.
│
└─ Can a lower edition's package scope/default still point at a removed provider?
└─ Run ValidatePackageGraph() for each edition in CI or startup checks.
Do I need Native AOT?
No. Features is useful in normal .NET builds for SKU wiring, generated gates, fewer hand-written checks, runtime flag consistency, and build-time diagnostics.
AOT and trimming matter when you want the stronger guarantee: lower-edition artifacts do not contain locked code. For actual absence, use one or more of these:
PublishTrimmed=trueor Native AOT publishing- premium logic isolated behind generated prune points
- premium services whose registrations are omitted and whose types become unreferenced
- premium code isolated in assemblies omitted from lower editions
- post-publish pruning verification
In an untrimmed same-assembly build, generated registrations and call paths can be absent, but unused premium types may still exist in the DLL. Use trimming, AOT, or locked references when artifact absence matters.
Security posture
Features improves security only when it helps you not ship code. It does not turn a client-side runtime check into DRM, anti-tamper, or server-side authorization.
The strongest guarantee is absence:
Code not present in the artifact cannot be called, patched, reflected into, or reverse-
engineered from that artifact.
Use this stack when code must be absent from lower editions:
- Isolate the implementation behind an edition service lock, edition prune point, or
LockedReference. - Publish lower editions with trimming or Native AOT.
- Add
[PrunedBelow(...)]to code that must be absent. - Enable
<VerifyEditionPruning>true</VerifyEditionPruning>in CI/publish. - Run the lowest and highest edition builds as separate release gates.
What AOT honestly adds
Native AOT can improve security posture by strengthening artifact minimization. When premium code is unreachable in a lower edition, AOT/trimming can remove the implementation, metadata, and reflection surface from the published artifact. A native artifact is also less transparent than IL.
AOT does not make shipped code tamper-proof. If a runtime gate, entitlement verifier, or premium implementation is still present in the binary, a user who controls that machine can patch, bypass, or call it another way. AOT helps with absence, not with making present code unbreakable.
Runtime gates and entitlements
Runtime gates are a policy seam for code that intentionally ships:
- beta flags
- kill switches
- in-app upsell paths
- customer entitlements
- code shared across editions
They centralize decisions and produce consistent behavior, but they are not a hard security boundary in a client-owned artifact. Offline entitlement verification prevents token forgery without the private key, but the public key and verifier live in the client binary and can still be patched by the machine owner.
For high-value operations, enforce authorization server-side. Use runtime gates for local UX, consistency, and defense-in-depth — not as the only control.
Interceptor mode security boundary
Interceptor mode gates direct call sites. It is useful as a migration bridge for existing
non-partial code, but it is not equivalent to a method-body guard. Statically visible escapes
— method-group captures (var render = dashboard.Render) and the unsupported call forms — are
reported as KANFL004. Reflection, dynamic, and other fully indirect invocations reach a
method without an interceptable call site and cannot be detected at all.
Use the partial method form when a runtime lock must apply to every route into the method:
[LockedBehind("premium.export", Fallback = nameof(Locked))]
public partial ExportResult Export(Project project);
private ExportResult ExportCore(Project project) => /* premium implementation */;
private ExportResult Locked(Project project) => ExportResult.Disabled;
Because the generated gate lives in the method body, direct calls, delegate calls, reflection, and dynamic invocation all enter through the same guard.
Security checklist
| Concern | Recommended pattern |
|---|---|
| Lower edition must not contain premium code | LockedReference or edition lock + trim/AOT + [PrunedBelow] verification |
| Premium provider must be absent | Put provider in a locked project/package or lock the provider service, then validate package graph per edition |
| Feature may ship but needs license | Runtime partial gate + entitlement-backed IFeatureGate |
| Disabled path must be safe | Add an explicit fallback and test disabled behavior |
| Existing non-partial code needs temporary gating | Interceptor mode, with documented call-form limits |
| High-value action must be protected | Server-side authorization; client Features are not enough |
| Feature keys reveal sensitive strategy | Use stable non-secret keys; treat feature names as public metadata |
| Offline expiry matters | Prefer server refresh/checks for valuable capabilities; local clocks can be manipulated |
The pieces
| Piece | Responsibility |
|---|---|
[EditionLadder] |
Marks the enum that is your SKU ladder; member declaration order = unlock order. |
<KanjectEdition> |
Selects the build edition for this artifact. |
Editions.Has* |
Generated compile-time constants for edition-aware branches. |
[LockedBehind(...)] |
Locks a class or method behind an edition or runtime flag. |
[PrunedBelow(...)] |
Marks a type/member that must be absent below an edition. |
| Locked references | Include project/package references only when the selected edition unlocks them. |
IFeatureGate |
Runtime seam for string-flag locks: betas, entitlements, kill switches. |
| Generators | Emit edition constants, AddLockedFeatures(), method guards, prune points, and interceptors. |
| Analyzers/tasks | Validate ladders, dependency graphs, locked shapes, references, and publish artifacts. |
Usage
Quick start
For the common Free/Pro shape:
using Kanject.Core.Annotations.Attributes.Features;
[EditionLadder]
public enum Edition
{
Free,
Pro,
}
[LockedBehind(Edition.Pro)]
public sealed class ProExportEngine : IExportEngine
{
public ExportResult Export(Project project) => /* paid implementation */;
}
services.AddLockedFeatures();
dotnet publish -p:KanjectEdition=Free -p:PublishTrimmed=true -o ./dist/free
dotnet publish -p:KanjectEdition=Pro -p:PublishTrimmed=true -o ./dist/pro
The Pro build registers ProExportEngine; the Free build omits that registration. With
trimming/AOT and no other references, the implementation can be absent from the Free
artifact.
1. Define the edition ladder
Declare the ladder once. Member declaration order is the unlock order, so Pro includes
everything available in Free, and Enterprise includes both.
[EditionLadder]
public enum Edition
{
Free,
Pro,
Enterprise,
}
Pick the edition at build time:
<PropertyGroup>
<KanjectEdition>Pro</KanjectEdition>
</PropertyGroup>
The generator emits sibling compile-time constants:
public static class Editions
{
public const Edition Current = Edition.Pro;
public const bool HasFree = true;
public const bool HasPro = true;
public const bool HasEnterprise = false;
}
An out-of-ladder <KanjectEdition> value raises KANFL006 and falls back to the
lowest edition.
2. Lock a service behind an edition
Use edition locks for code that should only ship in higher artifacts:
[LockedBehind(Edition.Pro)]
public sealed class AdvancedExportEngine : IExportEngine
{
public ExportResult Export(Project project) => /* premium implementation */;
}
Call the generated extension once at startup:
services.AddLockedFeatures();
In a Pro build, AdvancedExportEngine is registered as itself and as IExportEngine.
In a Free build, those registration lines are omitted. If the type is otherwise
unreferenced and the app is trimmed/AOT-published, the type can be removed from the
artifact. The KANFL003 analyzer flags lower-edition consumers that inject
higher-edition services.
Lifetime defaults to Scoped:
[LockedBehind(Edition.Pro, Lifetime = LockedServiceLifetime.Singleton)]
public sealed class AdvancedExportEngine : IExportEngine { }
3. Prune woven code with an edition partial
Some premium behavior is not a clean DI service. It may be a contribution inside a planner, analyzer, formatter, or other shared pipeline. For that shape, use an elidable edition partial:
public partial class QueryPlanner
{
[LockedBehind(Edition.Pro)]
partial void ContributeProRules(RuleSet rules);
public RuleSet BuildRules()
{
var rules = new RuleSet();
ContributeProRules(rules);
return rules;
}
private void ContributeProRulesCore(RuleSet rules)
{
rules.Add(new AdvancedJoinRewriteRule());
}
}
In editions that unlock the method, Kanject emits the implementing partial that calls the real premium contribution. In lower editions, Kanject emits no implementation, so the C# compiler elides the call site and its argument evaluation.
True elision is intentionally narrow: use an implicitly private partial void with no
out parameters. Generic methods and ref/in parameters are supported when the
matching {Method}Core signature is compatible. Non-elidable or mismatched shapes are
rejected by diagnostics so a lock never silently turns into a no-op.
public partial class Planner
{
[LockedBehind(Edition.Pro)]
partial void Contribute<T>(List<T> rules) where T : class;
private void ContributeCore<T>(List<T> rules) where T : class
{
rules.Add(/* premium rule */);
}
}
4. Omit premium assemblies
For the strongest boundary, isolate premium code in a separate project or package and make the reference edition-conditional. Lower editions never reference the assembly, so there is nothing for the trimmer to prove.
<ItemGroup>
<LockedReference Include="..\App.Pro\App.Pro.csproj" MinEdition="Pro" />
</ItemGroup>
Use this for large paid modules, customer-specific integrations, or code you want categorically absent from lower artifacts.
5. Verify pruning after publish
Mark code that must be absent below an edition:
[PrunedBelow(Edition.Pro)]
public sealed class AdvancedExportEngine { }
With pruning verification enabled, Kanject inspects the publish output and fails the publish if a lower-edition artifact still contains a marked type/member, or if an unlocking artifact unexpectedly lost it.
<PropertyGroup>
<VerifyEditionPruning>true</VerifyEditionPruning>
</PropertyGroup>
This turns "the trimmer should remove it" into a CI-checkable artifact property.
6. Gate runtime features
A string argument means "decide at runtime." Put the real logic in {Method}Core, declare
the entry method as partial, and inject an IFeatureGate:
public sealed partial class Dashboard
{
private readonly IFeatureGate _gate;
public Dashboard(IFeatureGate gate) => _gate = gate;
[LockedBehind("beta.export", Fallback = nameof(Locked))]
public partial string Render();
private string RenderCore() => "high-res export";
private string Locked() => "Upgrade to export.";
}
Wire a gate:
services.AddInMemoryFeatureGate("beta.export");
// or: services.AddFeatureGate(key => myRemoteFlags.IsOn(key.Name));
// or: services.AddFeatureGate<MyEntitlementGate>();
Runtime gates are the right tool for betas, gradual rollout, emergency kill switches, in-app upsell surfaces, and entitlements for code that must ship.
The method shape is deliberately checked:
- The containing type must be a non-static, non-generic, non-nested class.
- The locked method must be an instance
partialmethod. - The containing type needs an accessible
IFeatureGatefield or property. {Method}Coremust exist and match the locked method signature.Fallback, when declared, must match the locked method signature.- Generic method type parameters,
whereconstraints, andref/inparameters are preserved. outparameters are rejected on the partial guard path; return a result object/value instead, or gate a service.
When the feature is disabled and no fallback is declared, the generated guard throws
FeatureLockedException with runtime code KANFL050.
Present but gated. To ship an edition-locked capability in every artifact but gate
it at runtime instead of pruning it (an in-app upsell, an entitlement, a kill switch), set
Binding = FeatureBinding.Runtime. The edition then behaves like a string flag keyed on
"{EnumName}.{Member}" (e.g. "Edition.Pro"):
[LockedBehind(Edition.Pro, Binding = FeatureBinding.Runtime)]
public partial string RenderUpsell(); // present in Free, gated through IFeatureGate on "Edition.Pro"
On a service class, Binding = FeatureBinding.Runtime registers it unconditionally (it is
present in every edition; KANFL003 then leaves it alone). For code that must ship,
back the gate with a server-signed entitlement from the optional
Kanject.Core.Features.Entitlements package — its EntitlementFeatureGate verifies an
offline ECDSA token and denies on any uncertainty. For the full activation protocol
(issuer + client setup, refresh, revocation, typed claims, cold-start cache, attestation),
see its setup guide.
7. Interceptor mode
<EnableFeatureLockInterceptors>true</EnableFeatureLockInterceptors> lets you write a
normal method with a real body; the generator intercepts call sites instead of requiring a
partial/Core split. The IFeatureGate member and fallback must be non-private because
the interceptor lives in a separate generated class.
Experimental. Interceptor mode uses the file-path
[InterceptsLocation]form, which is fragile to edits above a call site. Prefer the partial method form unless you need to gate existing non-partial code.
Interceptor mode is intentionally stricter than the partial method form. It only supports locked instance methods called through an explicit, non-conditional receiver:
dashboard.Render(); // supported
Render(); // rejected: no explicit receiver
dashboard?.Render(); // rejected: conditional receiver
Generic methods, static methods, unsupported call forms, method-group captures
(var render = dashboard.Render), inaccessible gate members, and signature-mismatched
fallbacks are reported as KANFL004/KANFL002 instead of being silently ignored. Those are
the statically detectable escapes; reflection and dynamic reach a method with no call site
to rewrite and cannot be detected at all. If you want every call form to be safe — or a locked
method needs to be passed as a delegate — use the partial method form, whose gate lives in the
method body.
Artifact guarantees
| Binding | Decided | Mechanism | Hot path | Code in lower artifact? |
|---|---|---|---|---|
| Edition service | Build | omitted registration + trim/AOT | zero | absent when unreferenced and trimmed/AOT |
| Edition partial prune point | Build | no implementation in lower editions | zero | absent from generated/call-site IL |
| Locked reference | Build | assembly not referenced | zero | absent |
Editions.Has* branch |
Build | const bool branch folding |
zero | absent when dead code becomes unreferenced and trimmed/AOT |
| String flag | Run | IFeatureGate.IsEnabled |
one call + branch | present, intentionally gated |
Diagnostics
Prefix KANFL; compile-time range 001-049, runtime 050+, and MSBuild build/publish + pruning verification in the 9xx band.
| Code | When | Severity |
|---|---|---|
KANFL001 |
[LockedBehind] service references an edition not on the [EditionLadder] |
Error |
KANFL002 |
A [LockedBehind] fallback is missing or signature-mismatched |
Error |
KANFL003 |
A consumer reachable at edition E injects a capability provided only above E | Warning |
KANFL004 |
A [LockedBehind("flag")] method cannot be guarded or intercepted |
Error |
KANFL005 |
More than one [EditionLadder] enum in the assembly |
Error |
KANFL006 |
<KanjectEdition> names a value not on the ladder |
Error |
KANFL007 |
An edition-locked partial method cannot be elided | Error |
KANFL008 |
MSBuild edition ladder metadata disagrees with the [EditionLadder] enum |
Error |
KANFL009 |
A [LockedBehind] lock applied in an unsupported way (e.g. a string flag on a class) |
Error |
KANFL050 |
A locked runtime method was invoked while disabled with no fallback | Runtime exception |
KANFL910/911/912 |
(MSBuild) publish without an explicit edition / a <LockedReference> with no ladder or an off-ladder MinEdition |
Error |
KANFL921 |
(MSBuild) pruning verification failed — a marked type/member is present where it should be absent (or absent where it should be present), or the artifact is missing/unreadable (fail-closed) | Error |
How to respond to common diagnostics
| Code | What to do |
|---|---|
KANFL002 |
Make Fallback an instance method with the same return type, generic arity/constraints, parameter types, and ref/in/out modifiers as the locked method. |
KANFL003 |
Either raise the consumer's required edition, lower the dependency's edition, or introduce a lower-edition implementation of the dependency. |
KANFL004 |
For runtime method gates, prefer the partial method pattern with {Method}Core; for interceptor mode, call through an explicit receiver and avoid generic/static methods. |
KANFL007 |
Keep edition prune points as implicitly private partial void methods with no out parameters. |
KANFL050 |
Enable the feature in IFeatureGate, or add a Fallback method for graceful disabled behavior. |
Release safety and testing
For product builds, enable the publish guard so release artifacts must choose an edition explicitly:
<PropertyGroup>
<RequireExplicitEditionOnPublish>true</RequireExplicitEditionOnPublish>
</PropertyGroup>
For tests, run at least two passes:
- highest edition for full correctness coverage
- lowest edition for gating, pruning, and absence verification
The Kanject.Core.Features.TestKit package's [EditionFact] / [EditionTheory] attributes
skip a test automatically when the run's edition (the KANJECT_EDITION environment variable)
does not unlock the capability under test — so one suite runs everywhere and gating no longer
breaks tests at the default edition. The two-pass CI recipe:
# Pass 1 — full edition: correctness coverage
KANJECT_EDITION=Enterprise dotnet test
# Pass 2 — lowest edition: gating + the post-publish absence gate
KANJECT_EDITION=Free dotnet test
Assert both passes ran; green on one edition is not green.
What this is not
- Not DRM or anti-tamper. A runtime check in a shipped binary is bypassable by the machine owner. The hard guarantee is absence from artifacts you chose not to ship.
- Not a replacement for architecture. Premium code still needs to be isolated into prunable units: services, elidable partial contributions, or omitted assemblies.
- Not a mandate to use AOT. AOT/trimming is how you get the strongest absence guarantee; the generated SKU wiring, diagnostics, and runtime gates are useful without it.
Contributor notes: package layout
Each package follows its canonical layout: attributes and generators are category-first;
runtime Kanject.Core is feature-first like Packages/.
Kanject.Core.Annotations.Attributes/
Features/ EditionLadderAttribute, LockedBehindAttribute, PrunedBelowAttribute
Enums/Features/ LockedServiceLifetime, FeatureBinding
Kanject.Core.Annotations/
Constants/ FeatureLockNamespaces.cs
Generators/Features/ EditionConstants, LockedServiceRegistration, LockedMethodGuard,
LockedMethodPrune, LockedMethodInterceptor, PrunedBelowManifest
Analyzers/Features/ LockedServiceGraph (KANFL003), LockedMethodGroup (KANFL004), EditionLadder (KANFL005),
EditionLadderManifest (KANFL008), EditionUnreachableSuppressor (CS0162)
Helpers/Features/ EditionLadderReader, EditionRanker, LockedBehindReader, LockedMethodSymbols,
LockedMethodSignature, MethodSignatureComparer, FallbackValidator
Models/Features/ per-generator info records
Outputs/Features/ emitters
Enums/Features/ LockedServiceLifetime / FeatureBinding generator-side mirrors
build/ *.props, *.targets (edition, LockedReference, publish guard, verification)
Kanject.Core.Annotations.Tasks/ pruning verifier core (MSBuild-free) + inline-task entry point
Kanject.Core/
Features/
Interfaces/ IFeatureGate
Gates/ InMemoryFeatureGate, DelegateFeatureGate
Models/ FeatureKey
Extensions/ FeatureGateServiceCollectionExtensions
Diagnostics/ FeatureLockCodes
Diagnostics/Exceptions/ FeatureLockedException
Kanject.Core.Features.Entitlements/ EntitlementTokenCodec, EntitlementFeatureGate (optional, ECDSA)
Kanject.Core.Features.TestKit/ EditionTestGate, [EditionFact] / [EditionTheory]
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.8)
-
net8.0
- Microsoft.Extensions.DependencyInjection (>= 8.0.1)
-
net9.0
- Microsoft.Extensions.DependencyInjection (>= 9.0.10)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.13.1 | 107 | 9/27/2026 |
| 3.13.0 | 82 | 9/27/2026 |
| 3.12.7 | 88 | 9/26/2026 |
| 3.12.6 | 119 | 9/7/2026 |
| 3.12.5 | 106 | 8/27/2026 |
| 3.12.4 | 106 | 8/22/2026 |
| 3.12.3 | 122 | 8/10/2026 |
| 3.12.2 | 106 | 8/9/2026 |
| 3.12.1 | 109 | 8/5/2026 |
| 3.12.0 | 110 | 8/5/2026 |
| 3.11.0 | 116 | 8/3/2026 |
| 3.10.5 | 111 | 7/30/2026 |
| 3.10.4 | 121 | 7/18/2026 |
| 3.10.3 | 129 | 7/13/2026 |
| 3.10.2 | 118 | 7/11/2026 |
| 3.10.1 | 113 | 7/11/2026 |
| 3.10.0 | 130 | 7/9/2026 |
| 3.9.2 | 117 | 7/9/2026 |