注释
本文特定于 .NET Framework。 它不适用于 .NET 的较新版本实现,包括 .NET 6 及更高版本。
本文讨论如何避免导致InvalidCastException、MissingMethodException以及其他错误的类型标识问题的方法。 本文讨论以下建议:
第一个建议, 了解负载上下文的优点和缺点,为其他建议提供背景信息,因为它们都依赖于负载上下文的知识。
了解负载上下文的优点和缺点
在应用程序域中,程序集可以加载到三个上下文中的一个,或者可以在没有上下文的情况下加载程序集:
默认加载上下文包含通过探测全局程序集缓存找到的程序集、(如果托管了运行时)主机程序集存储区(例如在 SQL Server 中)以及应用程序域的 ApplicationBase 和 PrivateBinPath。 Load 方法的大多数重载都将程序集加载到此上下文中。
加载位置上下文包含从加载程序未搜索的位置加载的程序集。 例如,加载项可能安装在不在应用程序路径下的目录中。 Assembly.LoadFrom, AppDomain.CreateInstanceFrom以及 AppDomain.ExecuteAssembly 按路径加载的方法示例。
仅反射上下文包含使用 ReflectionOnlyLoad 和 ReflectionOnlyLoadFrom 方法加载的程序集。 无法执行此上下文中的代码,因此此处未讨论它。 有关详细信息,请参阅如何:将程序集加载到仅反射上下文中。
如果使用反射发出生成了一个瞬态动态程序集,则该程序集不在任何上下文中。 此外,使用 LoadFile 该方法加载的大多数程序集在没有上下文的情况下加载,并且从字节数组加载的程序集在没有上下文的情况下加载,除非其标识(应用策略后)会确定它们位于全局程序集缓存中。
执行上下文具有优点和缺点,如以下部分所述。
默认加载上下文
当程序集加载到默认加载上下文中时,会自动加载其依赖项。 将自动为默认加载上下文或加载位置上下文中的程序集查找加载到默认加载上下文中的依赖项。 程序集标识加载通过确保不使用未知版本的程序集来提高应用程序的稳定性(请参阅“ 避免在部分程序集名称上绑定 ”部分)。
使用默认加载上下文具有以下缺点:
加载到其他上下文中的依赖项将不可用。
无法将程序集从探测路径外部的位置加载到默认加载上下文中。
加载位置上下文
利用加载位置上下文,可从不在应用程序路径下(因此不包含在探测路径中)的某个路径加载程序集。 它使依赖项能够从该路径中定位和加载,因为路径信息由上下文维护。 另外,此上下文中的程序集可以使用加载到默认加载上下文中的依赖项。
使用 Assembly.LoadFrom 方法加载程序集,或使用其他按文件路径加载的方法,都具有以下缺点:
如果在从上下文中加载了具有相同标识的程序集,则即使指定了其他路径, LoadFrom 也会返回加载的程序集。
如果用 LoadFrom 加载一个程序集,随后默认加载上下文中的一个程序集尝试按显示名称加载同一程序集,则加载尝试将失败。 对程序集进行反序列化时,可能发生这种情况。
如果用 LoadFrom 加载一个程序集,并且探测路径包括一个具有相同标识但位置不同的程序集,则将发生 InvalidCastException、MissingMethodException 或其他意外行为。
LoadFrom 需要对指定路径的 FileIOPermissionAccess.Read 和 FileIOPermissionAccess.PathDiscovery 或 WebPermission。
如果存在程序集的本机映像,将不会使用它。
程序集无法以域中立方式加载。
在 .NET Framework 版本 1.0 和 1.1 中,不会应用策略。
无上下文
使用反射发出生成的瞬态程序集只能选择在没有下文的情况下进行加载。 在没有上下文的情况下加载是将具有相同标识的多个程序集加载到一个应用程序域中的唯一方法。 探测成本被避免。
从字节数组加载的程序集在没有上下文的情况下加载,除非在应用策略时建立的程序集的标识与全局程序集缓存中的程序集标识匹配;在这种情况下,将从全局程序集缓存加载程序集。
在没有上下文的情况下加载程序集具有以下缺点:
除非处理 AppDomain.AssemblyResolve 事件,否则其他程序集不能绑定到在没有上下文情况下加载的程序集。
不会自动加载依赖项。 可以在不使用上下文的情况下预加载它们,将其预加载到默认加载上下文中,或者通过处理 AppDomain.AssemblyResolve 事件来加载它们。
在没有上下文的情况下加载具有相同标识的多个程序集可能会导致类型标识问题类似于将具有相同标识的程序集加载到多个上下文中引起的类型标识问题。 请参阅 避免将程序集加载到多个上下文中。
如果存在程序集的本机映像,将不会使用它。
程序集无法以域中立方式加载。
在 .NET Framework 版本 1.0 和 1.1 中,不会应用策略。
避免对部分程序集名称进行绑定
加载程序集时,如果只指定程序集显示名称的一部分(FullName),则会发生部分名称绑定。 例如,调用 Assembly.Load 方法时,可以仅使用程序集的简单名称,省略版本、文化和公钥令牌。 或者,可以调用 Assembly.LoadWithPartialName 该方法,该方法首先调用 Assembly.Load 该方法,如果该方法找不到程序集,则搜索全局程序集缓存并加载程序集的最新可用版本。
部分名称绑定可能会导致许多问题,包括:
该方法 Assembly.LoadWithPartialName 可能会加载具有相同简单名称的不同程序集。 例如,两个应用程序可能会安装两个具有简单名称
GraphicsLibrary
的完全不同的程序集到全局程序集缓存中。实际加载的程序集可能无法向后兼容。 例如,不指定版本可能会导致加载比程序最初写入使用的版本要晚得多的版本。 更高版本中的更改可能会导致应用程序中出现错误。
实际加载的程序集可能无法向前兼容。 例如,你可能已使用最新版本的程序集生成和测试应用程序,但部分绑定可能会加载缺少应用程序使用的功能的早期版本。
安装新应用程序可能会中断现有应用程序。 使用 LoadWithPartialName 该方法的应用程序可以通过安装较新的不兼容版本的共享程序集来中断。
可能发生依赖项意外加载。 如果加载两个共享依赖项的程序集,并使用部分绑定方式进行加载,可能会导致其中一个程序集使用未经过构建或测试的组件。
由于它可能导致的问题,该方法 LoadWithPartialName 已标记为已过时。 建议改用该方法 Assembly.Load ,并指定完整的程序集显示名称。 请参阅 了解负载上下文的优点和缺点 , 并考虑切换到默认加载上下文。
如果想要使用 LoadWithPartialName 此方法,因为它使程序集加载变得容易,请考虑让应用程序失败并显示一条错误消息,指出缺少的程序集可能会提供更好的用户体验,而不是使用未知版本的程序集,这可能会导致不可预知的行为和安全漏洞。
避免将程序集加载到多个上下文中
将程序集加载到多个上下文中可能会导致类型标识问题。 如果将同一类型从同一程序集加载到两个不同的上下文中,则好像加载了同名的两种不同类型的类型一样。 如果尝试将一种类型强制转换为另一种类型,则会引发错误 InvalidCastException 消息,即类型 MyType
无法强制转换为类型 MyType
。
例如,假设接口 ICommunicate
在名为 Utility
的程序集中声明,该程序集由程序引用,也由程序加载的其他程序集引用。 这些其他程序集包含实现 ICommunicate
接口的类型,允许程序使用这些类型。
现在,请考虑运行程序时会发生什么情况。 程序中引用的程序集会被加载到默认加载上下文环境中。 如果使用该方法按标识 Load 加载目标程序集,它将位于默认加载上下文中,因此其依赖项也会加载。 程序和目标程序集都将使用相同的 Utility
程序集。
但是,假设使用LoadFile方法按文件路径加载目标程序集。 程序集加载时没有任何上下文,因此不会自动加载其依赖项。 你可能有一个针对AppDomain.AssemblyResolve事件的处理程序来提供依赖项,并且可能会使用Utility
方法在没有上下文的情况下加载LoadFile程序集。 现在,当你创建一个目标程序集内类型的实例并尝试将其分配给类型为ICommunicate
的变量时,会抛出一个InvalidCastException,因为运行时将不同副本的ICommunicate
程序集中的Utility
接口视为不同的类型。
在很多其他情况下,程序集可以加载到多个上下文中。 最佳方法是通过在应用程序路径中重新定位目标程序集并使用 Load 具有完整显示名称的方法来避免冲突。 然后,程序集将加载到默认加载上下文中,并且两个程序集都使用相同的 Utility
程序集。
如果目标程序集必须留在应用程序路径之外,则可以使用 LoadFrom 方法将其加载到加载上下文中。 如果目标程序集是在引用您应用程序的 Utility
程序集的情况下编译的,那么它将使用由您的应用程序加载到默认加载上下文中的 Utility
程序集。 请注意,如果目标程序集依赖于位于应用程序路径外部的 Utility
程序集的副本,则可能会出现问题。 如果在应用程序加载 Utility
程序集之前将该程序集加载到加载上下文中,则应用程序的加载将失败。
考虑切换到默认加载上下文部分讨论使用文件路径加载的替代方法,例如LoadFile和 LoadFrom。
避免将程序集的多个版本加载到同一上下文中
将程序集的多个版本加载到一个加载上下文中可能会导致类型标识问题。 如果同一类型是从同一程序集的两个版本加载的,则好像加载了同名的两种不同类型的类型。 如果尝试将一种类型强制转换为另一种类型,则会引发错误 InvalidCastException 消息,即类型 MyType
无法强制转换为类型 MyType
。
例如,程序可能会直接加载程序集的 Utility
一个版本,稍后它可能会加载另一个加载不同版本的 Utility
程序集。 或者,编码错误可能会导致应用程序中的两个不同的代码路径加载不同版本的程序集。
在默认加载上下文中,使用 Assembly.Load 该方法并指定包含不同版本号的完整程序集显示名称时,可能会出现此问题。 对于在没有上下文的情况下加载的程序集,问题可能是通过使用 Assembly.LoadFile 该方法从不同路径加载同一程序集引起的。 运行时将两个从不同路径加载的程序集视为不同的程序集,即使它们的标识相同。
除了类型标识问题之外,如果将从程序集的一个版本加载的类型传递给需要来自不同版本的类型的代码,则多个程序集版本还会导致 MissingMethodException。 例如,代码可能期望使用已在更高版本中添加的方法。
如果类型的行为在版本之间发生更改,则会发生更微妙的错误。 例如,方法可能会引发意外异常或返回意外值。
仔细检查代码,确保只加载一个程序集版本。 可以使用该方法 AppDomain.GetAssemblies 来确定在任何给定时间加载哪些程序集。
考虑切换到默认加载上下文
检查应用程序的程序集加载和部署模式。 是否能够消除从字节数组加载的程序集? 是否能够将程序集移动到探测路径中? 如果程序集位于全局程序集缓存中或应用程序域的探测路径(即其 ApplicationBase 和 PrivateBinPath),则可以按程序集标识加载程序集。
如果无法将所有程序集放在探测路径中,请考虑使用 .NET Framework 外接程序模型、将程序集放入全局程序集缓存或创建应用程序域等替代方法。
考虑使用 .NET Framework Add-In 模型
如果您使用从上下文加载实现外接程序,而这些外接程序通常不安装在应用程序基目录中,请使用 .NET Framework 外接程序模型。 此模型在应用程序域或进程级别提供隔离,无需自行管理应用程序域。 有关外接程序模型的信息,请参阅 外接程序和扩展性。
考虑使用全局程序集缓存
将程序集放在全局程序集缓存中,以获得应用程序基础外部的共享程序集路径的好处,而不会失去默认加载上下文的优势或采用其他上下文的缺点。
考虑使用应用程序域
如果确定某些程序集不能部署在应用程序的探测路径中,请考虑为这些程序集创建新的应用程序域。 使用 a AppDomainSetup 创建新的应用程序域,并使用 AppDomainSetup.ApplicationBase 属性指定包含要加载的程序集的路径。 如果有多个目录要探测,则可以将目录 ApplicationBase 设置为根目录,并使用 AppDomainSetup.PrivateBinPath 属性标识要探测的子目录。 或者,可以创建多个应用程序域,并为每个应用程序域配置 ApplicationBase,将其设置为程序集的适当路径。
请注意,可以使用 Assembly.LoadFrom 该方法加载这些程序集。 由于它们现在位于探查路径中,因此将会被加载到默认载入上下文中,而不是加载来源上下文中。 但是,我们建议切换到 Assembly.Load 该方法并提供完整的程序集显示名称,以确保始终使用正确的版本。