唯一支持热加载与热卸载插件的方案是 assemblyloadcontext(alc),需满足:上下文可回收、依赖全由该 alc 加载、接口类型严格跨上下文一致;否则将因依赖解析失败、类型隔离或引用残留导致静默错误或卸载失败。

不能用 Assembly.LoadFrom 或 Assembly.LoadFile 实现可卸载插件——它们加载的程序集永远驻留内存,卸载无效。 真正能热加载 + 热卸载的唯一路径是 AssemblyLoadContext(ALC),且必须满足三个硬性条件:上下文可回收(isCollectible: true)、插件依赖全部由该 ALC 加载、接口类型严格跨上下文一致。
为什么 AssemblyLoadContext.LoadFromAssemblyPath 直接调用会静默失败或抛 FileNotFoundException
这不是路径写错的问题,而是 ALC 默认不自动解析插件的依赖链。当你只传入 MyPlugin.dll,但它的运行时依赖(比如 Newtonsoft.Json.dll 或某个特定版本的 Microsoft.Extensions.DependencyInjection.dll)不在默认上下文里,ALC 就会 fallback 到 AssemblyLoadContext.Default 去找——而那里没有,于是报“找不到某某内部类型”,不是插件本身。
- 必须手动重写
AssemblyLoadContext.Load方法,在其中拦截所有依赖请求,并统一从插件目录加载对应 DLL - 插件发布必须用
dotnet publish -r win-x64 --self-contained false,确保生成.deps.json和.runtimeconfig.json - 别复制单个 DLL —— 整个发布输出目录(含 deps.json、runtimeconfig.json、所有依赖 DLL)一起拷进插件目录
- 若插件引用了宿主也引用的 NuGet 包(如
Serilog),需确保双方版本完全一致,否则 ALC 会因签名冲突拒绝加载
如何让插件类型在宿主中安全使用(避免 InvalidCastException 或 Cannot load type from assembly because it is not in the same context)
跨 ALC 的类型无法直接强制转换或 new 实例。哪怕 IPlugin 接口定义一模一样,只要它分别来自两个不同 ALC,CLR 就认为是两个类型。
- 接口必须定义在**独立共享契约程序集**(如
PluginContract.dll),且该程序集只能由AssemblyLoadContext.Default加载(即宿主启动时就LoadFromAssemblyPath进来) - 插件项目必须引用这个契约程序集(不能引用宿主项目,也不能把接口写在宿主里)
- 创建插件实例后,必须通过契约接口接收,例如:
IPlugin plugin = (IPlugin)assembly.CreateInstance("MyPlugin.PluginImpl"); - 禁止返回
List<t></t>、string、自定义类等托管对象——如果必须传数据,走 JSON 序列化,或定义只含get属性的简单 DTO 类(同样放在契约程序集中)
调用 context.Unload() 后插件还在运行?这是卸载前没清理干净
Unload() 不是“杀死进程”,而是发起异步卸载信号。只要还有任何托管引用指向该上下文中的对象,或者有线程仍在执行其代码,卸载就会挂起、永不完成。
- 卸载前必须显式调用插件的
Shutdown()方法(约定接口的一部分),释放 Timer、取消CancellationTokenSource、关闭HttpClient、注销事件(如AppDomain.CurrentDomain.UnhandledException) - 插件实例不能存进静态字段或全局字典(如
static Dictionary<string iplugin></string>)——这些引用会阻止 GC 回收上下文 - 插件启动的后台线程必须支持
Stop()或Dispose(),且不能用Task.Run(() => { while(true) {...} })这种无退出机制的写法 - 调用
context.Unload()后,应等待其Unloading事件触发,再做后续操作;也可加超时检测:if (!context.Unload(TimeSpan.FromSeconds(5))) { /* 失败处理 */ }
最容易被忽略的是插件的 .deps.json 文件缺失或版本错配——它不像 DLL 那样报“找不到文件”,而是表现为某次加载成功、另一次突然失败,或者只在某些机器上出问题。每次更新插件,都要核对它的 .deps.json 是否与宿主所用的 .NET 运行时版本兼容(比如插件用 Microsoft.NETCore.App 8.0.4,宿主却只装了 8.0.3)。











