Kanject.Core.Features 3.9.2

Prefix Reserved
There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Kanject.Core.Features" Version="3.9.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Kanject.Core.Features" Version="3.9.2" />
                    
Directory.Packages.props
<PackageReference Include="Kanject.Core.Features" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Kanject.Core.Features --version 3.9.2
                    
#r "nuget: Kanject.Core.Features, 3.9.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Kanject.Core.Features@3.9.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Kanject.Core.Features&version=3.9.2
                    
Install as a Cake Addin
#tool nuget:?package=Kanject.Core.Features&version=3.9.2
                    
Install as a Cake Tool

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:

  1. Use an edition service lock for whole capabilities with their own service.
  2. Use an edition partial prune point for small premium contributions inside shared code.
  3. Use a runtime partial method gate for shipped code that should run only when a flag, entitlement, or kill switch allows it.
  4. Use locked references for the strongest artifact boundary.
  5. 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:

  1. Inventory capabilities and choose the edition ladder.
  2. Classify each capability as absent below an edition, present but gated, or always present.
  3. Prefer service locks for whole capabilities.
  4. Use partial prune points for small premium contributions inside shared code.
  5. Use runtime partial gates for shipped code controlled by flags or entitlements.
  6. Use interceptor mode only as a migration bridge for existing non-partial methods.
  7. Run the generator/analyzer tests, fix KANFL00x diagnostics, then add lowest/highest edition CI passes.
  8. 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=true or 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:

  1. Isolate the implementation behind an edition service lock, edition prune point, or LockedReference.
  2. Publish lower editions with trimming or Native AOT.
  3. Add [PrunedBelow(...)] to code that must be absent.
  4. Enable <VerifyEditionPruning>true</VerifyEditionPruning> in CI/publish.
  5. 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 partial method.
  • The containing type needs an accessible IFeatureGate field or property.
  • {Method}Core must exist and match the locked method signature.
  • Fallback, when declared, must match the locked method signature.
  • Generic method type parameters, where constraints, and ref/in parameters are preserved.
  • out parameters 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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