插件目录需含.dll、.deps.json、.runtimeconfig.json三文件;接口须独立契约程序集;热更新需清理引用并确认IsUnloading;插件调用宿主服务须传入IServiceProvider或工厂委托。

插件目录结构不规范导致 AssemblyLoadContext 加载失败
插件化架构最常卡在第一步:程序根本找不到或加载不了插件 DLL。.NET 5+ 推荐用 AssemblyLoadContext 隔离加载,但它对路径和依赖非常敏感。不是把 DLL 扔进 ./plugins/ 就能自动识别。
必须保证插件目录里至少包含:
-
MyPlugin.dll(主程序集,含实现IPlugin的类型) -
MyPlugin.deps.json(由发布时dotnet publish生成,记录依赖树) -
MyPlugin.runtimeconfig.json(指定目标运行时,如Microsoft.NETCore.App版本)
缺任意一个,AssemblyLoadContext.LoadFromAssemblyPath() 可能静默失败,或抛出 FileNotFoundException(提示找不到某个“内部依赖”,而非插件本身)。别手动复制 DLL —— 用 dotnet publish -r win-x64 --self-contained false 发布插件项目,再整体拷贝输出目录。
插件接口定义必须放在独立的共享程序集里
如果把 IPlugin 接口直接写在宿主程序里,再让插件项目引用宿主项目,会导致类型加载冲突:Host.IPlugin 和 Plugin.IPlugin 在不同上下文里是两个不兼容的类型,即使签名完全一样,is IPlugin 也会返回 false。
正确做法是新建一个纯接口项目(如 PluginContract.csproj),只含 IPlugin、ICommandHandler 等契约类型,宿主和所有插件都引用它。这个程序集必须:
- 不包含任何实现、配置或第三方依赖
- 版本严格固定(避免
AssemblyVersion自动递增) - 由宿主程序通过
AssemblyLoadContext.Default.LoadFromAssemblyPath()加载(不能走插件上下文)
否则插件中 typeof(MyPlugin).GetInterfaces() 返回的 IPlugin 类型,和宿主用 typeof(IPlugin) 拿到的类型对象,== 判断为 false。
插件热更新时 AssemblyLoadContext.Unload() 不生效
.NET Core 3.0+ 支持卸载上下文,但实际中经常发现调用 context.Unload() 后内存没释放、旧插件还在响应请求。根本原因是存在托管对象跨上下文引用,比如:
- 宿主把插件实例存进了静态字典:
static Dictionary<string iplugin> Plugins</string> - 插件注册了全局事件(如
AppDomain.CurrentDomain.UnhandledException) - 插件启动了后台线程且未显式
Dispose()或Cancel()
卸载前必须确保:
• 调用插件的 Shutdown() 方法(约定接口的一部分)清理资源
• 从所有静态集合中移除插件实例引用
• 使用 WeakReference 缓存插件实例(避免强引用阻止 GC)
• 在 Unload 后主动触发一次 GC.Collect() + GC.WaitForPendingFinalizers()(仅调试用,生产慎用)
即便如此,Unload() 仍是异步操作,需轮询 context.IsUnloading 确认完成。
插件调用宿主服务时依赖注入容器隔离失效
宿主用 IServiceCollection 注册了 IDatabaseConnection,想让插件也能用。但插件在独立 AssemblyLoadContext 中,无法直接访问宿主的 IServiceProvider。
可行方案只有两种:
-
传入
IServiceProvider实例:宿主在创建插件实例后,调用其Initialize(IServiceProvider sp)方法传入作用域内的 provider(注意不是RootServiceProvider,避免生命周期错乱) -
暴露工厂方法:宿主提供
Func<IDatabaseConnection>委托给插件,插件按需调用获取新实例,宿主控制其生命周期
千万别让插件自己 new ServiceScope 或尝试解析 IServiceProvider —— 插件上下文里根本没有注册信息,会抛 InvalidOperationException: No service for type ...。
复杂点在于插件可能需要 scoped 服务(如 EF Core DbContext),此时必须由宿主统一管理 scope 创建与释放,插件只负责使用。


















