Plugin Isolation
October 1, 2025 ยท View on GitHub
Plugins load alongside the Lidarr host but live inside their own AssemblyLoadContext (ALC). The host shares only the stable Lidarr.Plugin.Abstractions contract; every plugin ships its own copy of Lidarr.Plugin.Common and private dependencies.
documentation flowchart LR Host[Host Process\n(Default ALC)] ALC1[Plugin A ALC\n(collectible)] ALC2[Plugin B ALC\n(collectible)] Host -->|Abstractions| ALC1 Host -->|Abstractions| ALC2 ALC1 -->|Common v1.x (private)| ALC1 ALC2 -->|Common v1.y (private)| ALC2
Minimal loader
The host uses AssemblyDependencyResolver to pull plugin-local assemblies while sharing the ABI assemblies with the default context.
Failure modes & debugging tips
- Default ALC bleed: Calling
Assembly.Load("Lidarr.Plugin.Common")will load into the default context. Always resolve usingAssemblyDependencyResolverandLoadFromAssemblyPath. - File locks on Windows: Static references or unmanaged handles keep the ALC alive. Dispose plugin services, call
GC.Collect()/WaitForPendingFinalizers()twice, and ensure no static holds host services. - apiVersion mismatch: If the manifest declares an incompatible
apiVersion, the loader must refuse to load (see PLUGIN_MANIFEST.md). - Shared singleton collisions: Keep plugin state scoped to the plugin ALC. Avoid global singletons in Common that capture host services.
Optional shim pattern
When the host loader cannot create custom ALCs, ship a stub plugin that immediately spins up its own ALC and forwards calls.
Contract
- Host-owned assemblies (
Lidarr.Plugin.Abstractions, logging abstractions) load in the default ALC. - Every plugin loads inside a collectible ALC created per plugin directory.
- Only types defined in
Lidarr.Plugin.Abstractionscross the boundary. apiVersionmajor mismatch aborts plugin load and logs a friendly error.- Unloading a plugin must leave no file locks after two GC cycles.