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:
PluginLoaderis now stored as an instance field (_pluginLoader) inApplicationHost, keeping a strong reference throughout the entireRunAsynclifecyclePluginLoaderinternally tracks all createdPluginLoadContextinstances in a_loadContextslist to keep them alive until service registration completes- A new
UnloadAll()method provides a cleanup entry point for all plugin ALCs on host shutdown
| Change | File |
|---|---|
_pluginLoader instance field | ApplicationHost.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 keysNew behavior:
Config file exists?
├── No → Generate from DefaultConfig, write to disk
└── Yes → Read existing config, recursively merge missing keys from DefaultConfig, write backMerge rules:
| Scenario | Behavior |
|---|---|
Key in DefaultConfig but missing from existing config | Added 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 casing | Preserve existing key name; no duplicate added; nested objects merged recursively |
Extra keys in existing config not in DefaultConfig | Preserved, 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.
// 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 DefaultConfigCompatibility
- 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
nullor omit them fromDefaultConfig