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
                    
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="Nuplane.Loading.Abstractions" Version="0.0.11" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Nuplane.Loading.Abstractions" Version="0.0.11" />
                    
Directory.Packages.props
<PackageReference Include="Nuplane.Loading.Abstractions" />
                    
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 Nuplane.Loading.Abstractions --version 0.0.11
                    
#r "nuget: Nuplane.Loading.Abstractions, 0.0.11"
                    
#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 Nuplane.Loading.Abstractions@0.0.11
                    
#: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=Nuplane.Loading.Abstractions&version=0.0.11
                    
Install as a Cake Addin
#tool nuget:?package=Nuplane.Loading.Abstractions&version=0.0.11
                    
Install as a Cake Tool

Nuplane

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/:

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 .nupkg local 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:

  1. Determine desired packages
  2. Compare with current state
  3. Compute a diff
  4. Apply transactional updates
  5. 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:

  • Collectible is the default mode for isolated or scan-only plugin scenarios where package assemblies should remain unloadable when references drain.
  • HostIntegrated is 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 its StoreRegistryOptions overload — when a short-lived worker process must load the host's active package set into assemblies exactly the way that host's HostIntegrated loading does: same graph grouping, same load contexts, same asset selection, same Default.Resolving hook, 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 an ActivePackage carries, so only LoadFromStateAsync guarantees 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. Use NuplaneRestore.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 IPackageAssemblyCatalog as the default loading-enabled host integration surface when you want sane-default access to loaded Assembly instances 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 PluginCatalog when you want to explicitly discover all IPlugin implementations from the current active loaded package set while keeping package-aware control over the discovery flow.
  • In the sample HTTP payloads for /catalog/assemblies and /catalog/assemblies/{packageId}, loadedAssemblies is the runtime-loaded assembly view while selectedAssemblyReferences is 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-state is owned by Nuplane.Loading.Api, not by the core admin packages.
  • GET /nuplane/admin/snapshot is intentionally removed; package inventory and operational state are now separate reads.
  • The sample's /catalog/packages, /catalog/load-state, /catalog/assemblies, and /catalog/plugins endpoints 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:
    • DesiredState
    • Reconciliation
    • FeedResolution
    • SourceTrust
    • FeedTrustPolicy
    • LockFile
    • CleanupPolicy
    • Convergence
    • TrustedSourcePolicy
    • StoreRegistry
  • Nuplane:Loading — optional loading settings:
    • Enabled
    • DeactivationTimeout
    • ActiveStoreRoot
    • DefaultLoadMode
    • LoadModeSelectionPolicy
    • PackageLoadModes
    • SharedAssemblies

When both layers express the same setting, the more specific runtime option section wins over the Nuplane:Setup shorthand:

  • Reconciliation:EnableAutomaticReconciliation overrides Setup:AutomaticReconciliation in both directions when the key is present; an explicit false disables automatic reconciliation even when the shorthand enables it.
  • Reconciliation:PollInterval overrides Setup:PollInterval; when neither is set, automatic reconciliation polls every 60 seconds.
  • StoreRegistry:StateFilePath overrides Setup:StateFilePath, and StoreRegistry:UseInMemoryStore overrides Setup:UseInMemoryStore in both directions; an explicit false there 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 StoreRegistry also suppresses the opposing Setup shorthand: StoreRegistry:UseInMemoryStore: true ignores Setup:StateFilePath, and StoreRegistry:StateFilePath ignores Setup: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 StatefulSet with one volume per replica is the best fit when warm restarts matter
  • if UseInMemoryStore=true is enabled, restart persistence is intentionally disabled
  • if PackageInstallRoot is not persisted, Nuplane may need to download and extract packages again after pod restart even if the store state file is preserved
  • PackageInstallRoot must be writable; it is the only package location Nuplane writes to, so feed directories supplying .nupkg files 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, or feed.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:

  1. A local packages folder acts as a directory-backed feed (desired state).
  2. A file-system watcher detects new .nupkg files and triggers reconciliation (with 1-second debounce).
  3. Nuplane resolves and extracts the package, loads its assemblies, and emits PackageChangeSet events.
  4. PackageChangeObserver and PluginDiscoveryObserver react to those events and re-query the authoritative catalog surfaces.
  5. The /catalog/plugins endpoint exposes all IPlugin implementations 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 .nupkg and triggers manual reconcile asynchronously.
  • Nuplane applies any changes and emits PackageChangeSet events.
  • PackageChangeObserver logs the change-set lifecycle.
  • PluginDiscoveryObserver scans changed package contexts for IPlugin and logs discovered type names (for example, Nuplane.Sample.Plugin.HelloPlugin).

See:

  • samples/Nuplane.Sample.AspNetCore/PackageChangeObserver.cs
  • samples/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, or Untrusted.
  • Use untrusted overrides only with scoped intent (package or feed-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.0 records each exact acquired .nupkg as sha512:<standard-padded-base64>; the digest includes every archive byte, including package signatures.
  • Use generate mode to atomically refresh the complete root and dependency closure after a successful cycle. Unchanged closure output remains byte-for-byte stable.
  • Use enforce mode to constrain matching entries to their locked version and feed before acquisition; packages with no entry continue through live resolution.
  • Use strict mode 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 by enforce and strict; run a successful generate cycle 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 ConvergenceOptions to 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) or Nuplane.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); a null, empty, or omitted publicKeyToken identifies 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 UnloadPending as 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 registration
  • Nuplane.Abstractions — contracts and data models shared across packages
  • Nuplane.Sources.Directory — directory-backed feed with local .nupkg discovery and change monitoring
  • Nuplane.Loading (optional) — assembly loading with collectible load contexts, with contracts in Nuplane.Loading.Abstractions
  • Nuplane.Admin (optional) — snapshots and manual reconciliation triggers
  • Nuplane.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

MIT


Nuplane is infrastructure for runtime package reconciliation — clean, predictable, and composable.

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 (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.

Version Downloads Last Updated
0.0.11 7 10/10/2026
0.0.10 620 8/11/2026
0.0.9 149 8/7/2026
0.0.8 11,155 5/14/2026
0.0.7 163 5/10/2026
0.0.6 281 5/7/2026
0.0.5 163 5/6/2026
0.0.4 150 5/6/2026
0.0.3 157 5/5/2026
0.0.2 152 5/4/2026
0.0.1 890 4/17/2026