Nuplane.Loading.Abstractions
0.0.11
dotnet add package Nuplane.Loading.Abstractions --version 0.0.11
NuGet\Install-Package Nuplane.Loading.Abstractions -Version 0.0.11
<PackageReference Include="Nuplane.Loading.Abstractions" Version="0.0.11" />
<PackageVersion Include="Nuplane.Loading.Abstractions" Version="0.0.11" />
<PackageReference Include="Nuplane.Loading.Abstractions" />
paket add Nuplane.Loading.Abstractions --version 0.0.11
#r "nuget: Nuplane.Loading.Abstractions, 0.0.11"
#:package Nuplane.Loading.Abstractions@0.0.11
#addin nuget:?package=Nuplane.Loading.Abstractions&version=0.0.11
#tool nuget:?package=Nuplane.Loading.Abstractions&version=0.0.11
Nuplane
Nuplane is a runtime control plane for NuGet packages. It lets your .NET application install, update, and load NuGet packages while it is running — no restart required.
Drop a .nupkg into a watched folder or point Nuplane at a NuGet feed. It resolves the package, extracts it to a deterministic local store, loads the assemblies into an isolated context, and signals your host. Your host decides what to do with the loaded types.
What you can build
The most immediate use case is dynamic NuGet package installation into a live .NET application:
- Hot-reload plugin systems — ship plugins as NuGet packages; drop them into a local feed folder at runtime and have them discoverable in the live app within seconds.
- Modular feature delivery — split an application into independently versioned feature packages and update individual features at runtime without a full redeployment.
- SaaS per-tenant extensions — load per-tenant behaviour packages at runtime; each tenant gets their customizations without shared hosting risk.
- Workflow and rule engines — deploy new steps, validators, or rules as packages and pick them up live without restarting the engine.
- Internal tool hosts — extend internal platforms dynamically by pushing a new package to a watched folder; the host picks it up automatically.
See the End-to-End ASP.NET Plugin Demo below for a full walkthrough of the drop-folder workflow with a live ASP.NET Core host.
Nuplane handles the infrastructure: feed resolution, deterministic storage, transactional updates, last-known-good fallback, isolated assembly loading, and structured change events. Your host decides what the loaded packages mean.
📖 Start With The Wiki
If you are evaluating or onboarding to Nuplane, start with the repository-owned wiki under docs/wiki/:
Home— product framing and audience routesOverview— why Nuplane exists, what it does, and what it does not doGetting Started— recommended first-use pathUsage Guide— core-runtime vs optional-loading usage guidanceArchitecture Guide— module map and repository-to-concept viewConcepts and Glossary— normalized terminology
The wiki follows a hybrid-hub model: it is self-sufficient for evaluation and onboarding, while deeper validation details, roadmap history, sample mechanics, and other fast-moving reference material remain repository-owned.
✨ What Nuplane Does
- Resolve packages from NuGet v3 feeds
- Support
.nupkglocal directory feed deployment - Maintain a deterministic on-disk package store
- Reconcile desired vs actual package state
- Apply atomic per-package updates
- Provide last-known-good (LKG) fallback
- Emit structured change events for host integration
- Offer integrity validation hooks
- Provide operational visibility (logs, metrics, health)
🚫 What Nuplane Does Not Do
- It does not define a plugin entrypoint model.
- It does not mutate your DI container.
- It does not impose activation semantics.
- It does not guarantee in-process assembly unload.
- It does not sandbox untrusted code.
Nuplane is infrastructure. Your host decides what to do when packages change.
🧠 Core Concept
Nuplane implements a simple control loop:
- Determine desired packages
- Compare with current state
- Compute a diff
- Apply transactional updates
- Emit change events
Hosts (e.g., web apps, workers, modular systems) react to change events by reloading, rescanning, or reconfiguring as needed.
📚 Terminology and Concepts
Desired state
The set of packages Nuplane should make active. Desired state comes from configured sources, such as remote feeds, directory-backed feeds, or other desired-state providers.
When more than one desired source contributes the same package ID, configure source precedence under
Nuplane:DesiredState:SourcePriorities. Lower numbers win and an unlisted source has the lowest
precedence (int.MaxValue). Source names are matched case-insensitively, and the name used here is
the PackageRequest.SourceName emitted by the source. A directory registration sets that source name
to its feed name, while a custom source can use a different stable name. A source's CLR type name is
not used for precedence.
{
"Nuplane": {
"DesiredState": {
"SourcePriorities": {
"renewal-demo-updates": 0,
"renewal-demo-baseline": 10
}
}
}
}
In a configuration-driven host, the builder callback runs after binding and can override configured priorities:
using Nuplane.Sources.Configuration;
services.AddNuplane(configuration.GetSection("Nuplane"), nuplane =>
{
nuplane.Services.Configure<DesiredStateOptions>(options =>
{
options.SetPriority("renewal-demo-updates", 0);
options.SetPriority("renewal-demo-baseline", 10);
});
});
For code-only setup, omit the configuration argument and keep the same callback.
For a duplicate ID, Nuplane selects the request with the lowest source priority, then uses the
existing case-insensitive SourceName and VersionRange ordering, followed by deterministic feed,
update-policy, and casing tie-breaks. The selected PackageRequest remains intact. This policy only
resolves overlap in desired state: feed priorities still control feed candidate ordering, and source
admission, trust checks, package resolution, fallback behavior, and semantic version selection remain
their own stages. FeedResolution:FeedPriorities does not implicitly set source priorities, and Nuplane
does not compare semantic versions across sources. For example, a higher-priority exact 1.0.0 request
remains the winner over a lower-priority source's newer 1.1.0 request. With no priorities, existing
case-insensitive source-name ordering remains the default.
Actual state
The packages currently installed and active in the local package store. Nuplane compares actual state with desired state during each reconciliation cycle.
Feed
A named package source Nuplane can resolve from.
A feed can point at a NuGet v3 service index or at a local directory containing .nupkg files.
Feeds can also define trust level, credentials, and include patterns.
A private feed's credentials are a secrets://<provider>/<name> reference — never the secret itself — resolved at runtime by a registered provider, with a built-in env provider reading secrets://env/MY_FEED_TOKEN from the process environment; see Usage Guide: Feed credentials.
Include pattern
A package ID filter applied to a feed.
For example, MyApp.Plugins.* means that feed is authoritative for matching package IDs only.
These patterns also inform source/package trust defaults unless you explicitly override trust options.
Directory-backed feed
A local folder treated as a feed.
Nuplane can scan it for .nupkg files and optionally watch it for file changes with debounce.
This is what powers the sample's drop-folder workflow.
The folder itself is only read: packages resolved from it are extracted under FeedResolution:PackageInstallRoot,
so a pre-populated package folder can be mounted read-only (-v "$(pwd)/packages:/app/packages:ro").
Reconciliation
A control-loop cycle where Nuplane:
- reads desired state
- resolves package versions from feeds
- computes the diff versus actual state
- applies transactional add/update/remove operations
- emits observer events and operational telemetry
Reconciliation can be manual, startup-triggered, or periodic.
Package store
Nuplane's deterministic on-disk storage area for downloaded packages, current-package pointers, staging work, and persisted state. It is designed so updates are atomic and safe to retry.
Active version
The version Nuplane currently considers live for a package in the local store. This is the version your host should treat as the current runtime target.
Last-known-good (LKG)
The most recent version Nuplane successfully applied for a package. If a future update fails, Nuplane can preserve the LKG state rather than leaving the system half-updated.
Observer
A host-owned callback type registered in DI. Nuplane emits events such as package-change completion or package-loading completion, and your observers decide what the application should do in response.
Optional loading
An opt-in subsystem that loads resolved packages into assembly load contexts. Nuplane supports two package load modes:
Collectibleis the default mode for isolated or scan-only plugin scenarios where package assemblies should remain unloadable when references drain.HostIntegratedis for packages that contribute application-lifetime framework types such as DI registrations, endpoints, hosted services, options, validators, or database migrations.
Shared assemblies remain a separate policy: they solve contract/type identity by resolving selected abstractions from the host/default context, but they do not by themselves make package assemblies framework-integrated or resolvable by name. Nuplane can manage shared contract assemblies, deactivation timeout, unload coordination for collectible packages, and host-integrated assembly-name resolution, but your host still decides what loaded types mean.
Load mode can be selected automatically. Nuplane resolves package graphs first, evaluates load-mode advisors, and promotes a graph to HostIntegrated when a package declares a host-integration requirement. Explicit PackageLoadModes overrides remain authoritative, and packages without overrides or metadata keep using DefaultLoadMode.
Package authors can declare Nuplane loading metadata once in package-root nuplane.json:
{
"schemaVersion": 1,
"loading": {
"loadMode": "HostIntegrated",
"scope": "DependencyClosure",
"reason": "Uses framework type resolution and runtime scheduler integration."
}
}
Nuplane reads this metadata only from packages that have already been resolved and installed through the configured source and integrity paths. Metadata is trusted only as much as the package itself; it does not bypass source trust, package validation, or host-owned activation decisions.
Schema 2 of the same file adds capabilities: a package declares a named choice of root package it needs at run time but does not depend on — which database engine, for example — the host picks one with Nuplane:Capabilities:<name>, and that option's package is resolved as an ordinary root; a capability no host has selected is refused rather than guessed at. See Selecting a package capability and Package Authoring.
A host can also refuse activation outright. A registered IPackageActivationGate is consulted for each package graph that is about to load — after the load mode is decided and before any load context exists — and can return Block(reason) to stop that graph. A block fails every package in that graph as an ordinary load failure and leaves other graphs alone; gates are fail-closed, so a gate that throws also blocks; and a blocked graph is re-evaluated on the next attempt. This is what keeps a process restart or reconcile from silently reactivating a package whose host-side pre-condition, such as a database schema version, is no longer met.
Module registration
Each optional Nuplane module (loading, directory-source) provides its own direct IServiceCollection registration extension.
You can register modules independently of the builder surface, or use the module-owned builder integration package for fluent configuration.
If the same module is registered through both paths, the last registration wins and the service graph remains deduplicated.
Configuration-driven setup
A way to declare Nuplane infrastructure in configuration instead of code.
Nuplane:Setup covers builder-only concepts like feeds, polling, and state-file persistence, while Nuplane:Loading covers optional runtime loading behavior.
Observer registration and other host-specific reactions stay in code.
📦 Quick Start
Use the Nuplane configuration section for infrastructure, then keep host-owned reactions in code:
using Nuplane;
using Nuplane.Loading.Hosting.Builder;
using Nuplane.Sources.Directory.Configuration;
var builder = WebApplication.CreateBuilder(args);
var nuplaneConfiguration = builder.Configuration.GetSection("Nuplane");
builder.Services.AddNuplane(nuplaneConfiguration, nuplane =>
{
nuplane.AddDirectoryFeedsFromConfiguration(nuplaneConfiguration);
nuplane.AutoloadPackages(nuplaneConfiguration.GetSection("Loading"));
nuplane.OnPackagesChanged<PackageChangeObserver>();
nuplane.OnPackagesLoaded<PluginDiscoveryObserver>();
});
{
"Nuplane": {
"Setup": {
"AutomaticReconciliation": true,
"PollInterval": "00:01:00",
"Feeds": {
"local-packages": {
"DirectoryPath": "packages",
"IncludePatterns": [
"*"
],
"Directory": {
"Watch": true,
"DebounceWindow": "00:00:01"
}
},
"nuget.org": {
"ServiceIndex": "https://api.nuget.org/v3/index.json",
"IncludePatterns": [
"Elsa.*"
]
}
}
},
"Loading": {
"Enabled": true,
"DefaultLoadMode": "Collectible",
"LoadModeSelectionPolicy": "Automatic",
"PackageLoadModes": [
{
"PackageId": "Elsa.Persistence.EFCore.PostgreSql",
"LoadMode": "HostIntegrated"
}
],
"SharedAssemblies": [
{
"Name": "Nuplane.Abstractions",
"PublicKeyToken": "31bf3856ad364e35",
"MajorVersion": 1
}
]
}
}
}
When packages are added, updated, or removed, Nuplane emits PackageChangeSet events for your host to react to.
Query-first catalog access
Observers are supplemental invalidation and logging signals. For authoritative reads, query the standalone catalog services directly:
using Nuplane.Sample.AspNetCore.Catalog;
app.MapSampleCatalog();
- Use
IActivePackageCatalog.GetActivePackagesAsync(ct)when you only need the authoritative active package inventory from inside a running host. - Use
NuplaneStore.ReadActivePackagesAsync(stateFilePath, ct)when you need the same active package id/version/install-path data without a running host, a DI container, or network access — see Usage Guide: Offline reads of the active package set. - Use
NuplaneHostIntegratedLoader.LoadFromStateAsync(stateFilePath, options, ct)— or itsStoreRegistryOptionsoverload — when a short-lived worker process must load the host's active package set into assemblies exactly the way that host'sHostIntegratedloading does: same graph grouping, same load contexts, same asset selection, sameDefault.Resolvinghook, without a running host. - Use
NuplaneHostIntegratedLoader.LoadActivePackagesAsync(activePackages, options, ct)when the caller assembles or filters the package set itself; it groups by graph generation identity, which is all anActivePackagecarries, so onlyLoadFromStateAsyncguarantees the host's grouping. Either way the load is irreversible for the process; see Usage Guide: Host-free loading of the active package set. - Use
NuplaneRestore.RestoreAsync(configuration, options, ct)when out-of-process tooling must populate a never-started host's install root and store from that host's own configuration: one reconciliation cycle, no host, nothing loaded, and the resolved state file and install root reported back so the caller can prove where it wrote. UseNuplaneRestore.DescribeDesiredAsync(configuration, options, ct)for the side-effect-free, network-free pre-flight that lists what a restore would ask for and whether each request is a single-point pin. See Usage Guide: Host-free restore of the package set. - Use
IPackageLoadStateCatalog.GetLoadStateAsync(ct)when the optional loading module is installed and you need current-process load-state availability or per-package load status. - Use
IPackageAssemblyCatalogas the default loading-enabled host integration surface when you want sane-default access to loadedAssemblyinstances for the current active package set without manually filtering load-state snapshots first. - Use
IPackageAssemblyCatalog.GetAssembliesAsync(packageId, ct)when you want the currently active loaded version for one package identifier. - Use
IPackageTypeFinder.FindTypesAsync(packageId, ct)only as an optional convenience after assembly access when you want Nuplane to apply assignability-based filtering over the current active loaded version for one package. - Use a host-owned query service such as the sample
PluginCatalogwhen you want to explicitly discover allIPluginimplementations from the current active loaded package set while keeping package-aware control over the discovery flow. - In the sample HTTP payloads for
/catalog/assembliesand/catalog/assemblies/{packageId},loadedAssembliesis the runtime-loaded assembly view whileselectedAssemblyReferencesis the durable loader-selected metadata view. - Use the admin API (
/nuplane/admin/packages,/nuplane/admin/load-state,/nuplane/admin/state) when you want HTTP access to the same composed read surfaces.
Clean-break notes for query surfaces
GET /nuplane/admin/load-stateis owned byNuplane.Loading.Api, not by the core admin packages.GET /nuplane/admin/snapshotis intentionally removed; package inventory and operational state are now separate reads.- The sample's
/catalog/packages,/catalog/load-state,/catalog/assemblies, and/catalog/pluginsendpoints intentionally teach the default host decision tree in that order: active packages, load state, assemblies, then optional type finding. - The sample observers demonstrate cache invalidation and logging only; they are not the authoritative package/loading inventory source.
- These surface changes are intentional breaking changes for hosts that previously depended on merged snapshot or core-admin loading compatibility payloads.
Configuration model
Nuplane has two configuration layers:
Nuplane:Setup— the builder-only setup surface:- automatic reconciliation
- poll interval
- optional state file path
- feeds, include patterns, directory watcher settings
- existing runtime option sections under
Nuplane:*— advanced operator configuration:DesiredStateReconciliationFeedResolutionSourceTrustFeedTrustPolicyLockFileCleanupPolicyConvergenceTrustedSourcePolicyStoreRegistry
Nuplane:Loading— optional loading settings:EnabledDeactivationTimeoutActiveStoreRootDefaultLoadModeLoadModeSelectionPolicyPackageLoadModesSharedAssemblies
When both layers express the same setting, the more specific runtime option section wins over the Nuplane:Setup shorthand:
Reconciliation:EnableAutomaticReconciliationoverridesSetup:AutomaticReconciliationin both directions when the key is present; an explicitfalsedisables automatic reconciliation even when the shorthand enables it.Reconciliation:PollIntervaloverridesSetup:PollInterval; when neither is set, automatic reconciliation polls every 60 seconds.StoreRegistry:StateFilePathoverridesSetup:StateFilePath, andStoreRegistry:UseInMemoryStoreoverridesSetup:UseInMemoryStorein both directions; an explicitfalsethere keeps state persisted even when the shorthand asks for an in-memory store.- The two store persistence settings are mutually exclusive, so an explicit choice in
StoreRegistryalso suppresses the opposingSetupshorthand:StoreRegistry:UseInMemoryStore: trueignoresSetup:StateFilePath, andStoreRegistry:StateFilePathignoresSetup:UseInMemoryStore.
Builder calls run after configuration binds, so WithStateFile(...) and UseInMemoryStore() in the AddNuplane callback still override both layers.
For unrestricted feeds, prefer one of these explicit forms:
- configuration:
"IncludePatterns": ["*"] - configuration alias:
"IncludeAll": true - fluent API:
feed.IncludeAll()
For configuration-driven setup, prefer the keyed Nuplane:Setup:Feeds object shape:
"Feeds": {
"feedz.io": {
"ServiceIndex": "https://new.example/nuget/index.json"
}
}
The feed key is the feed name. This shape is preferred for layered .NET configuration because later providers can override Feeds:feedz.io by identity instead of merging array entries by numeric position. Feed object order is not semantic; use DesiredState:SourcePriorities for overlapping desired requests and FeedResolution:FeedPriorities for feed resolution candidates.
Migration from the legacy array shape is mechanical:
"Feeds": [
{
"Name": "feedz.io",
"ServiceIndex": "https://old.example/nuget/index.json"
}
]
becomes:
"Feeds": {
"feedz.io": {
"ServiceIndex": "https://new.example/nuget/index.json"
}
}
Kubernetes And Restart Persistence
Nuplane keeps runtime state and extracted packages on disk. If a pod restarts with an empty filesystem, Nuplane will need to reconcile and acquire packages again.
To avoid that, persist both of these paths on a mounted volume:
- the package install root
- the store state file
Recommended approach:
- use one persistent volume per replica
- mount it at a stable path such as
/var/lib/nuplane - configure package extraction under
/var/lib/nuplane/packages - configure store state at
/var/lib/nuplane/store-state.json
Example configuration:
{
"Nuplane": {
"Setup": {
"StateFilePath": "/var/lib/nuplane/store-state.json",
"AutomaticReconciliation": true,
"PollInterval": "00:01:00",
"Feeds": {
"nuget.org": {
"ServiceIndex": "https://api.nuget.org/v3/index.json",
"IncludePatterns": [
"*"
]
}
}
},
"FeedResolution": {
"PackageInstallRoot": "/var/lib/nuplane/packages"
}
}
}
Operational guidance:
- prefer a per-replica persistent volume, not a single shared read-write-many package store across replicas
- for Kubernetes, a
StatefulSetwith one volume per replica is the best fit when warm restarts matter - if
UseInMemoryStore=trueis enabled, restart persistence is intentionally disabled - if
PackageInstallRootis not persisted, Nuplane may need to download and extract packages again after pod restart even if the store state file is preserved PackageInstallRootmust be writable; it is the only package location Nuplane writes to, so feed directories supplying.nupkgfiles can stay read-only
With both paths persisted, a restarted pod can typically reload prior active state and reuse previously extracted packages instead of rebuilding its local package store from scratch.
Example StatefulSet sketch:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nuplane-host
spec:
serviceName: nuplane-host
replicas: 2
selector:
matchLabels:
app: nuplane-host
template:
metadata:
labels:
app: nuplane-host
spec:
containers:
- name: app
image: your-registry/nuplane-host:latest
volumeMounts:
- name: nuplane-data
mountPath: /var/lib/nuplane
env:
- name: Nuplane__Setup__StateFilePath
value: /var/lib/nuplane/store-state.json
- name: Nuplane__FeedResolution__PackageInstallRoot
value: /var/lib/nuplane/packages
volumeClaimTemplates:
- metadata:
name: nuplane-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
This keeps each replica's package cache and persisted state warm across pod restarts while avoiding a shared multi-writer package store.
Storage planning notes:
- size the PVC for more than just the currently active package set; leave headroom for staged downloads, retained previous versions, and transient extraction work
- Nuplane may keep prior package versions on disk to preserve transactional safety and last-known-good behavior
- if you enable cleanup policies, align retention settings with your rollback expectations and available disk budget
- if you do not enable cleanup, expect disk usage to grow over time as new package versions are acquired
Breaking change: omitted include filters no longer mean “accept everything.” A feed without
IncludePatterns,IncludeAll, orfeed.IncludeAll()now contributes no packages.
Configuration vs code
Use configuration for infrastructure and policy:
- feeds
- polling and persistence
- loading options
- trust and resolution settings
Keep application-specific behavior in code:
OnPackagesChanged<T>()OnPackagesLoaded<T>()- any logic that decides how your host reacts to package changes
Fluent builder still works
If you prefer code-first setup, the fluent API remains available:
builder.Services.AddNuplane(nuplane =>
{
nuplane.PollEvery(TimeSpan.FromSeconds(60));
nuplane.AddDirectoryFeed("local-packages", "packages", dir =>
{
dir.Watch = true;
dir.DebounceWindow = TimeSpan.FromSeconds(1);
dir.IncludeAll();
});
});
🗂 Package Store Layout
Nuplane maintains a deterministic store:
root/
state.json
packages/{id}/{version}/
current/{id} -> ../packages/{id}/{version}
staging/
Updates are atomic:
- Download to staging
- Validate
- Move to immutable store
- Atomically switch active version
- Persist state
If anything fails, the previous version remains active.
🔄 Reconciliation Model
Nuplane runs a polling loop (configurable interval):
- Aggregate desired state (explicit + discovery sources)
- Resolve versions from feeds
- Compute diff (add / update / remove)
- Apply per-package transactions
- Emit change events
The process is idempotent and safe to retry.
Each cycle holds an exclusive lock on the store it writes — a file beside store-state.json — so two
processes never interleave their read-modify-write of one store. A cycle that cannot take the lock
does nothing and reports Skipped with ReconciliationSkipReason.StoreLockUnavailable instead of
waiting or becoming a second writer. It is on by default; see
Usage Guide: The store lock for the default's rationale,
the in-memory and unlockable-store exemptions, and Nuplane:Reconciliation:EnableStoreLock.
🔍 Desired State Sources
Nuplane can discover desired packages from configured feeds and sources.
Directory-backed local feed
builder.Services.AddNuplane(nuplane =>
{
nuplane.AddDirectoryFeed("local-packages", "packages", dir =>
{
dir.Watch = true;
dir.DebounceWindow = TimeSpan.FromSeconds(1);
dir.Include("MyApp.Plugins.*");
});
});
Dropping a .nupkg into the folder adds it.
Removing the file removes it.
The same setup can be declared through Nuplane:Setup:Feeds when you prefer configuration-driven hosts.
🧪 End-to-End ASP.NET Plugin Demo — Dynamic Package Installation in Action
The sample app shows what it looks like to install a NuGet package into a running ASP.NET Core application without restarting it:
- A local
packagesfolder acts as a directory-backed feed (desired state). - A file-system watcher detects new
.nupkgfiles and triggers reconciliation (with 1-second debounce). - Nuplane resolves and extracts the package, loads its assemblies, and emits
PackageChangeSetevents. PackageChangeObserverandPluginDiscoveryObserverreact to those events and re-query the authoritative catalog surfaces.- The
/catalog/pluginsendpoint exposes allIPluginimplementations discovered from the newly loaded packages.
The whole loop — from dropping a file to having the types available through the HTTP surface — takes about a second.
Build and pack the sample plugin
dotnet pack samples/Nuplane.Sample.Plugin/Nuplane.Sample.Plugin.csproj -c Debug
This produces a .nupkg like:
samples/Nuplane.Sample.Plugin/bin/Debug/Nuplane.Sample.Plugin.1.0.0.nupkg
Start the ASP.NET sample
dotnet run --project samples/Nuplane.Sample.AspNetCore/Nuplane.Sample.AspNetCore.csproj
The app is configured (via the Nuplane section in samples/Nuplane.Sample.AspNetCore/appsettings.json) to watch:
packages
Trigger reconciliation by dropping a package
In another shell:
mkdir -p packages
cp samples/Nuplane.Sample.Plugin/bin/Debug/Nuplane.Sample.Plugin.1.0.0.nupkg packages/
Expected behavior:
- The file watcher detects the new
.nupkgand triggers manual reconcile asynchronously. - Nuplane applies any changes and emits
PackageChangeSetevents. PackageChangeObserverlogs the change-set lifecycle.PluginDiscoveryObserverscans changed package contexts forIPluginand logs discovered type names (for example,Nuplane.Sample.Plugin.HelloPlugin).
See:
samples/Nuplane.Sample.AspNetCore/PackageChangeObserver.cssamples/Nuplane.Sample.AspNetCore/PluginDiscoveryObserver.cs
To trigger another cycle, update/remove packages in packages.
⚙️ Phase 2 Operator Guidance
Use these conventions when enabling advanced feed governance:
- Configure deterministic feed priorities and keep names stable across environments.
- Set trust explicitly per feed:
Trusted,Restricted, orUntrusted. - Use untrusted overrides only with scoped intent (
packageorfeed-rule) and always provide an operator reason. - Enable strict outage handling only when you want impacted packages to fail fast while unrelated packages continue.
Lock-file conventions
- Recommended lock path:
./state/nuplane.lock.json(outside source-controlled app code paths). - Commit lock files only for reproducibility workflows where environment parity is required.
- Schema
2.0records each exact acquired.nupkgassha512:<standard-padded-base64>; the digest includes every archive byte, including package signatures. - Use
generatemode to atomically refresh the complete root and dependency closure after a successful cycle. Unchanged closure output remains byte-for-byte stable. - Use
enforcemode to constrain matching entries to their locked version and feed before acquisition; packages with no entry continue through live resolution. - Use
strictmode to require a valid entry for every acquired root and dependency. Host-provided dependencies are not acquired and therefore require no entry. - Schema
1.0, malformed hashes, missing hashes, and artifact mismatches are rejected byenforceandstrict; run a successfulgeneratecycle to migrate a legacy lock. - Rotate lock files intentionally and treat lock updates as auditable operational changes.
⚙️ Phase 4 Operator Guidance (Convergent Runtime Loading)
Use these conventions when enabling cluster-convergent runtime loading:
- Configure a shared desired manifest with exact version pins for deterministic convergence across replicas.
- Update manifests atomically: upload package artifacts first, then write/update the manifest last.
- Use
ConvergenceOptionsto configure manifest path, admin surfaces, optional loader boundary, and poll interval. - Keep loader integration opt-in and default-disabled unless the host explicitly wants Nuplane-managed loading.
- Use
INuplaneAdminOperations(in-process) orNuplane.Admin.AspNetCore(HTTP) for admin reads and manual reconcile triggers. - Monitor convergence through correlation-linked logs, metrics, health transitions, and observer failure events.
- Treat degraded cycles as non-mutating: LKG active state is preserved; impacted scope is explicitly reported.
Phase 4 validation baseline
- Profile:
phase4-convergent-loading-baseline - Replicas: 2+
- Desired input: shared manifest with exact package versions
- Determinism window: 20 unchanged cycles
- Failure injections: manifest invalid, source outage, acquisition failure, loader failure, manual trigger unavailable/rejected
⚙️ Phase 3 Operator Guidance (Optional Loading)
Use these conventions when enabling optional in-process loading:
- Keep loading opt-in and default-disabled unless the host explicitly wants Nuplane-managed loading.
- Use per-package isolated load contexts and configure shared contracts by identity (
name,publicKeyToken,majorVersion); anull, empty, or omittedpublicKeyTokenidentifies an unsigned assembly, and a shared assembly a package carries is the host's: it is left out of the package's assemblies and a host-integrated load is refused if the host has no matching copy. See Usage Guide: Sharing assemblies with the host. - Configure bounded deactivation timeout and continue with unload attempt on timeout.
- Treat
UnloadPendingas degraded and retry pending unload on each reconciliation cycle. - Capture outcome evidence using observer callbacks plus correlation-linked logs/metrics/health.
Phase 3 validation baseline
- Profile:
phase3-loading-baseline - Dataset: 20 active packages (including overlapping dependencies + shared-contract references)
- Window: 10 identical reconciliation cycles
- Failure injection: load failures, unload failures, deactivation timeout events
🛡 Integrity & Trust
Nuplane supports validation hooks:
public interface IPackageValidator
{
Task ValidateAsync(PackageArtifact artifact);
}
Possible implementations:
- Hash validation
- Signature validation
- Allowlist / denylist
- Feed trust enforcement
Nuplane assumes trusted code execution unless your host enforces additional policies.
📊 Observability
Nuplane provides:
- Structured lifecycle logs
- Per-cycle correlation IDs
- Metrics (adds, updates, failures, durations)
- Health state (healthy / degraded)
- Persistent state tracking
🧱 Architecture
Nuplane is modular:
Nuplane— control plane: reconciliation loop, deterministic package store, NuGet feed integration, and DI/Generic Host registrationNuplane.Abstractions— contracts and data models shared across packagesNuplane.Sources.Directory— directory-backed feed with local.nupkgdiscovery and change monitoringNuplane.Loading(optional) — assembly loading with collectible load contexts, with contracts inNuplane.Loading.AbstractionsNuplane.Admin(optional) — snapshots and manual reconciliation triggersNuplane.Admin.Api/Nuplane.Loading.Api(optional) — ASP.NET Core endpoints for the admin and loading catalog surfaces
🎯 Design Principles
- Deterministic
- Transactional
- Host-neutral
- Operationally safe
- Minimal abstraction surface
- No accidental framework creep
🚀 Roadmap
See docs/roadmap.md for detailed phase breakdown.
📐 Coding Conventions
See docs/coding-conventions.md for project coding standards and conventions.
License
Nuplane is infrastructure for runtime package reconciliation — clean, predictable, and composable.
| 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
- Nuplane.Abstractions (>= 0.0.11)
-
net8.0
- Nuplane.Abstractions (>= 0.0.11)
-
net9.0
- Nuplane.Abstractions (>= 0.0.11)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Nuplane.Loading.Abstractions:
| Package | Downloads |
|---|---|
|
Nuplane.Loading
Core assembly loading services for Nuplane, including collectible load contexts and package loading coordination. |
|
|
Nuplane.Loading.Api
ASP.NET Core endpoint extensions for exposing Nuplane loading catalog APIs. |
|
|
CShells.Nuplane
Optional integration that discovers loaded Nuplane package features and refreshes the CShells feature catalog. |
GitHub repositories
This package is not used by any popular GitHub repositories.