Skip to content

Lifecycle

The host drives every plugin transition. The manager does not start anything on its own: LoadAll is you asking for the whole set, and the per-plugin calls below are the rest.

manager.LoadAll(); // load everything under plugins/, in dependency order
manager.Load(packageDirectory); // load one package by path
manager.Enable("Hello"); // enable by manifest name
manager.Disable("Hello");
manager.Unload("Hello");

Load and Enable return a bool. Unload returns an UnloadResult with two fields:

public readonly record struct UnloadResult(bool Released, string? Error);
  • Released is true only when the plugin was actually reclaimed.
  • Error carries the reason when it was not.

Unload is not one step. The manager first releases the instance and its scope, then waits for the loader to confirm the assembly load context was reclaimed. Both stages can fail differently:

Situation What happens
A plugin still depends on it Refused; Released is false. Unload downstream plugins first — the manager does not cascade.
The plugin is a registered loader Refused; loaders cannot be replaced at runtime. Restart the server.
Disable is refused by the plugin Refused; the plugin stays in place and may still be active.
Released, but reclamation is not confirmed Released is false with the timeout in Error; the plugin stays Detached. Call Unload again to re-verify.

The last case is the one worth knowing about. The instance is gone, but the load context is pinned by something — a static field, a live thread, a reference kept in OnUnload. Calling Unload again re-runs only the verification, and a temporarily pinned context can pass the second time.

UnloadVerificationTimeout sets the budget for that wait; see Getting started.

Plugins returns a read-only snapshot of every slot, including plugins that are detached and waiting for collection:

using Microsoft.Extensions.Logging;
var log = loggerFactory.CreateLogger("PluginManager");
foreach (var info in manager.Plugins)
{
log.LogInformation(
"{Name} {Version} loader={Loader} state={State} enabled={Enabled}",
info.Name, info.Version, info.Loader, info.State, info.Enabled);
}

TryGetPlugin("Hello", out var info) returns one entry by name; it finds Detached entries too.

PluginInfo carries the manifest name and version, the loader id, PluginState, the logical Enabled flag, whether the package is a registered loader, and the declared dependencies. Each dependency exposes Name and VersionRange.

Two fields are easy to confuse:

  • PluginState.Loaded means the instance and scope are present. PluginState.Detached means they have been released and only collection is pending — the plugin is still in the table, but it is not running.
  • Enabled is a separate axis. A plugin can be loaded and disabled at the same time.

LoadedNames is the shorter list of plugins that are present and Loaded.

It is a snapshot: holding it does not pin anything, and it does not reflect later changes. Query Plugins again for a current view. It also does not hand out plugin instances — for that, call the manager; the manager owns the handles.