应使用 assemblyloadcontext(iscollectible: true) 实现可卸载插件系统,而非 assembly.loadfrom;后者无法卸载、引发版本冲突与依赖失控,且在 .net 5+ 中已成技术债。

别用 Assembly.LoadFrom 做插件系统,除非你明确接受它无法卸载、版本冲突、依赖失控这三重枷锁。 它在 .NET Framework 里“能跑”,但在 .NET 5+ 中已成技术债源头——真正可维护的动态加载只有一条路:AssemblyLoadContext + isCollectible: true。
Assembly.LoadFrom 为什么在插件场景下会静默失败
它看似方便,实则把所有加载行为都塞进 AssemblyLoadContext.Default,而这个默认上下文永远不可卸载。你调用一百次 Assembly.LoadFrom("v2.dll"),只要程序集显示名称(Name + Version + PublicKeyToken)和之前加载过的相同,返回的仍是第一次加载的那个实例。
常见错误现象:
- 替换插件 DLL 后重启功能,实际运行的还是旧版本逻辑
- 两个插件都依赖
Newtonsoft.Json,但版本不同,运行时报FileNotFoundException或MethodAccessException - 试图用
GC.Collect()+GC.WaitForPendingFinalizers()强制释放,毫无效果
根本原因不是代码写错,而是 LoadFrom 的设计目标从来就不是插件隔离——它是为“一次性扩展”准备的,不是为“热更新”或“多版本共存”设计的。
.NET 5+ 必须用 AssemblyLoadContext.LoadFromAssemblyPath
这是目前唯一能真正卸载程序集的机制,但必须满足三个硬性条件:
- 上下文必须是
new AssemblyLoadContext(isCollectible: true),不能是default或未指定isCollectible - 加载必须走该上下文的
LoadFromAssemblyPath(path),不能 调用静态Assembly.LoadFrom或Assembly.LoadFile - 所有反射操作(
GetType、Activator.CreateInstance、GetMethod)都必须在该上下文内完成,跨上下文直接传递类型会报Cannot load type 'X' from assembly 'Y' because it is not in the same context
示例关键片段:
var context = new AssemblyLoadContext(isCollectible: true);
var assembly = context.LoadFromAssemblyPath(@"plugins\MyPlugin.dll");
var type = assembly.GetType("MyPlugin.PluginImpl");
// 必须在 context 内执行
var instance = context.ExecuteInLoadContext(() =>
{
return Activator.CreateInstance(type);
});
// 调用也需在 context 内
context.ExecuteInLoadContext(() =>
{
type.GetMethod("Execute").Invoke(instance, null);
});
context.Unload(); // 标记为可回收,GC 后真正释放
跨上下文通信只能靠契约接口,不能靠继承或强引用
插件 DLL 里写的 class Plugin : IPlugin,和主程序里定义的 IPlugin 接口,哪怕完全一样,只要不在同一个 AssemblyLoadContext,CLR 就认为是两个类型。所以:
- 必须把接口(如
IPlugin)和 DTO 类抽到一个独立的、不随插件更新的程序集(例如IPluginContract.dll) - 插件项目必须引用这个契约程序集,且编译时不能包含任何主程序类型(如
Logger、ConfigService) - 主程序通过
Activator.CreateInstance创建插件实例后,只能 cast 成契约接口,不能 cast 成具体类名 - 插件需要回调主程序?只能通过接口方法传入
Action或Func<t></t>委托,不能反向持有主程序类型引用
否则你会在运行时看到:InvalidCastException: Unable to cast object of type 'PluginImpl' to type 'IPlugin'——这不是代码问题,是加载上下文的铁律。
Assembly.LoadFile 是最后的备选,但要亲手处理所有依赖
它能绕过同名冲突(同一路径下多个 MyPlugin.dll 可以加载出不同 Assembly 实例),但代价是完全不自动解析依赖。你加载一个插件,它依赖 Newtonsoft.Json 和 MyUtils.dll,这些都不会被自动拉进来。
可行方案只有两种:
- 把所有依赖 DLL 全部拷贝到主程序
bin目录,靠默认AssemblyResolve机制找到(简单但污染主程序部署) - 订阅
AppDomain.CurrentDomain.AssemblyResolve,手动查找并返回依赖程序集(需自己实现路径解析逻辑,容易出错)
注意:Assembly.LoadFile 在 .NET 5+ 中已被标记为“legacy”,且不支持 Unload;它适合临时调试或单次脚本式加载,不适合长期运行的插件服务。
最常被忽略的一点:即使你用了 AssemblyLoadContext,如果插件内部启动了后台线程、注册了静态事件、或缓存了委托到全局变量,context.Unload() 会失败或挂起——卸载前必须确保插件主动清理所有外部引用,否则上下文会卡在“unloading”状态,内存永远不释放。










