.NET 6中AssemblyLoadContext.Default与自定义LoadContext插件加载异常问题
我有一个主控制台应用程序和一个作为DLL的插件,插件必须实现IPlugin接口,涉及三个项目:
- 主控制台应用程序项目
- 插件项目
- 包含
IPlugin接口的抽象项目,供前两个项目引用
插件被复制到主控制台应用程序下的一个文件夹中,IPlugin.dll同时存在于主程序目录和插件目录中。我通过自定义JobLoadContext类动态加载插件及其子目录中的依赖项,代码如下:
class JobLoadContext : AssemblyLoadContext { private AssemblyDependencyResolver _resolver; public JobLoadContext(string pluginPath) { _resolver = new AssemblyDependencyResolver(pluginPath); } protected override Assembly? Load(AssemblyName assemblyName) { string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); if (assemblyPath != null) { return LoadFromAssemblyPath(assemblyPath); } return null; } protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); if (libraryPath != null) { return LoadUnmanagedDllFromPath(libraryPath); } return IntPtr.Zero; } }
在主程序中通过以下代码加载插件:
var loadContext = new JobLoadContext(path); var asm = loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(path)));
加载完成后,尝试查询实现IPlugin接口的类型:
var moduleInitializerType = asm.GetTypes().FirstOrDefault(t => typeof(IPlugin).IsAssignableFrom(t)); //调用插件中实现IPlugin接口的Init()方法的其他代码
问题在于moduleInitializerType始终为null,但在监视窗口执行相同语句却能找到该类型。
推测原因是LoadContext的工作机制导致的冲突:主程序使用的是自身目录下的IPlugin.dll,而自定义LoadContext加载的插件引用的是插件目录下的IPlugin.dll,即使版本相同也会造成类型不匹配。若使用AssemblyLoadContext.Default加载插件,则无法加载插件目录中的部分依赖DLL;仅当将插件项目添加到主程序编译时,问题才会消失。
想知道是否有方法正确合并LoadContext,或者将自定义LoadContext的内容复制到包含主程序静态引用的AssemblyLoadContext.Default中?
核心思路:共享抽象程序集
问题根源在于同一个IPlugin类型被加载到了两个不同的AssemblyLoadContext中,导致类型系统认为它们是不同的类型。解决的关键是让自定义JobLoadContext复用主程序(Default上下文)中已加载的IPlugin程序集,而不是重新加载插件目录下的副本。
修改JobLoadContext的Load方法,优先从Default上下文查找并加载抽象程序集:
class JobLoadContext : AssemblyLoadContext { private AssemblyDependencyResolver _resolver; public JobLoadContext(string pluginPath) : base(isCollectible: false) { _resolver = new AssemblyDependencyResolver(pluginPath); } protected override Assembly? Load(AssemblyName assemblyName) { // 先检查Default上下文是否已加载该程序集,尤其是抽象接口所在的程序集 var defaultAssembly = AssemblyLoadContext.Default.LoadFromAssemblyName(assemblyName); if (defaultAssembly != null) { return defaultAssembly; } // 再尝试从插件目录解析加载其他依赖 string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); if (assemblyPath != null) { return LoadFromAssemblyPath(assemblyPath); } return null; } protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); if (libraryPath != null) { return LoadUnmanagedDllFromPath(libraryPath); } return IntPtr.Zero; } }
额外优化建议
- 清理插件目录中的重复抽象DLL:直接删除插件目录下的
IPlugin.dll,确保插件编译时仅引用抽象项目,运行时依赖主程序提供的IPlugin.dll,从根源避免重复加载。 - 避免将自定义上下文内容复制到Default上下文:
AssemblyLoadContext设计上是隔离的,强行合并会破坏隔离性,引发更多依赖冲突问题。正确的做法是通过共享核心抽象来打通上下文间的类型兼容。 - 验证类型匹配:修改后可以在代码中添加验证,确认插件中的类型与主程序的
IPlugin类型属于同一个程序集:
var pluginType = asm.GetTypes().FirstOrDefault(t => typeof(IPlugin).IsAssignableFrom(t)); if (pluginType != null) { Console.WriteLine($"插件类型所属程序集:{pluginType.Assembly.FullName}"); Console.WriteLine($"主程序IPlugin所属程序集:{typeof(IPlugin).Assembly.FullName}"); }
内容的提问来源于stack exchange,提问作者user2058413

