.NET 5+热加载需用AssemblyLoadContext并重写Load方法,依赖解析靠.deps.json;契约接口须独立成程序集且仅由Default上下文加载;卸载前须清理所有引用并确保无执行流。

热加载不是“把 DLL 加进去再删掉”就能成的事。.NET 5+ 中唯一能真正卸载程序集的机制是 AssemblyLoadContext,但直接调用 LoadFromAssemblyPath 几乎必然失败——不是路径错,而是依赖链断了、类型跨上下文不兼容、或上下文根本不可回收。
为什么 LoadFromAssemblyPath 直接调用会静默失败或抛 FileNotFoundException
错误现象:插件 DLL 路径完全正确,LoadFromAssemblyPath 却报 FileNotFoundException,提示找不到某个内部类型(比如 Newtonsoft.Json.JsonSerializer),而不是插件本身。
根本原因:AssemblyLoadContext 默认不自动解析插件的依赖树。它只加载你传入的那一个 DLL,其余依赖会 fallback 到 AssemblyLoadContext.Default 去找——而那里没有,于是炸。
实操建议:
- 必须重写
Load方法,用AssemblyDependencyResolver拦截所有依赖请求 - 插件目录必须包含完整发布输出:不只是
MyPlugin.dll,还要有.deps.json、.runtimeconfig.json和所有依赖 DLL - 发布命令必须是
dotnet publish -r win-x64 --self-contained false,不能用dotnet build输出 - 若插件和宿主都引用了
Serilog这类共享包,版本号必须一字不差,否则签名冲突直接拒载
如何让插件类型在宿主中安全使用(避免 InvalidCastException)
错误现象:插件实现了 IPlugin,宿主也定义了同名同签名的 IPlugin,但 (IPlugin)instance 仍抛 InvalidCastException。
根本原因:CLR 认为“来自不同 AssemblyLoadContext 的同名接口是两个类型”,哪怕源码一模一样。
实操建议:
- 接口必须抽离到独立契约程序集(如
PluginContract.dll),且该程序集**只能由AssemblyLoadContext.Default加载**(即宿主启动时就LoadFromAssemblyPath进来) - 插件项目必须 NuGet 引用或项目引用这个契约程序集,**绝不能引用宿主项目**
- 创建实例后,必须用契约接口接收:
IPlugin plugin = (IPlugin)assembly.CreateInstance("MyPlugin.PluginImpl") - 禁止从插件返回
List<T>、string以外的托管对象;如需传数据,走 JSON 序列化,或定义只含get属性的 DTO 类(同样放在契约程序集中)
卸载前必须清理什么,否则 Unload() 会抛 InvalidOperationException
错误现象:context.Unload() 抛 InvalidOperationException:“无法卸载正在执行代码的上下文”。
根本原因:卸载不是“删内存”,而是触发 GC 回收,前提是该上下文里已无任何活跃引用或执行流。
实操建议:
- 必须显式调用
context.Unload(),仅设isCollectible: true不够 - 卸载前确保:插件对象已
Dispose()、所有事件订阅已取消、无后台线程仍在调用插件方法、无Timer或Task持有插件实例引用 - 不要在插件内部开新线程并长期持有上下文对象;如有,需暴露
Stop()方法供宿主调用 - 调试时可用
context.Assemblies检查是否还有残留程序集
最容易被忽略的一点:插件的 .deps.json 文件不是可选的——它是 AssemblyDependencyResolver 解析依赖的唯一依据。少一个字节,整个依赖链就断掉,而错误信息永远指向“找不到某某类型”,不是“找不到 deps.json”。


















