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.
The calls
Section titled “The calls”manager.LoadAll(); // load everything under plugins/, in dependency ordermanager.Load(packageDirectory); // load one package by pathmanager.Enable("Hello"); // enable by manifest namemanager.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);Releasedis true only when the plugin was actually reclaimed.Errorcarries the reason when it was not.
Unload has two stages
Section titled “Unload has two stages”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.
Status snapshot
Section titled “Status snapshot”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.Loadedmeans the instance and scope are present.PluginState.Detachedmeans they have been released and only collection is pending — the plugin is still in the table, but it is not running.Enabledis 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.
What the snapshot is not
Section titled “What the snapshot is not”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.
