Skip to content

v0.2.5 Changelog

Release date: May 7, 2026

v0.2.5 is a bugfix and feature release in the v0.2.x line with no breaking changes. The Contracts package is unchanged — all existing deployments and plugins can be upgraded without any modifications.

Bug Fixes

Fixed: Plugin AssemblyLoadContext Garbage-Collected Before Service Registration

Under certain conditions, plugins would crash during the RegisterServices phase with a ReflectionTypeLoadException or fail to resolve plugin types.

Root cause:

Plugins are loaded in isolated AssemblyLoadContext (ALC) instances with isCollectible: true. In v0.2.4 and earlier, the PluginLoader was a local variable in ApplicationHost.RunAsync. Once the method moved past plugin loading, the PluginLoader lost all strong references. If the garbage collector triggered during service registration, these ALCs would be marked for unloading, making plugin assembly types unavailable.

Fix:

  • PluginLoader is now stored as an instance field (_pluginLoader) in ApplicationHost, keeping a strong reference throughout the entire RunAsync lifecycle
  • PluginLoader internally tracks all created PluginLoadContext instances in a _loadContexts list to keep them alive until service registration completes
  • A new UnloadAll() method provides a cleanup entry point for all plugin ALCs on host shutdown
ChangeFile
_pluginLoader instance fieldApplicationHost.cs
_loadContexts tracking list + LoadContexts property + UnloadAll()PluginLoader.cs

New Features

New: Incremental Plugin Config File Merging

Prior to v0.2.5, EnsurePluginConfigFile only created a plugin config file (config/{PluginName}.json) if it didn't already exist. If a plugin developer added new keys to DefaultConfig in a newer plugin version, existing users' config files would never receive those new keys — the only workaround was to delete the config file and restart, losing all custom settings.

v0.2.5 overhauls the config file generation logic:

Old behavior:

Config file exists?
  ├── No → Generate from DefaultConfig, write to disk
  └── Yes → Do nothing, even if DefaultConfig has new keys

New behavior:

Config file exists?
  ├── No → Generate from DefaultConfig, write to disk
  └── Yes → Read existing config, recursively merge missing keys from DefaultConfig, write back

Merge rules:

ScenarioBehavior
Key in DefaultConfig but missing from existing configAdded to existing config (using DefaultConfig's casing)
Key in DefaultConfig already exists (exact case match)Value types / arrays → preserve existing value; both are nested objects → recursively merge missing sub-keys
Key in DefaultConfig exists with different casingPreserve existing key name; no duplicate added; nested objects merged recursively
Extra keys in existing config not in DefaultConfigPreserved, not removed

This means plugin developers can safely add new fields to DefaultConfig over time — users' existing config files will be automatically augmented on next startup, without overwriting existing values or removing extra keys.

csharp
// Example: Plugin v1.0 DefaultConfig
public object? DefaultConfig => new MySettings { Host = "localhost" };
// User's existing config/my.plugin.json: { "Host": "192.168.1.100" }

// Plugin v1.1 DefaultConfig (adds Port field)
public object? DefaultConfig => new MySettings { Host = "localhost", Port = 8080 };
// After upgrade, config/my.plugin.json automatically becomes:
// { "Host": "192.168.1.100", "Port": 8080 }
// ↑ Host preserved from user, Port supplemented from DefaultConfig

Compatibility

  • No breaking changes — drop-in replacement for existing v0.2.x deployments
  • Contracts unchanged — plugins do not need recompilation or NuGet updates
  • Config file merging happens automatically at startup with no manual intervention required
  • Plugin developers who want to prevent new keys from being auto-merged should keep those keys as null or omit them from DefaultConfig