.NET插件架构出现InvalidCastException类型转换异常排查
基于共享DLL的插件架构接口转换InvalidCastException问题
问题背景
搭建了一套基于共享DLL的插件架构,通过Shared.dll定义插件接口IPlugin与基础插件类BasePlugin,该DLL同时被插件项目与主应用程序引用。主程序实例化插件对象后尝试转换为IPlugin接口时,抛出InvalidCastException异常,提示无法完成接口转换,存在以下反常现象:
- 对应插件类确实实现了
IPlugin接口 - Visual Studio调试器鼠标悬停查看
objInstance is IPlugin表达式时返回true,但单步执行或直接运行exe时该if条件判断结果为false - Visual Studio即时窗口同样确认上述表达式为
true,且可成功完成对象转换 - 已清理删除所有bin目录,分别在VS2019、VS2022环境测试,异常现象完全一致
初步怀疑是同一DLL存在多份副本重复加载导致类型不匹配,但调试结果与实际运行结果不符提升了排查难度。
相关代码与部署结构
Shared.dll 代码
public interface IPlugin { } public class BasePlugin : IPlugin { }
Plugin.dll 代码
class MyPlugin : BasePlugin { void Init() { // 插件包含主程序DLL引用,用于调用主程序内部能力,比如添加菜单项等逻辑 } }
Main.exe 核心逻辑(伪代码,省略Assembly.Load()加载插件DLL的框架逻辑)
var obj = Activator.CreateInstance(pluginType); // 此处pluginType为MyPlugin类型 if (obj is IPlugin) // <== 实际运行时返回false,但VS调试器中计算结果为true { } var castObj = (IPlugin)obj // <== 此处抛出无效转换异常
部署文件夹结构
|--- MainApp.exe |--- Shared.dll |--- Addins |------- Plugin.dll |------- Shared.dll
问题根因
该问题的核心诱因是CLR程序集加载上下文隔离导致的类型不匹配:
- .NET CLR判定两个类型是否兼容的核心前提是:两个类型必须来自同一个被加载到当前应用域的程序集。哪怕两份DLL的代码、版本完全一致,只要是从不同路径、通过不同加载上下文加载到应用域,CLR就会将其识别为完全独立的程序集,对应类型也不存在实现/继承关系,自然无法完成类型转换。
- 从给出的部署结构可以看到,主程序根目录和Addins插件目录下各存在一份
Shared.dll。主程序启动时会加载根目录的Shared.dll到默认加载上下文;后续加载插件时,CLR解析Plugin.dll的依赖,会优先从插件所在的Addins目录找到第二份Shared.dll并加载,最终应用域中存在两份独立的Shared.dll程序集,插件MyPlugin实现的是Addins目录下那份Shared.dll中的IPlugin,和主程序引用的根目录下的IPlugin不是同一个类型,因此转换失败。 - 调试器显示结果和实际运行不一致的原因是:Visual Studio调试器的表达式求值模块没有严格遵循CLR的加载上下文隔离规则,会将同名、同结构的类型判定为匹配,因此会返回
is IPlugin为true的误判结果,和CLR实际运行时的判定逻辑存在差异。
排查与修复方案
排查步骤
首先确认是否为重复加载问题:在程序触发转换异常的断点处,通过即时窗口执行以下代码,查看当前应用域中加载的Shared程序集数量:
AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name == "Shared").Count()
如果返回结果大于1,即可确认是多副本重复加载导致的问题。
修复方案
- 清理多余DLL副本:删除Addins目录下的
Shared.dll,所有公共依赖DLL统一放在主程序根目录,保证整个应用域内只存在一份Shared.dll文件。 - 修正项目引用配置:插件项目中对
Shared.dll、主程序相关DLL的引用,将复制本地(Copy Local)属性设置为False,避免编译时自动将公共依赖复制到插件输出目录,从编译环节避免产生多余副本。 - 修正程序集加载方式:加载插件时不要使用
Assembly.LoadFile()方法(该方法不会检查已加载程序集,会重复加载同路径外的同标识DLL),改用Assembly.LoadFrom()加载插件,该方法会自动复用已加载到应用域的同标识程序集,不会触发重复加载。如果是.NET Core/.NET 5+版本,可以使用自定义AssemblyLoadContext做插件隔离,将共享依赖统一加载到默认上下文,避免重复加载。 - (可选增强)给
Shared.dll添加强签名,在主程序配置文件中通过probingPath配置插件目录为额外探测路径,CLR加载程序集时会优先匹配已加载的同版本、同强签名的程序集,进一步避免重复加载问题。
内容的提问来源于stack exchange,提问作者Simple Guy
相关产品推荐
相关产品推荐

