适用于程序集加载的最佳做法Best Practices for Assembly Loading

本文讨论避免类型标识问题的方法,从而避免发生 InvalidCastExceptionMissingMethodException 以及其他错误。This article discusses ways to avoid problems of type identity that can lead to InvalidCastException, MissingMethodException, and other errors. 本文讨论以下建议:The article discusses the following recommendations:

第一个建议(即了解加载上下文的优点和缺点)为其他建议提供了背景信息,因为这些建议都依赖于对加载上下文的了解。The first recommendation, understand the advantages and disadvantages of load contexts, provides background information for the other recommendations, because they all depend on a knowledge of load contexts.

了解加载上下文的优点和缺点Understand the Advantages and Disadvantages of Load Contexts

在应用程序域中,可以将程序集加载到以下三个上下文之一中,也可以在没有上下文的情况下加载它们:Within an application domain, assemblies can be loaded into one of three contexts, or they can be loaded without context:

  • 默认加载上下文包含通过探测全局程序集缓存找到的程序集、(如果托管了运行时)主机程序集存储区(例如在 SQL Server 中)以及应用程序域的 ApplicationBasePrivateBinPathThe default load context contains assemblies found by probing the global assembly cache, the host assembly store if the runtime is hosted (for example, in SQL Server), and the ApplicationBase and PrivateBinPath of the application domain. Load 方法的大多数重载都将程序集加载到此上下文中。Most overloads of the Load method load assemblies into this context.

  • 加载位置上下文包含从加载程序未搜索的位置加载的程序集。The load-from context contains assemblies that are loaded from locations that are not searched by the loader. 例如,外接程序可能安装在一个不在应用程序路径下的目录中。For example, add-ins might be installed in a directory that is not under the application path. Assembly.LoadFromAppDomain.CreateInstanceFromAppDomain.ExecuteAssembly 都是通过路径加载的方法的示例。Assembly.LoadFrom, AppDomain.CreateInstanceFrom, and AppDomain.ExecuteAssembly are examples of methods that load by path.

  • 仅反射上下文包含使用 ReflectionOnlyLoadReflectionOnlyLoadFrom 方法加载的程序集。The reflection-only context contains assemblies loaded with the ReflectionOnlyLoad and ReflectionOnlyLoadFrom methods. 由于无法执行此上下文中的代码,因此在这里不对其进行讨论。Code in this context cannot be executed, so it is not discussed here. 有关详细信息,请参阅如何:将程序集加载到仅反射上下文中For more information, see How to: Load Assemblies into the Reflection-Only Context.

  • 如果使用反射发出生成了一个瞬态动态程序集,则该程序集不在任何上下文中。If you generated a transient dynamic assembly by using reflection emit, the assembly is not in any context. 此外,使用 LoadFile 方法加载的大多数程序集都是在没有上下文的情况下加载的,并且从字节数组加载的程序集也是在没有上下文的情况下加载的,除非这些程序集的标识(在应用策略后)证实它们位于全局程序集缓存中。In addition, most assemblies that are loaded by using the LoadFile method are loaded without context, and assemblies that are loaded from byte arrays are loaded without context unless their identity (after policy is applied) establishes that they are in the global assembly cache.

以下各节将讨论执行上下文的优点和缺点。The execution contexts have advantages and disadvantages, as discussed in the following sections.

默认加载上下文Default Load Context

将程序集加载到默认加载上下文中时,会自动加载其依赖项。When assemblies are loaded into the default load context, their dependencies are loaded automatically. 将自动为默认加载上下文或加载位置上下文中的程序集查找加载到默认加载上下文中的依赖项。Dependencies that are loaded into the default load context are found automatically for assemblies in the default load context or the load-from context. 按程序集标识进行加载可确保不使用未知版本的程序集,从而提高应用程序的稳定性(请参阅避免对部分程序集名称进行绑定一节)。Loading by assembly identity increases the stability of applications by ensuring that unknown versions of assemblies are not used (see the Avoid Binding on Partial Assembly Names section).

使用默认加载上下文具有以下缺点:Using the default load context has the following disadvantages:

  • 加载到其他上下文中的依赖项将不可用。Dependencies that are loaded into other contexts are not available.

  • 不能将位于探测路径外部位置的程序集加载到默认加载上下文中。You cannot load assemblies from locations outside the probing path into the default load context.

加载位置上下文Load-From Context

利用加载位置上下文,可从不在应用程序路径下(因此不包含在探测路径中)的某个路径加载程序集。The load-from context lets you load an assembly from a path that is not under the application path, and therefore is not included in probing. 加载位置上下文允许从该路径查找和加载依赖项,因为路径信息由上下文维护。It enables dependencies to be located and loaded from that path, because the path information is maintained by the context. 另外,此上下文中的程序集可以使用加载到默认加载上下文中的依赖项。In addition, assemblies in this context can use dependencies that are loaded into the default load context.

使用 Assembly.LoadFrom 方法或其他按路径加载的方法之一加载程序集具有以下缺点:Loading assemblies by using the Assembly.LoadFrom method, or one of the other methods that load by path, has the following disadvantages:

  • 如果已加载一个具有相同标识的程序集,则即使指定了不同的路径,LoadFrom 仍返回已加载的程序集。If an assembly with the same identity is already loaded, LoadFrom returns the loaded assembly even if a different path was specified.

  • 如果用 LoadFrom 加载一个程序集,随后默认加载上下文中的一个程序集尝试按显示名称加载同一程序集,则加载尝试将失败。If an assembly is loaded with LoadFrom, and later an assembly in the default load context tries to load the same assembly by display name, the load attempt fails. 对程序集进行反序列化时,可能发生这种情况。This can occur when an assembly is deserialized.

  • 如果用 LoadFrom 加载一个程序集,并且探测路径包括一个具有相同标识但位置不同的程序集,则将发生 InvalidCastExceptionMissingMethodException 或其他意外行为。If an assembly is loaded with LoadFrom, and the probing path includes an assembly with the same identity but in a different location, an InvalidCastException, MissingMethodException, or other unexpected behavior can occur.

  • LoadFrom 需要对指定路径的 FileIOPermissionAccess.ReadFileIOPermissionAccess.PathDiscoveryWebPermissionLoadFrom demands FileIOPermissionAccess.Read and FileIOPermissionAccess.PathDiscovery, or WebPermission, on the specified path.

  • 如果存在程序集的本机映像,将不会使用它。If a native image exists for the assembly, it is not used.

  • 程序集不能以非特定于域的方式加载。The assembly cannot be loaded as domain-neutral.

  • 该策略不适用于 1.0 和 1.1 版本的 .NET Framework。In the .NET Framework versions 1.0 and 1.1, policy is not applied.

无上下文No Context

使用反射发出生成的瞬态程序集只能选择在没有下文的情况下进行加载。Loading without context is the only option for transient assemblies that are generated with reflection emit. 在没有上下文的情况下进行加载是将具有同一标识的多个程序集加载到一个应用程序域中的唯一方式。Loading without context is the only way to load multiple assemblies that have the same identity into one application domain. 这将省去探测成本。The cost of probing is avoided.

从字节数组加载的程序集都是在没有上下文的情况下加载的,除非程序集的标识(在应用策略后建立)与全局程序集缓存中的程序集标识匹配;在此情况下,将会从全局程序集缓存加载程序集。Assemblies that are loaded from byte arrays are loaded without context unless the identity of the assembly, which is established when policy is applied, matches the identity of an assembly in the global assembly cache; in that case, the assembly is loaded from the global assembly cache.

在没有上下文的情况下加载程序集具有以下缺点:Loading assemblies without context has the following disadvantages:

  • 无法将其他程序集绑定到在没有上下文的情况下加载的程序集,除非处理 AppDomain.AssemblyResolve 事件。Other assemblies cannot bind to assemblies that are loaded without context, unless you handle the AppDomain.AssemblyResolve event.

  • 依赖项无法自动加载。Dependencies are not loaded automatically. 可以在没有上下文的情况下预加载依赖项、将依赖项预加载到默认加载上下文中或通过处理 AppDomain.AssemblyResolve 事件来加载依赖项。You can preload them without context, preload them into the default load context, or load them by handling the AppDomain.AssemblyResolve event.

  • 在没有上下文的情况下加载具有同一标识的多个程序集会导致出现类型标识问题,这些问题与将具有同一标识的多个程序集加载到多个上下文中所导致的问题类似。Loading multiple assemblies with the same identity without context can cause type identity problems similar to those caused by loading assemblies with the same identity into multiple contexts. 请参阅避免将一个程序集加载到多个上下文中See Avoid Loading an Assembly into Multiple Contexts.

  • 如果存在程序集的本机映像,将不会使用它。If a native image exists for the assembly, it is not used.

  • 程序集不能以非特定于域的方式加载。The assembly cannot be loaded as domain-neutral.

  • 该策略不适用于 1.0 和 1.1 版本的 .NET Framework。In the .NET Framework versions 1.0 and 1.1, policy is not applied.

避免对部分程序集名称进行绑定Avoid Binding on Partial Assembly Names

加载程序集时,如果仅指定程序集显示名称的一部分 (FullName),则会发生部分名称绑定。Partial name binding occurs when you specify only part of the assembly display name (FullName) when you load an assembly. 例如,可能会在调用 Assembly.Load 方法时仅使用程序集的简单名称,而忽略版本、区域性和公钥标记。For example, you might call the Assembly.Load method with only the simple name of the assembly, omitting the version, culture, and public key token. 也可能会调用 Assembly.LoadWithPartialName 方法,该方法首先调用 Assembly.Load 方法,再搜索全局程序集缓存(如果未能找到程序集)并加载程序集的最新可用版本。Or you might call the Assembly.LoadWithPartialName method, which first calls the Assembly.Load method and, if that fails to locate the assembly, searches the global assembly cache and loads the latest available version of the assembly.

部分名称绑定会导致出现许多问题,其中包括:Partial name binding can cause many problems, including the following:

  • Assembly.LoadWithPartialName 方法可能会加载简单名称相同的不同程序集。The Assembly.LoadWithPartialName method might load a different assembly with the same simple name. 例如,两个应用程序可能会将具有简单名称 GraphicsLibrary 的两个完全不同的程序集安装到全局程序集缓存中。For example, two applications might install two completely different assemblies that both have the simple name GraphicsLibrary into the global assembly cache.

  • 实际加载的程序集可能无法后向兼容。The assembly that is actually loaded might not be backward-compatible. 例如,若不指定版本,加载的版本可能会比最初编写程序时指定使用的版本新很多。For example, not specifying the version might result in the loading of a much later version than the version your program was originally written to use. 较新版本中的更改可能会导致应用程序中出现错误。Changes in the later version might cause errors in your application.

  • 实际加载的程序集可能无法前向兼容。The assembly that is actually loaded might not be forward-compatible. 例如,可以使用程序集的最新版本来生成并测试应用程序,但部分绑定可能会加载一个缺少应用程序所用功能的早期版本。For example, you might have built and tested your application with the latest version of an assembly, but partial binding might load a much earlier version that lacks features your application uses.

  • 安装新的应用程序可能会损坏现有应用程序。Installing new applications can break existing applications. 安装共享程序集的更新的非兼容版本会损坏使用 LoadWithPartialName 方法的应用程序。An application that uses the LoadWithPartialName method can be broken by installing a newer, incompatible version of a shared assembly.

  • 可能发生意外的依赖项加载。Unexpected dependency loading can occur. 在加载共享一个依赖项的两个程序集时,如果利用部分绑定来加载它们,则可能会导致其中一个程序集使用未用来生成或测试该程序集的组件。It you load two assemblies that share a dependency, loading them with partial binding might result in one assembly using a component that it was not built or tested with.

由于部分名称绑定会导致出现上述问题,因此已将 LoadWithPartialName 方法标记为已过时。Because of the problems it can cause, the LoadWithPartialName method has been marked obsolete. 建议改用 Assembly.Load 方法,并指定完整的程序集显示名称。We recommend that you use the Assembly.Load method instead, and specify full assembly display names. 请参阅了解加载上下文的优点和缺点考虑切换到默认加载上下文See Understand the Advantages and Disadvantages of Load Contexts and Consider Switching to the Default Load Context.

如果希望使用 LoadWithPartialName 方法(因为此方法使程序集加载变得很轻松),请考虑在应用程序失败时提供用于标识缺失的程序集的错误消息,与自动使用程序集的未知版本(可能会导致不可预知的行为和安全漏洞)相比,这样做可能能够提供更好的用户体验。If you want to use the LoadWithPartialName method because it makes assembly loading easy, consider that having your application fail with an error message that identifies the missing assembly is likely to provide a better user experience than automatically using an unknown version of the assembly, which might cause unpredictable behavior and security holes.

避免将一个程序集加载到多个上下文中Avoid Loading an Assembly into Multiple Contexts

将一个程序集加载到多个上下文中会导致出现类型标识问题。Loading an assembly into multiple contexts can cause type identity problems. 将同一个程序集中的相同类型加载到两个不同的上下文中,就像是加载具有相同名称的两个不同的类型一样。If the same type is loaded from the same assembly into two different contexts, it is as if two different types with the same name had been loaded. 如果尝试将一个类型强制转换为另一个类型,则将引发 InvalidCastException,并显示一条令人混淆的消息,指示不能将类型 MyType 强制转换为类型 MyTypeAn InvalidCastException is thrown if you try to cast one type to the other, with the confusing message that type MyType cannot be cast to type MyType.

例如,假设在一个名为 Utility 的程序集中声明 ICommunicate 接口,该接口由程序及其加载的其他程序集引用。For example, suppose that the ICommunicate interface is declared in an assembly named Utility, which is referenced by your program and also by other assemblies that your program loads. 这些其他程序集包含实现 ICommunicate 接口的类型,并允许程序使用它们。These other assemblies contain types that implement the ICommunicate interface, allowing your program to use them.

下面来看看在程序运行时会出现的情况。Now consider what happens when your program is run. 程序所引用的程序集将加载到默认加载上下文中。Assemblies that are referenced by your program are loaded into the default load context. 如果使用 Load 方法按照目标程序集的标识来加载该程序集,则该程序集及其依赖项都将位于默认加载上下文中。If you load a target assembly by its identity, using the Load method, it will be in the default load context, and so will its dependencies. 程序和目标程序集将使用同一个 Utility 程序集。Both your program and the target assembly will use the same Utility assembly.

不过,假设使用 LoadFile 方法按照目标程序集的文件路径加载该程序集。However, suppose you load the target assembly by its file path, using the LoadFile method. 该程序集将在没有任何上下文的情况下进行加载,因此不会自动加载其依赖项。The assembly is loaded without any context, so its dependencies are not automatically loaded. 可能具有 AppDomain.AssemblyResolve 事件的处理程序来提供依赖项,并且该处理程序可能会使用 LoadFile 方法在没有上下文的情况下加载 Utility 程序集。You might have a handler for the AppDomain.AssemblyResolve event to supply the dependency, and it might load the Utility assembly with no context by using the LoadFile method. 此时,若创建目标程序集中包含的某个类型的实例,并尝试将该实例分配给类型 ICommunicate 的变量,则将引发 InvalidCastException,因为运行时会将 Utility 程序集的两个副本中的 ICommunicate 接口视为不同的类型。Now when you create an instance of a type that is contained in the target assembly and try to assign it to a variable of type ICommunicate, an InvalidCastException is thrown because the runtime considers the ICommunicate interfaces in the two copies of the Utility assembly to be different types.

在许多其他情况下,也可以将一个程序集加载到多个上下文中。There are many other scenarios in which an assembly can be loaded into multiple contexts. 最佳方法是通过在应用程序路径中重新定位目标程序集,并对 Load 方法使用完整的显示名称,从而避免冲突。The best approach is to avoid conflicts by relocating the target assembly in your application path and using the Load method with the full display name. 然后,将目标程序集加载到默认加载上下文中,并且两个程序集将使用同一个 Utility 程序集。The assembly is then loaded into the default load context, and both assemblies use the same Utility assembly.

如果目标程序集必须保留在应用程序路径的外部,可以使用 LoadFrom 方法将目标程序集加载到加载位置上下文中。If the target assembly must remain outside your application path, you can use the LoadFrom method to load it into the load-from context. 如果编译的目标程序集中存在对应用程序的 Utility 程序集的引用,则目标程序集将会使用应用程序已加载到默认加载上下文中的 Utility 程序集。If the target assembly was compiled with a reference to your application's Utility assembly, it will use the Utility assembly that your application has loaded into the default load context. 请注意,如果目标程序集依赖应用程序路径外部的 Utility 程序集副本,则会出现问题。Note that problems can occur if the target assembly has a dependency on a copy of the Utility assembly located outside your application path. 如果在应用程序加载 Utility 程序集之前已将该程序集加载到加载位置上下文中,则应用程序的加载将失败。If that assembly is loaded into the load-from context before your application loads the Utility assembly, your application's load will fail.

考虑切换到默认加载上下文一节讨论了针对使用文件路径加载(例如 LoadFileLoadFrom)的替代方法。The Consider Switching to the Default Load Context section discusses alternatives to using file path loads such as LoadFile and LoadFrom.

避免将一个程序集的多个版本加载到同一上下文中Avoid Loading Multiple Versions of an Assembly into the Same Context

将一个程序集的多个版本加载到一个加载上下文中会导致出现类型标识问题。Loading multiple versions of an assembly into one load context can cause type identity problems. 从同一个程序集的两个版本加载同一个类型,就像是加载了具有相同名称的两个不同类型一样。If the same type is loaded from two versions of the same assembly, it is as if two different types with the same name had been loaded. 如果尝试将一个类型强制转换为另一个类型,则将引发 InvalidCastException,并显示一条令人混淆的消息,指示不能将类型 MyType 强制转换为类型 MyTypeAn InvalidCastException is thrown if you try to cast one type to the other, with the confusing message that type MyType cannot be cast to type MyType.

例如,程序可能会直接加载 Utility 程序集的一个版本,稍后它可能会加载另一个程序集,而该程序集将加载 Utility 程序集的一个不同的版本。For example, your program might load one version of the Utility assembly directly, and later it might load another assembly that loads a different version of the Utility assembly. 或者,编码错误可能会导致应用程序中两个不同的代码路径加载一个程序集的不同版本。Or a coding error might cause two different code paths in your application to load different versions of an assembly.

在默认加载上下文中,如果使用 Assembly.Load 方法并指定包含不同版本号的完整程序集显示名称,则会出现此问题。In the default load context, this problem can occur when you use the Assembly.Load method and specify complete assembly display names that include different version numbers. 对于在没有上下文的情况下加载的程序集来说,若使用 Assembly.LoadFile 方法从不同的路径加载同一程序集,则会出现此问题。For assemblies that are loaded without context, the problem can be caused by using the Assembly.LoadFile method to load the same assembly from different paths. 运行时会将从不同的路径加载的两个程序集视为不同的程序集,即使这两个程序集的标识相同也是如此。The runtime considers two assemblies that are loaded from different paths to be different assemblies, even if their identities are the same.

除了类型标识问题之外,如果将从程序集的一个版本加载的类型传递给需要来自不同版本的类型的代码,则多个程序集版本还会导致 MissingMethodExceptionIn addition to type identity problems, multiple versions of an assembly can cause a MissingMethodException if a type that is loaded from one version of the assembly is passed to code that expects that type from a different version. 例如,此代码可能需要已添加到更高版本的方法。For example, the code might expect a method that was added to the later version.

如果版本之间的类型行为发生更改,则会出现更多的细微错误。More subtle errors can occur if the behavior of the type changed between versions. 例如,某个方法可能会引发意外的异常或返回意外的值。For example, a method might throw an unexpected exception or return an unexpected value.

请认真检查代码,确保仅加载程序集的一个版本。Carefully review your code to ensure that only one version of an assembly is loaded. 可以使用 AppDomain.GetAssemblies 方法确定在任何给定时间加载的程序集。You can use the AppDomain.GetAssemblies method to determine which assemblies are loaded at any given time.

考虑切换到默认加载上下文Consider Switching to the Default Load Context

检查应用程序的程序集加载和部署模式。Examine your application's assembly loading and deployment patterns. 是否能够消除从字节数组加载的程序集?Can you eliminate assemblies that are loaded from byte arrays? 是否能够将程序集移动到探测路径中?Can you move assemblies into the probing path? 如果程序集位于全局程序集缓存中或应用程序域的探测路径(即 ApplicationBasePrivateBinPath)中,则可以按照程序集的标识来加载程序集。If assemblies are located in the global assembly cache or in the application domain's probing path (that is, its ApplicationBase and PrivateBinPath), you can load the assembly by its identity.

如果无法将所有程序集放入探测路径中,请考虑替代方式,例如使用 .NET Framework 外接程序模型,将程序集放置到全局程序集缓存中或创建应用程序域。If it is not possible to put all your assemblies in the probing path, consider alternatives such as using the .NET Framework add-in model, placing assemblies into the global assembly cache, or creating application domains.

考虑使用 .NET Framework 外接程序模型Consider Using the .NET Framework Add-In Model

如果使用加载位置上下文来实现外接程序(它们通常未安装在应用程序基中),请使用 .NET Framework 外接程序模型。If you are using the load-from context to implement add-ins, which typically are not installed in the application base, use the .NET Framework add-in model. 此模型提供应用程序域或进程级别的隔离,无需自行管理应用程序域。This model provides isolation at the application domain or process level, without requiring you to manage application domains yourself. 有关外接程序模型的信息,请参阅外接程序和扩展性For information about the add-in model, see Add-ins and Extensibility.

考虑使用全局程序集缓存Consider Using the Global Assembly Cache

将程序集置于全局程序集缓存中,不但可以获得位于应用程序基外部的共享程序集路径的好处,而且不会丧失默认加载上下文的优点,也不会承袭其他上下文的缺点。Place assemblies in the global assembly cache to get the benefit of a shared assembly path that is outside the application base, without losing the advantages of the default load context or taking on the disadvantages of the other contexts.

考虑使用应用程序域Consider Using Application Domains

如果确定无法在应用程序的探测路径中部署某些程序集,请考虑为这些程序集创建新的应用程序域。If you determine that some of your assemblies cannot be deployed in the application's probing path, consider creating a new application domain for those assemblies. 使用 AppDomainSetup 可创建新的应用程序域,而使用 AppDomainSetup.ApplicationBase 属性可指定包含要加载的程序集的路径。Use an AppDomainSetup to create the new application domain, and use the AppDomainSetup.ApplicationBase property to specify the path that contains the assemblies you want to load. 如果要探测多个目录,则可以将 ApplicationBase 设置为根目录,并使用 AppDomainSetup.PrivateBinPath 属性标识要探测的子目录。If you have multiple directories to probe, you can set the ApplicationBase to a root directory and use the AppDomainSetup.PrivateBinPath property to identify the subdirectories to probe. 或者,可以创建多个应用程序域,并将每个应用程序域的 ApplicationBase 设置为其程序集的相应路径。Alternatively, you can create multiple application domains and set the ApplicationBase of each application domain to the appropriate path for its assemblies.

请注意,可以使用 Assembly.LoadFrom 方法加载这些程序集。Note that you can use the Assembly.LoadFrom method to load these assemblies. 由于这些程序集此时位于探测路径中,因此会将它们加载到默认加载上下文(而非加载位置上下文)中。Because they are now in the probing path, they will be loaded into the default load context instead of the load-from context. 不过,建议切换到 Assembly.Load 方法并提供完整的程序集显示名称,从而确保始终使用正确的版本。However, we recommend that you switch to the Assembly.Load method and supply full assembly display names to ensure that correct versions are always used.

请参阅See also