dll publickeytoken

dll publickeytoken是 .NET 中部署的基本单,也是所有类型的容器。程序集包含已编译的类型及其中间语言 (IL) 代码、运行时资源和信息,以帮助进行版本控制和引用其他程序集。程序集还定义类型解析的边界。在 .NET 中,程序集由扩展名为的单个文件组成。注意在 .NET 中生成可执行应用程序时,最终会得到

是 .NET 中部署的基本单,也是所有类型的容器。程序集包含已编译的类型及其中间语言 (IL) 代码、运行时资源和信息,以帮助进行版本控制和引用其他程序集。程序集还定义类型解析的边界。在 .NET 中,程序集由扩展名为的单个文件组成。

注意

在 .NET 中生成可执行应用程序时,最终会得到两个文件:程序集 () 和适用于目标平台的可执行启动器 ()。

这与 .NET Framework 中发生的情况不同,后者生成可 (PE) 程序集。PE 具有扩展,并充当程序集和应用程序启动器。PE 可以同时面向 32 位和 64 位版本的 Windows。

本章中的大多数类型都来自以下命名空间:

System.Reflection System.Resources System.Globalization

程序集中的内容

程序集包含四种内容:

程序集清单

向 CLR 提供信息,例如程序集的名称、版本及其引用的其他程序集

应用程序清单

向操作系统提供信息,例如应如何部署程序集以及是否需要管理提升

已编译的类型

程序集中定义的类型的已编译 IL 代码和数据

资源

嵌入在程序集中的其他数据,如图像和可本地化的文本

其中,只有程序集清单是必需的,尽管程序集几乎总是包含已编译的类型(除非它是资源)。参见)。

程序集清单

程序集清单有两个用途:

  • 它向托管宿主环境描述程序集。
  • 它充当程序集中模块、类型和资源的目录。

因此,程序集是。使用者可以发现程序集的所有数据、类型和函数,而无需其他文件。

注意

程序集清单不是显式添加到程序集的内容,而是作为编译的一部分自动嵌入到程序集中。

以下是清单中存储的功能重要数据的摘要:

  • 程序集的简单名称
  • 版本号(汇编版本)
  • 程序集的公钥和签名哈希(如果具有强名称)
  • 引用的程序集的列表,包括其版本和公钥
  • 程序集中定义的类型的列表
  • 它所针对的区域性,如果是附属程序集 ( AssemblyCulture )

清单还可以存储以下信息数据:

  • 完整的标题和描述(程序集标题和程序集说明)
  • 公司和版权信息(组装公司和组装版权)
  • 显示版本(汇编信息版本)
  • 自定义数据的其他属性

其中一些数据派生自提供给编译器的参数,例如引用的程序集列表或用于对程序集进行签名的公钥。其余部分来自括号中指示的程序集属性。

注意

可以使用 .NET 工具 查看程序集清单的内容。在第中,我们描述了如何使用反射以编程方式执行相同的操作。

指定程序集属性

可以在 Visual Studio 中项目的“属性”页上的“包”选项卡上指定常用程序集属性。该选项卡上的设置将添加到项目文件 () 中。

若要指定“包”选项卡不支持的属性,或者如果不使用 文件,可以在源代码中指定程序集属性(这通常在名为 的文件中完成)。

专用属性文件仅包含 using 语句和程序集属性声明。例如,若要向单测试项目公开内部作用域的类型,应执行以下操作:

using System.Runtime.CompilerServices; [assembly:InternalsVisibleTo("MyUnitTestProject")]

应用程序清单 (Windows)

应用程序清单是一个 XML 文件,用于将有关程序集的信息传达给操作系统。在生成过程中,应用程序清单作为 Win32 资源嵌入到启动可执行文件中。如果存在,则在 CLR 加载程序集之前读取和处理清单,并可能影响 Windows 启动应用程序进程的方式。

.NET 应用程序清单在 XML 命名空间 urn:schemas-microsoft-com:asm.v1 中具有一个名为程序集的根素:

<?xml version="1.0" encoding="utf-8"?> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> <!-- contents of manifest --> </assembly>

以下清单指示操作系统请求管理提升:

<?xml version="1.0" encoding="utf-8"?> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> <security> <requestedPrivileges> <requestedExecutionLevel level="requireAdministrator" /> </requestedPrivileges> </security> </trustInfo> </assembly>

(UWP 应用程序具有更详细的清单,如 文件中所述。这包括程序功能的声明,这些功能决定了操作系统授予的权限。编辑此文件的最简单方法是使用 Visual Studio,当您双击清单文件时,它会显示一个对话框。

部署应用程序清单

可以通过在“解决方案资源管理器”中右键单击项目,依次选择“添加”、“新建项”,然后选择“应用程序清单文件”,将应用程序清单添加到 Visual Studio 中的 .NET 项目中。生成后,清单将嵌入到输出程序集中。

注意

.NET 工具 对嵌入式应用程序清单的存在视而不见。但是,Visual Studio 指示如果在解决方案资源管理器中双击程序集,则嵌入的应用程序清单是否存在。

模块

程序集的内容实际上打包在称为的中间容器中。模块对应于包含程序集内容的文件。这个额外的容器层的原因是允许程序集跨越多个文件,这是.NET Framework中存在但在.NET 5和.NET Core中不存在的功能。 说明了这种关系。

dll publickeytoken

单文件程序集

尽管 .NET 不支持多文件程序集,但有时需要注意模块施加的额外容器交付级别。主要场景是反射(请参阅中的“”和)。

程序集类

System.Reflection 中的程序集类是在运行时访问程序集数据的网关。有多种方法可以获取程序集对象:最简单的方法是通过 Type 的程序集属性:

Assembly a = typeof (Program).Assembly;

您还可以通过调用 Assembly 的静态方法之一来获取程序集对象:

GetExecutingAssembly

返回定义当前正在执行的函数的类型的程序集

获取呼叫程序集

与 GetExecutingAssembly 执行相同的操作,但针对调用当前执行函数的函数

获取条目程序集

返回定义应用程序的原始输入方法的程序集

拥有程序集对象后,可以使用其属性和方法来查询程序集的数据并反映其类型。 显示了这些功能的摘要。

大会成员

功能

目的

请参阅该部分…

全名 , 获取名称

返回完全限定名或程序集名称对象

“程序集名称”

代码库 , 位置

程序集文件的位置

“加载、解析和隔离程序集”

加载 , 加载自 ,

手动将程序集加载到内存中

“加载、解析和隔离程序集”

获取卫星组装

定位给定区域性的附属程序集

“资源和卫星组件”

GetType , GetTypes

返回程序集中定义的一个或多个类型。

入口点

返回应用程序的输入方法,作为 MethodInfo

GetModule , GetModules , ManifestModule

返回程序集的所有模块或主模块

中的

GetCustomAttribute, GetCustomAttributes

返回程序集的属性

中的

强名称和程序集签名

注意

在 .NET Framework 中,强命名程序集非常重要,原因有两个:

  • 它允许将程序集加载到“全局程序集缓存”中。
  • 它允许程序集通过其他强名称程序集引用。

强命名在 .NET 5 和 .NET Core 中的重要性要低得多,因为这些运行时没有全局程序集缓存,也不会施加第二个限制。

程序集具有唯一的标识。它的工作原理是向清单添加两个数据位:

  • 属于程序集作者的唯一
  • 程序集的,证明唯一编号持有者生成了程序集

这需要公钥/私钥对。提供唯一的标识号,便于签名。

注意

签名与签名不同。我们将在本章后面介绍 Authenticode。

公钥在保证程序集引用的唯一性方面很有价值:强名称程序集将公钥合并到其标识中。

在 .NET Framework 中,私钥可防止程序集被篡改,因为如果没有私钥,任何人都无法在不破坏签名的情况下发布程序集的修改版本。实际上,当将程序集加载到 .NET Framework 的全局程序集缓存中时,这会很有用。在 .NET 5 和 .NET Core 中,签名几乎没有用处,因为它从未被选中。

向以前“弱”命名的程序集添加强名称会更改其标识。出于这个原因,如果您认为程序集将来可能需要强名称,从一开始就对程序集进行强名称是值得的。

如何对程序集进行强命名

要为程序集指定一个强名称,请首先使用 实用程序生成公钥/私钥对:

sn.exe -k MyKeyPair.snk

注意

Visual Studio 安装一个名为 的快捷方式,该快捷方式启动一个命令提示符,其 PATH 包含 等开发工具。

这将生成一个新的密钥对,并将其存储到名为的文件中。如果随后丢失此文件,将永久失去使用相同的标识重新编译程序集的能力。

可以通过更新项目文件来使用此文件对程序集进行签名。从 Visual Studio 转到“项目属性”窗口,然后在“签名”选项卡上,选中“对程序集进行”复选框并选择 文件。

同一密钥对可以对多个程序集进行签名 – 如果它们的简单名称不同,它们仍将具有不同的标识。

程序集名称

程序集的“标识”包含其清单中的四个数据:

  • 它的简单名称
  • 其版本(如果不存在,则为“0.0.0.0”)
  • 它的文化(如果不是卫星,则为“中立”)
  • 其公钥标记(如果未强名称,则为“null”)

简单名称不是来自任何属性,而是来自最初编译到的文件的名称(减去任何扩展名)。因此,程序集的简单名称是“System.Xml”。重命名文件不会更改程序集的简单名称。

版本号来自 AssemblyVersion 属性。它是一个字符串,分为四个部分,如下所示:

major.minor.build.revision

您可以指定版本号,如下所示:

[assembly: AssemblyVersion ("2.5.6.7")]

区域性来自 AssemblyCulture 属性,适用于附属程序集,稍后将在一节中介绍。

公钥标记来自编译时提供的强名称,如上一节所述。

完全限定名称

完全限定的程序集名称是包含所有四个标识组件的字符串,格式如下:

simple-name, Version=version, Culture=culture, PublicKeyToken=public-key

例如,System.Private.CoreLib.dll 的完全限定名称是 。

如果程序集没有 AssemblyVersion 属性,则版本显示为 0.0.0.0 。如果未签名,则其公钥标记显示为 null 。

程序集对象的 FullName 属性返回其完全限定名称。编译器在清单中记录程序集引用时始终使用完全限定的名称。

注意

完全限定的程序集名称不包括目录路径,以帮助在磁盘上查找它。查找驻留在另一个目录中的程序集是我们在中处理的完全独立的问题。

程序集名称类

AssemblyName 是一个类,对于完全限定程序集名称的四个组件中的每一个组件都有一个类型化属性。程序集名称有两个用途:

  • 它分析或生成完全限定的程序集名称。
  • 它存储一些额外的数据,以帮助解决(查找)程序集。

可以通过以下任一方式获取程序集名称对象:

  • 实例化程序集名称 ,提供完全限定的名称。
  • 在现有程序集 上调用 GetName。
  • 调用 AssemblyName.GetAssemblyName ,提供磁盘上程序集文件的路径。

还可以实例化不带任何参数的 AssemblyName 对象,然后设置其每个属性以生成完全限定名。以这种方式构造时,程序集名称是可变的。

以下是它的基本属性和方法:

string FullName { get; } // Fully qualified name string Name { get; set; } // Simple name Version Version { get; set; } // Assembly version CultureInfo CultureInfo { get; set; } // For satellite assemblies string CodeBase { get; set; } // Location byte[] GetPublicKey(); // 160 bytes void SetPublicKey (byte[] key); byte[] GetPublicKeyToken(); // 8-byte version void SetPublicKeyToken (byte[] publicKeyToken);

版本本身是一种强类型表示形式,具有“主要”、“次要”、“内部版本”和“修订号”的属性。GetPublicKey 返回完整的加密公钥;GetPublicKeyToken 返回用于建立标识的最后八个字节。

使用程序集名称获取程序集的简单名称:

Console.WriteLine (typeof (string).Assembly.GetName().Name); // System.Private.CoreLib

获取程序集版本:

string v = myAssembly.GetName().Version.ToString();

程序集信息和文件版本

另外两个程序集属性可用于表示与版本相关的信息。与AssemblyVersion不同,以下两个属性不会影响程序集的标识,因此对编译时或运行时发生的情况没有影响:

汇编信息版本

向最终用户显示的版本。这在“Windows 文件属性”对话框中显示为“产品版本”。任何字符串都可以转到此处,例如“5.1 Beta 2”。通常,将为应用程序中的所有程序集分配相同的信息版本号。

汇编文件版本

这旨在引用该程序集的内部版本号。这在“Windows 文件属性”对话框中显示为“文件版本”。与AssemblyVersion一样,它必须包含一个由最多四个数字组成的字符串,这些数字由句点分隔。

验证码签名

是一种代码签名系统,其目的是证明发布者的身份。验证码和签名是独立的:您可以使用其中一个或两个系统对程序集进行签名。

尽管强名称签名可以证明程序集 A、B 和 C 来自同一参与方(假设私钥未泄露),但它无法告诉您该参与方是谁。要知道派对是乔·阿尔巴哈里(Joe Albahari)或Microsoft公司(Joe Albahari),你需要Authenticode。

从 Internet 下载程序时,验证码很有用,因为它可以保证程序来自证书颁发机构指定的任何人,并且在传输过程中未被修改。它还可以防止首次运行下载的应用程序时出现“未知发布者”警告。将应用提交到 Windows 应用商店时,验证码签名也是一项要求。

验证码不仅适用于 .NET 程序集,还适用于非托管可执行文件和二进制文件(如部署文件)。当然,Authenticode 并不能保证程序没有恶意软件,尽管它确实降低了它的可能性。个人或实体愿意将其名称(由护照或公司文件支持)放在可执行文件或库后面。

注意

CLR 不会将验证码签名视为程序集标识的一部分。但是,它可以按需读取和验证验证码签名,您很快就会看到。

使用 Authenticode 签名要求您联系 (CA),并提供您的个人身份或公司身份(公司章程等)的证据。CA 检查您的文档后,它将颁发一个 X.509 代码签名证书,该证书的有效期通常为一到五年。这使您能够使用 实用程序对程序集进行签名。您也可以使用 实用程序自己制作证书;但是,只有在显式安装了证书的计算机上才能识别它。

(非自签名)证书可以在任何计算机上工作的事实依赖于基础结构。实质上,您的证书是使用属于 CA 的另一个证书签名的。CA 是受信任的,因为所有 CA 都加载到操作系统中。 (转到 Windows 控制面板,然后在搜索框中键入。在“管理工具”部分中,单击“管理计算机证书”。这将启动证书管理器。打开“受信任的根证书颁发机构”节点,然后单击“证书”。如果泄露,CA 可以吊销发布者的证书,因此验证验证码签名需要定期向 CA 询问证书吊销的最新列表。

由于验证码使用加密签名,因此如果随后有人篡改文件,验证码签名无效。我们将在第 中讨论加密、散列和签名。

如何使用验证码签名

获取和安装证书

第一步是从 CA 获取代码签名证书(请参阅下面的侧栏)。然后,可以将证书作为受密码保护的文件使用,也可以将证书加载到计算机的证书存储中。执行后者的好处是,您无需指定密码即可签名。这是有利的,因为它可以防止密码在自动生成脚本或批处理文件中可见。

从何处获取代码签名证书

只有少数代码签名 CA 作为根证书颁发机构预加载到 Windows 中。其中包括Comodo,Go Daddy,GlobalSign,DigiCert,Thawte和Symantec。

还有一些经销商,如K Software,提供上述当局的折扣代码签名证书。

由K Software,Comodo,Go Daddy和GlobalSign颁发的Authenticode证书被宣传为限制较少,因为它们也将签署非Microsoft程序。除此之外,所有供应商的产品在功能上都是等效的。

请注意,SSL 证书通常不能用于验证码签名(尽管使用相同的 X.509 基础结构)。这部分是因为SSL证书是关于证明域所有权的;Authenticode是关于证明你是谁。

若要将证书加载到计算机的证书存储中,请按照前面所述打开证书管理器。打开“个人”文件夹,右键单击其“证书”文件夹,然后选择“所有任务/导入”。导入向导将指导您完成此过程。导入完成后,单击证书上的“查看”按钮,转到“详细信息”选项卡,然后复制证书的。这是随后在签名时需要标识证书的 SHA-256 哈希。

注意

如果还希望对程序集进行强名称签名,则必须在验证码签名执行此操作。这是因为 CLR 知道验证码签名,但反之则不然。因此,如果在对程序集进行验证码签名程序集进行强名称签名,则后者会将添加 CLR 的强名称视为未经授权的修改,并认为程序集。

使用签名工具签名.exe

您可以使用 Visual Studio 附带的 实用工具对程序进行验证签名(查看”下的 文件夹)。下面使用安全的 SHA256 哈希算法对名为 的文件进行签名.exe该文件的证书位于计算机的“中,名为“Joseph Albahari”:

signtool sign /n "Joseph Albahari" /fd sha256 LINQPad.exe

您还可以使用 /d 和 /du 指定描述和产品 URL:

 ... /d LINQPad /du http://www.linqpad.net

在大多数情况下,您还需要指定。

时间戳

证书过期后,您将无法再对程序进行签名。但是,如果在签名时使用 /tr 开关指定了,则在过期签名的程序仍将有效。CA将为此目的为您提供一个URI:以下内容适用于Comodo(或K软件):

 ... /tr http://timestamp.comodoca.com/authenticode /td SHA256

验证程序是否已签名

查看文件验证码签名的最简单方法是在 Windows 资源管理器中查看文件的属性(在“数字签名”选项卡中查看)。实用程序也为此提供了一个选项。

资源和附属组件

应用程序通常不仅包含可执行代码,还包含文本、图像或 XML 文件等内容。此类内容可以通过在程序集中表示。资源有两个重叠的用例:

  • 合并无法进入源代码的数据,例如图像
  • 在多语言应用程序中存储可能需要翻译的数据

程序集资源最终是具有名称的字节流。您可以将程序集视为包含按字符串键控的字节数组字典。如果在 ildasm 中反汇编包含名为 的资源的程序集.jpg 和名为 的资源,则可以在 中看到这一点:

.mresource public banner.jpg { // Offset: 0x00000F58 Length: 0x000004F6 } .mresource public data.xml { // Offset: 0x00001458 Length: 0x0000027E }

在本例中,和数据直接包含在程序集中 — 每个都作为其自己的嵌入资源。这是最简单的工作方式。

.NET 还允许您通过中间 容器添加内容。这些旨在保存可能需要翻译成不同语言的内容。本地化的 可以打包为单独的附属程序集,这些附属程序集在运行时根据用户的操作系统语言自动选取。

演示了一个程序集,其中包含两个直接嵌入的资源,以及一个名为 容器,我们为其创建了两个本地化的附属服务器。

dll publickeytoken

资源

直接嵌入资源

注意

在 Window 应用商店应用中不支持将资源嵌入到程序集中。相反,请将任何额外的文件添加到部署包中,并通过从应用程序 StorageFolder ( Package.Current.InstalledLocation ) 读取来访问它们。

要使用 Visual Studio 直接嵌入资源,请执行以下操作:

  • 将文件添加到项目中。
  • 将其生成操作设置为“嵌入的资源”。

Visual Studio 始终在资源名称前面加上项目的默认命名空间,以及包含该文件的任何子文件夹的名称。因此,如果项目的默认命名空间是 Westwind.Reports 并且您的文件称为 .jpg则在文件夹中,资源名称将为 。

注意

资源名称区分大小写。这使得 Visual Studio 中包含资源的项目子文件夹名称有效地区分大小写。

若要检索资源,请在包含该资源的程序集上调用 GetManifestResourceStream。这将返回一个流,然后您可以像任何其他流一样读取该流:

Assembly a = Assembly.GetEntryAssembly(); using (Stream s = a.GetManifestResourceStream ("TestProject.data.xml")) using (XmlReader r = XmlReader.Create (s)) ... System.Drawing.Image image; using (Stream s = a.GetManifestResourceStream ("TestProject.banner.jpg")) image = System.Drawing.Image.FromStream (s);

返回的流是可搜索的,因此您也可以执行以下操作:

byte[] data; using (Stream s = a.GetManifestResourceStream ("TestProject.banner.jpg")) data = new BinaryReader (s).ReadBytes ((int) s.Length);

如果使用 Visual Studio 嵌入资源,则必须记住包含基于命名空间的前缀。为了帮助避免错误,可以使用在单独的参数中指定前缀。类型的命名空间用作前缀:

using (Stream s = a.GetManifestResourceStream (typeof (X), "data.xml"))

X 可以是具有所需资源命名空间的任何类型(通常是同一项目文件夹中的类型)。

注意

在 Visual Studio 中将项目项的生成操作设置为 Windows Presentation Foundation (WPF) 应用程序中的资源与将其生成操作设置为“嵌入的资源”不同。前者实际上将该项添加到名为 的 文件中,该文件的内容通过 WPF 的应用程序类访问,使用 URI 作为键。

为了增加混淆,WPF 进一步重载了术语“资源”。资源和动态资源都与程序集无关!

GetManifestResourceNames 返回程序集中所有资源的名称。

.资源文件

文件是潜在可本地化内容的容器。 文件最终会成为程序集中的嵌入资源,就像任何其他类型的文件一样。不同之处在于您必须执行以下操作:

  • 首先将内容打包到 文件中
  • 通过 ResourceManager 或 而不是 GetManifestResourceStream 访问其内容

文件以二进制形式构建,因此不可人工编辑;因此,您必须依靠 .NET 和 Visual Studio 提供的工具来处理它们。字符串或简单数据类型的标准方法是使用 .resx 格式,可以通过 Visual Studio 或 工具将其转换为 文件。 格式也适用于用于 Windows 窗体或 ASP.NET 应用程序的图像。

在 WPF 应用程序中,必须对需要由 URI 引用的图像或类似内容使用 Visual Studio 的“资源”生成操作。无论是否需要本地化,这都适用。

我们将在以下各节中介绍如何执行其中的每一个操作。

.resx 文件

文件是用于生成 文件的设计时格式。 文件使用 XML,其结构为名称/值对,如下所示:

<root> <data name="Greeting"> <value>hello</value> </data> <data name="DefaultFontSize" type="System.Int32, mscorlib"> <value>10</value> </data> </root>

若要在 Visual Studio 中创建 文件,请添加类型为“资源文件”的项目项。其余工作将自动完成:

  • 将创建正确的标头。
  • 提供了一个设计器,用于添加字符串、图像、文件和其他类型的数据。
  • 文件会自动转换为 格式,并在编译时嵌入到程序集中。
  • 编写一个类来帮助您稍后访问数据。

注意

资源设计器将图像添加为类型化图像对象 (),而不是字节数组,这使得它们不适合 WPF 应用程序。

读取 .resources 文件

注意

如果在 Visual Studio 中创建 文件,则会自动生成一个同名的类,其中包含用于检索其每个项的属性。

类读取程序集中嵌入的 文件:

ResourceManager r = new ResourceManager ("welcome", Assembly.GetExecutingAssembly());

(如果资源是在 Visual Studio 中编译的,则第一个参数必须以命名空间为前缀。

然后,您可以通过使用强制转换调用 GetString 或 GetObject 来访问内部内容:

string greeting = r.GetString ("Greeting"); int fontSize = (int) r.GetObject ("DefaultFontSize"); Image image = (Image) r.GetObject ("flag.png"); 

枚举 文件的内容:

ResourceManager r = new ResourceManager (...); ResourceSet set = r.GetResourceSet (CultureInfo.CurrentUICulture, true, true); foreach (System.Collections.DictionaryEntry entry in set) Console.WriteLine (entry.Key);

在 Visual Studio 中创建包 URI 资源

在 WPF 应用程序中,XAML 文件需要能够通过 URI 访问资源。例如:

<Button> <Image Height="50" Source="flag.png"/> </Button>

或者,如果资源位于另一个程序集中:

<Button> <Image Height="50" Source="UtilsAssembly;Component/flag.png"/> </Button>

(组件是一个文字关键字。

若要创建可以这种方式加载的资源,不能使用 文件。相反,必须将文件添加到项目中,并将其生成操作设置为“资源”(而不是“嵌入的资源”)。然后,Visual Studio 将它们编译为名为 的 . 文件,该文件也是编译的 XAML () 文件的主页。

若要以编程方式加载 URI 键资源,请调用 Application.GetResourceStream:

Uri u = new Uri ("flag.png", UriKind.Relative); using (Stream s = Application.GetResourceStream (u).Stream)

请注意,我们使用了相对 URI。您还可以使用完全以下格式的绝对 URI(三个逗号不是拼写错误):

Uri u = new Uri ("pack://application:,,,/flag.png");

如果您希望指定程序集对象,则可以使用资源管理器来检索内容:

Assembly a = Assembly.GetExecutingAssembly(); ResourceManager r = new ResourceManager (a.GetName().Name + ".g", a); using (Stream s = r.GetStream ("flag.png")) ...

资源管理器还允许您枚举给定程序集中 . 容器的内容。

附属组件

中的数据是可本地化的。

当应用程序在构建为以不同语言显示所有内容的 Windows 版本上运行时,资源本地化是相关的。为了保持一致性,应用程序也应使用相同的语言。

典型的设置如下:

  • 主程序集包含默认语言或语言的 。
  • 单独的包含翻译成不同语言的本地化 。

当应用程序运行时,.NET 会检查当前操作系统的语言(来自 CultureInfo.CurrentUICulture )。每当使用 资源管理器 请求资源时,运行时都会查找本地化的附属程序集。如果可用(并且它包含您请求的资源密钥),则使用该密钥代替主程序集的版本。

这意味着您只需添加新的附属组件即可增强语言支持,而无需更改主程序集。

注意

附属程序集不能包含可执行代码,只能包含资源。

附属程序集部署在程序集文件夹的子目录中

programBaseFolder\MyProgram.exe \MyLibrary.exe \XX\MyProgram.resources.dll \XX\MyLibrary.resources.dll

XX指两个字母的语言代码(例如“de”表示德语)或语言和地区代码(例如“en-GB”表示英国的英语)。此命名系统允许 CLR 自动查找并加载正确的附属程序集。

构建附属程序集

回想一下我们之前的 示例,其中包括以下内容:

<root> ... <data name="Greeting" <value>hello</value> </data> </root>

然后,我们在运行时检索问候语,如下所示:

ResourceManager r = new ResourceManager ("welcome", Assembly.GetExecutingAssembly()); Console.Write (r.GetString ("Greeting"));

假设我们希望它改为在德语版本的Windows上运行时写“hallo”。第一步是添加另一个名为文件,该文件用代替:

<root> <data name="Greeting"> <value>hallo<value> </data> </root>

在 Visual Studio 中,只需执行以下操作 — 重新生成时,将在名为 的子目录中自动创建名为 的附属程序集。

测试附属程序集

若要模拟在具有不同语言的操作系统上运行,必须使用 Thread 类更改 CurrentUICulture:

System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo ("de");

CultureInfo.CurrentUICulture 是同一属性的只读版本。

注意

一个有用的测试策略是将 lѻ¢αlïʐɘ 转换为仍然可以读作英语的单词,但不要使用标准的罗马 Unicode 字符。

Visual Studio 设计器支持

Visual Studio 中的设计器为本地化组件和可视素提供了扩展支持。WPF 设计器具有自己的本地化工作流;其他基于组件的设计器使用仅设计时属性来使组件或 Windows 窗体控件看起来具有语言属性。若要针对其他语言进行自定义,只需更改 Language 属性,然后开始修改组件。属性为可本地化的控件的所有属性都将保存到该语言的 文件中。只需更改 Language 属性,即可随时在语言之间切换。

文化和亚文化

文化分为文化和亚文化。文化代表一种特定的语言;亚文化代表该语言的区域变体。.NET 运行时遵循 RFC1766 标准,该标准使用两个字母的代码表示区域性和子区域性。以下是英语和德语区域性的代码:

En de

以下是澳大利亚英语和奥地利德语亚文化的代码:

en-AU de-AT

区域性在 .NET 中使用 System.Globalization.CultureInfo 类表示。可以检查应用程序的当前区域性,如下所示:

Console.WriteLine (System.Threading.Thread.CurrentThread.CurrentCulture); Console.WriteLine (System.Threading.Thread.CurrentThread.CurrentUICulture);

在针对澳大利亚本地化的计算机上运行此函数说明了两者之间的区别:

en-AU en-US

CurrentCulture 反映 Windows 控制面板的区域设置,而 CurrentUICulture 反映操作系统的语言。

区域设置包括时区以及货币和日期的格式等。CurrentCulture 确定诸如 DateTime.Parse 之类的函数的默认行为。可以自定义区域设置,使其不再类似于任何特定区域性。

CurrentUICulture 确定计算机与用户通信的语言。澳大利亚不需要单独的英语版本,所以它只使用美国的英语版本。如果我在奥地利工作了几个月,我会转到控制面板并将我的 CurrentCulture 更改为奥地利德语。但是,鉴于我不会说德语,我的 CurrentUICulture 将仍然是美国英语。

默认情况下,资源管理器使用当前线程的 CurrentUICulture 属性来确定要加载的正确附属程序集。资源管理器在加载资源时使用回退机制。如果定义了亚文化程序集,则使用该组合体;否则,它将回退到通用区域性。如果泛型区域性不存在,它将回退到主程序集中的默认区域性。

加载、解析和隔离程序集

从已知位置加载程序集是一个相对简单的过程。我们将其称为。

但是,更常见的是,您(或 CLR)需要加载仅知道其完整(或简单)名称的程序集。这称为。程序集分辨率与加载的不同之处在于,必须首先找到程序集。

在两种情况下触发程序集解析:

  • 通过 CLR,当它需要解析依赖项时
  • 显式地,当您调用诸如 Assembly.Load(AssemblyName) 之类的方法时

为了说明第一种方案,请考虑一个包含主程序集和一组静态引用的库程序集(依赖项)的应用程序,如以下示例所示:

AdventureGame.dll // Main assembly Terrain.dll // Referenced assembly UIEngine.dll // Referenced assembly

通过“静态引用”,我们的意思是.dll是引用和编译的。编译器本身不需要执行程序集解析,因为它被告知(显式或由 MSBuild)在哪里查找 .dll 和 。在编译过程中,它将 Terrain 和 UIEngine 程序集的写入 的数据中,但没有关于在哪里可以找到它们的信息。因此,在运行时,必须地形和 UIEngine 程序集。

程序集加载和解析由 (ALC) 处理;具体来说,System.Runtime.Loader 中的 AssemblyLoadContext 类的一个实例。由于 是应用程序的主程序集,因此 CLR 使用 ( AssemblyLoadContext.Default ) 来解析其依赖关系。默认 ALC 首先通过查找并检查名为 的文件(该文件描述了在何处查找依赖项)来解决依赖项,或者如果不存在,它会在应用程序基文件夹中查找,在该文件夹中可以找到 和 。(默认 ALC 还会解析 .NET 运行时程序集。

作为开发人员,您可以在程序执行期间动态加载其他程序集。例如,您可能希望将可选功能打包到仅在购买这些功能后部署的程序集中。在这种情况下,您可以通过调用 Assembly 来加载额外的程序集(如果存在)。加载(程序集名称) .

一个更复杂的示例是实现插件系统,用户可以在其中提供应用程序在运行时检测和加载的第三方程序集,以扩展应用程序的功能。之所以出现复杂性,是因为每个插件程序集可能都有自己的依赖项,这些依赖项也必须解决。

通过子类化 AssemblyLoadContext 并重写其程序集解析方法 ( Load ),可以控制插件查找其依赖项的方式。例如,您可能决定每个插件都应驻留在其自己的文件夹中,并且其依赖项也应驻留在该文件夹中。

ALC 还有另一个用途:通过为每个 ALC 实例化一个单独的 AssemblyLoadContext(插件 + 依赖项),您可以保持每个 ALC 的隔离,确保它们的依赖项并行加载并且不会相互干扰(或主机应用程序)。例如,每个都可以有自己的 JSON.NET 版本。因此,除了和之外,ALC还提供了机制。在某些条件下,甚至可以ALC,从而释放其内存。

在本节中,我们将详细阐述这些原则中的每一个,并描述以下内容:

  • ALC 如何处理负载和分辨率
  • 默认 ALC 的角色
  • Assembly.Load and for context ALC
  • 如何使用程序集依赖项解析程序
  • 如何加载和解析非托管库
  • 卸载铝型铝
  • 旧程序集加载方法

然后,我们将理论付诸实践,并演示如何编写具有ALC隔离的插件系统。

注意

AssemblyLoadContext 类是 .NET 5 和 .NET Core 的新增功能。在 .NET Framework 中,ALC 存在,但受到限制和隐藏:创建和与它们交互的唯一方法是间接通过程序集类上的 LoadFile(string)、LoadFrom(string) 和 Load(byte[]) 静态方法。与 ALC API 相比,这些方法不灵活,它们的使用可能会导致意外(尤其是在处理依赖项时)。因此,最好支持在 .NET 5 和 .NET Core 中显式使用 AssemblyLoadContext API。

程序集加载上下文

正如我们刚才所讨论的,AssemblyLoadContext 类负责加载和解析程序集,并提供隔离机制。

每个 .NET 程序集对象只属于一个 AssemblyLoadContext 。您可以获取程序集的 ALC,如下所示:

Assembly assem = Assembly.GetExecutingAssembly(); AssemblyLoadContext context = AssemblyLoadContext.GetLoadContext (assem); Console.WriteLine (context.Name);

相反,您可以将 ALC 视为“包含”或“拥有”程序集,可以通过其 Assemblies 属性获取这些程序集。继上一个之后:

foreach (Assembly a in context.Assemblies) Console.WriteLine (a.FullName);

类还具有枚举所有 ALC 的静态 All 属性。

你可以通过实例化AssemblyLoadContext并提供一个名称来创建新的ALC(该名称在调试时很有帮助),尽管更常见的是,你首先要子类AssemblyLoadContext,以便你可以实现逻辑来依赖关系;换句话说,从程序集加载程序集。

加载程序集

AssemblyLoadContext 提供了以下方法来将程序集显式加载到其上下文中:

public Assembly LoadFromAssemblyPath (string assemblyPath); public Assembly LoadFromStream (Stream assembly, Stream assemblySymbols);

第一种方法从文件路径加载程序集,而第二种方法从 Stream(可以直接来自内存)加载程序集。第二个参数是可选的,对应于项目 debug () 文件的内容,该文件允许堆栈跟踪在代码执行时包含源代码信息(在异常报告中很有用)。

使用这两种方法时,不会进行。

下面将程序集 加载到其自己的 ALC 中:

var alc = new AssemblyLoadContext ("Test"); Assembly assem = alc.LoadFromAssemblyPath (@"c:\temp\foo.dll");

如果程序集有效,则加载将始终成功,但要遵守一条重要规则:程序集的在其 ALC 中必须是唯一的。这意味着不能将同名程序集的多个版本加载到单个 ALC 中;为此,您必须创建其他 ALC。我们可以加载另一个 的副本

var alc2 = new AssemblyLoadContext ("Test 2"); Assembly assem2 = alc2.LoadFromAssemblyPath (@"c:\temp\foo.dll");

请注意,源自不同程序集对象的类型是不兼容的,即使这些程序集在其他方面是相同的。在我们的示例中,assem 中的类型与 assem2 中的类型不兼容。

加载组件后,除非卸载其 ALC,否则无法卸载该组件(请参见)。CLR 在加载文件的持续时间内保持文件的锁定。

注意

您可以通过字节数组加载程序集来避免锁定文件:

bytes[] bytes = File.ReadAllBytes (@"c:\temp\foo.dll"); var ms = new MemoryStream (bytes); var assem = alc.LoadFromStream (ms);

这有两个缺点:

  • 程序集的“位置”属性最终将为空。有时,了解程序集的加载位置很有用(某些 API 依赖于填充程序集)。
  • 专用内存消耗必须立即增加,以适应程序集的完整大小。如果改为从文件名加载,CLR 将使用内存映射文件,这将启用延迟加载和进程共享。此外,如果内存不足,操作系统可以释放其内存并根据需要重新加载,而无需写入页面文件。

LoadFromAssemblyName

AssemblyLoadContext 还提供了以下方法,该方法按加载程序集:

public Assembly LoadFromAssemblyName (AssemblyName assemblyName);

与刚才讨论的两种方法不同,您不会传入任何信息来指示程序集所在的位置;相反,您正在指示 ALC 程序集。

解析程序集

上述方法触发。CLR 还会在加载依赖项时触发程序集解析。例如,假设程序集 A 静态引用程序集 B。为了解析引用 B,CLR 会在加载 程序集上触发程序集解析。

注意

CLR 通过触发程序集解析(无论触发程序集是默认程序集还是自定义 ALC)来解析依赖项。不同之处在于,使用默认 ALC,解析规则是硬编码的,而使用自定义 ALC,您可以自己编写规则。

然后会发生什么:

  1. CLR 首先检查该 ALC 中是否已发生相同的解析(具有匹配的完整程序集名称);如果是这样,它将返回之前返回的程序集。
  2. 否则,它将调用 ALC 的(虚拟受保护)Load 方法,该方法执行定位和加载程序集的工作。默认 ALC 的加载方法应用我们在中描述的规则。使用自定义 ALC,完全取决于您如何定位程序集。例如,您可以在某个文件夹中查找,然后在找到程序集时调用 LoadFromAssemblyPath。从相同或另一个 ALC 返回已加载的程序集也是完全合法的(我们在中演示了这一点)。
  3. 如果步骤 2 返回 null,则 CLR 将在默认 ALC 上调用 Load 方法(这用作解析 .NET 运行时和常见应用程序程序集的有用“回退”)。
  4. 如果步骤 3 返回 null,则 CLR 将在两个 ALC 上触发解析事件 — 首先在默认 ALC 上触发,然后在原始 ALC 上触发。
  5. (为了与 .NET Framework 兼容):如果程序集仍未解析,则会触发 AppDomain.CurrentDomain.AssemblyResolve 事件。
  6. 注意
  7. 此过程完成后,CLR 将执行“健全性检查”,以确保加载的任何程序集的名称都与请求的名称兼容。简单名称必须匹配;公钥标记必须匹配()。版本不需要匹配 – 它可以高于或低于请求的版本。

由此,我们可以看到有两种方法可以在自定义 ALC 中实现程序集解析:

  • 覆盖 ALC 的加载方法。这使您的ALC“首先决定”发生的事情,这通常是可取的(并且在您需要隔离时是必不可少的)。
  • 处理 ALC 的解析事件。仅当默认 ALC 解析程序集失败,才会触发此操作。

注意

如果将多个事件处理程序附加到 Resolve 事件,则第一个返回非 null 值的事件处理程序优先。

为了说明这一点,假设我们要加载一个主应用程序在编译时一无所知的程序集,称为 ,位于 (与我们的应用程序文件夹不同)。我们还假设 对 有私有依赖。我们希望确保当我们加载 并执行其代码时, 可以正确解析。我们还希望确保foo及其私有依赖项bar不会干扰主应用程序。

让我们首先编写一个覆盖 Load 的自定义 ALC:

using System.IO; using System.Runtime.Loader; class FolderBasedALC : AssemblyLoadContext { readonly string _folder; public FolderBasedALC (string folder) => _folder = folder; protected override Assembly Load (AssemblyName assemblyName) { // Attempt to find the assembly: string targetPath = Path.Combine (_folder, assemblyName.Name + ".dll"); if (File.Exists (targetPath)) return LoadFromAssemblyPath (targetPath); // Load the assembly return null; // We can’t find it: it could be a .NET runtime assembly } }

请注意,在 Load 方法中,如果程序集文件不存在,则返回 null。此检查很重要,因为 也将依赖于 .NET BCL 程序集;因此,Load 方法将在诸如 System.Runtime 之类的程序集上调用。通过返回 null,我们允许 CLR 回退到默认 ALC,这将正确解析这些程序集。

注意

请注意,我们没有尝试将 .NET 运行时 BCL 程序集加载到我们自己的 ALC 中。这些系统程序集不是设计为在默认 ALC 之外运行,尝试将它们加载到您自己的 ALC 中可能会导致不正确下降和意外的类型不兼容。

以下是我们如何使用自定义 ALC 在 中加载 程序集:

var alc = new FolderBasedALC (@"c:\temp"); Assembly foo = alc.LoadFromAssemblyPath (@"c:\temp\foo.dll"); ...

当我们随后开始在 foo 程序集中调用代码时,CLR 在某些时候将需要解决对 的依赖关系。此时,自定义 ALC 的 Load 方法将触发并在 中成功找到 程序集。

在这种情况下,我们的 Load 方法也能够解析 ,因此我们可以将代码简化为:

var alc = new FolderBasedALC (@"c:\temp"); Assembly foo = alc.LoadFromAssemblyName (new AssemblyName ("foo")); ...

现在,让我们考虑一个替代解决方案:我们可以实例化一个普通的 AssemblyLoadContext 并处理其解析事件,而不是子类化 AssemblyLoadContext 并覆盖 Load:

var alc = new AssemblyLoadContext ("test"); alc.Resolving += (loadContext, assemblyName) => { string targetPath = Path.Combine (@"c:\temp", assemblyName.Name + ".dll"); return alc.LoadFromAssemblyPath (targetPath); // Load the assembly }; Assembly foo = alc.LoadFromAssemblyName (new AssemblyName ("foo"));

现在请注意,我们不需要检查程序集是否存在。由于 Resolve 事件在默认 ALC 有机会解析程序集触发(并且仅在它失败时触发),因此我们的处理程序不会为 .NET BCL 程序集触发。这使得此解决方案更简单,尽管存在缺点。请记住,在我们的场景中,主应用程序在编译时对 或 一无所知。这意味着主应用程序本身可能依赖于称为 或 的程序集。如果发生这种情况,解析事件将永远不会触发,而是加载应用程序的 foo 和 bar 程序集。换言之,我们将无法实现。

注意

我们的 FolderBasedALC 类非常适合说明程序集解析的概念,但它在现实生活中的用处较少,因为它无法处理特定于平台和(对于库项目)开发时 NuGet 依赖项。在”中,我们描述了这个问题的解决方案,在中,我们给出了一个详细的例子。

默认 ALC

当应用程序启动时,CLR 会为静态 AssemblyLoadContext 分配一个特殊的 ALC。默认属性。默认 ALC 是加载启动程序集及其静态引用依赖项和 .NET 运行时 BCL 程序集的位置。

默认 ALC 首先在默认路径中查找以自动解析程序集(请参阅);这通常等同于应用程序的 . 和 . 文件中指示的位置。

如果 ALC 在其默认探测路径中找不到程序集,则会触发其解析事件。通过处理此事件,您可以从其他位置加载程序集,这意味着您可以将应用程序的依赖项部署到其他位置,例如子文件夹、共享文件夹,甚至作为宿主程序集内的二进制资源:

AssemblyLoadContext.Default.Resolving += (loadContext, assemblyName) => { // Try to locate assemblyName, returning an Assembly object or null. // Typically you’d call LoadFromAssemblyPath after finding the file. // ... };

当自定义 ALC 无法解析时(换句话说,当其 Load 方法返回 null 时),默认 ALC 中的 Resolve 事件也会触发,并且默认 ALC 无法解析程序集。

还可以从解析事件外部将程序集加载到默认 ALC 中。但是,在继续之前,您应该首先确定是否可以通过使用单独的 ALC 或使用我们在下一节中描述的方法(使用和 ALC)来更好地解决问题。硬编码为默认 ALC 会使代码变得脆弱,因为它不能作为一个整体进行隔离(例如,通过单测试框架或 LINQPad)。

如果仍要继续,最好调用(即 LoadFromAssemblyName)而不是(例如 LoadFromAssemblyPath),尤其是在程序集被静态引用的情况下。这是因为程序集可能已经加载,在这种情况下,LoadFromAssemblyName 将返回已加载的程序集,而 LoadFromAssemblyPath 将引发异常。

(使用 LoadFromAssemblyPath ,您还可以冒着从与 ALC 的默认解析机制不一致的位置加载程序集的风险。

如果程序集位于 ALC 不会自动找到它的位置,您仍然可以按照此过程进行操作,并另外处理 ALC 的解析事件。

请注意,调用 LoadFromAssemblyName 时,不需要提供全名;简单名称就可以了(即使程序集是强名称的,也是有效的):

AssemblyLoadContext.Default.LoadFromAssemblyName ("System.Xml");

但是,如果在名称中包含公钥标记,则它必须与加载的内容匹配。

默认探测

默认探测路径通常包括以下内容:

  • 中指定的路径(其中 是应用程序主程序集的名称)。如果此文件不存在,则改用应用程序基文件夹。
  • 包含 .NET 运行时系统程序集的文件夹(如果应用程序依赖于框架)。

MSBuild 自动生成一个名为 的文件,该文件描述了在何处查找其所有依赖项。其中包括放置在应用程序基文件夹中的与平台无关的程序集,以及放置在运行时子目录下(如 或 )下的特定于平台的程序集。

生成的 . 文件中指定的路径相对于应用程序基文件夹,或您在 AppName.runtimeconfig.json 和/或 配置文件的 additionalProbingPath 部分中指定的任何其他文件夹(后者仅适用于开发环境)。

“当前”的 ALC

在上一节中,我们警告不要将程序集显式加载到默认 ALC 中。相反,您通常想要的是加载/解析到“当前”ALC 中。

在大多数情况下,“当前”ALC 是包含当前正在执行的程序集的 ALC:

var executingAssem = Assembly.GetExecutingAssembly(); var alc = AssemblyLoadContext.GetLoadContext (executingAssem); Assembly assem = alc.LoadFromAssemblyName (...); // to resolve by name // OR: = alc.LoadFromAssemblyPath (...); // to load by path

以下是获取 ALC 的更灵活、更明确的方法:

var myAssem = typeof (SomeTypeInMyAssembly).Assembly; var alc = AssemblyLoadContext.GetLoadContext (myAssem); ...

有时,无法推断“当前”ALC。例如,假设您负责编写 .NET 二进制序列化程序(我们将 的联机补充中介绍序列化)。像这样的序列化程序写入它序列化的类型的全名(包括其程序集名称),必须在反序列化期间这些名称。问题是,您应该使用哪种 ALC?依赖正在执行的程序集的问题在于,它将返回包含反序列化程序的任何程序集,而不是反序列化程序的程序集。

最好的解决方案不是猜测,而是问:

public object Deserialize (Stream stream, AssemblyLoadContext alc) { ... }

明确可以最大限度地提高灵活性并最大限度地减少犯错的机会。调用方现在可以决定什么应该算作“当前”ALC:

var assem = typeof (SomeTypeThatIWillBeDeserializing).Assembly; var alc = AssemblyLoadContext.GetLoadContext (assem); var object = Deserialize (someStream, alc);

Assembly.Load and Concontext ALC

帮助处理将程序集加载到当前执行的 ALC 中的常见情况;那是

var executingAssem = Assembly.GetExecutingAssembly(); var alc = AssemblyLoadContext.GetLoadContext (executingAssem); Assembly assem = alc.LoadFromAssemblyName (...);

Microsoft已在程序集类中定义了以下方法

public static Assembly Load (string assemblyString);

以及接受 AssemblyName 对象的功能相同的版本:

public static Assembly Load (AssemblyName assemblyRef);

(不要将这些方法与传统的 Load(byte[]) 方法混淆,后者的行为方式完全不同 — 请参阅

与 LoadFromAssemblyName 一样,您可以选择指定程序集的简单名称、部分名称或全名:

Assembly a = Assembly.Load ("System.Private.Xml");

这会将 System.Private.Xml 程序集加载到的任何 ALC 中。

在本例中,我们指定了一个简单的名称。以下字符串也是有效的,并且在 .NET 中所有字符串的结果都相同:

"System.Private.Xml, PublicKeyToken=cc7b13ffcd2ddd51" "System.Private.Xml, Version=4.0.1.0" "System.Private.Xml, Version=4.0.1.0, PublicKeyToken=cc7b13ffcd2ddd51"

如果选择指定公钥标记,则它必须与加载的内容匹配。

注意

Microsoft开发人员网络 (MSDN) 警告不要从部分名称加载程序集,建议您指定确切的版本和公钥标记。它们的基本原理基于与 .NET Framework 相关的因素,例如全局程序集缓存和代码访问安全性的影响。在 .NET 5 和 .NET Core 中,不存在这些因素,从简单名称或部分名称加载通常是安全的。

这两种方法都严格用于,因此无法指定文件路径。(如果在 AssemblyName 对象中填充 CodeBase 属性,则将忽略该属性。

警告

不要落入使用 Assembly.Load 加载静态引用程序集的陷阱。在这种情况下,您需要做的就是引用程序集中的一个类型,并从中获取程序集:

Assembly a = typeof (System.Xml.Formatting).Assembly;

或者,您甚至可以这样做:

Assembly a = System.Xml.Formatting.Indented.GetType().Assembly;

这可以防止对程序集名称进行硬编码(将来可能会更改),同时在 ALC 上触发程序集解析(就像 一样)。

如果你要写大会.自己加载方法,它(几乎)看起来像这样:

[MethodImpl(MethodImplOptions.NoInlining)] Assembly Load (string name) { Assembly callingAssembly = Assembly.GetCallingAssembly(); var callingAlc = AssemblyLoadContext.GetLoadContext (callingAssembly); return callingAlc.LoadFromAssemblyName (new AssemblyName (name)); }

进入情境反思

集会。加载 使用调用程序集的 ALC 上下文的策略在程序集 失败时失败。负载通过中介(如反序列化程序或单测试运行程序)调用。如果中介在不同的程序集中定义,则使用中介的加载上下文,而不是调用方的加载上下文。

注意

我们之前在讨论如何编写反序列化程序时描述了这种情况。在这种情况下,理想的解决方案是强制调用方指定 ALC,而不是使用 Assembly.Load(string) 推断它。

但是,由于 .NET 5 和 .NET Core 是从 .NET Framework 演变而来的(在 .NET Framework 中,隔离是通过应用程序域而不是 ALC 完成的),因此理想的解决方案并不普遍,并且在无法可靠地推断 ALC 的情况下,有时会不恰当地使用 Assembly.Load(string)。一个例子是 .NET 二进制序列化程序。

要允许程序集 。加载在这种情况下仍然有效,Microsoft向AssemblyLoadContext添加了一个名为EnterContextualReflection的方法。这会将 ALC 分配给 AssemblyLoadContext。当前上下文反射上下文 .尽管这是一个静态属性,但其值存储在 AsyncLocal 变量中,因此它可以在不同的线程上保存单独的值(但仍会在整个异步操作中保留)。

如果此属性为非空,则程序集 .Load 自动使用它,而不是调用 ALC:

Method1(); var myALC = new AssemblyLoadContext ("test"); using (myALC.EnterContextualReflection()) { Console.WriteLine ( AssemblyLoadContext.CurrentContextualReflectionContext.Name); // test Method2(); } // Once disposed, EnterContextualReflection() no longer has an effect. Method3(); void Method1() => Assembly.Load ("..."); // Will use calling ALC void Method2() => Assembly.Load ("..."); // Will use myALC void Method3() => Assembly.Load ("..."); // Will use calling ALC

我们之前演示了如何编写功能类似于 汇编 的方法。负荷。下面是一个更准确的版本,它考虑了上下文反射上下文:

[MethodImpl(MethodImplOptions.NoInlining)] Assembly Load (string name) { var alc = AssemblyLoadContext.CurrentContextualReflectionContext ?? AssemblyLoadContext.GetLoadContext (Assembly.GetCallingAssembly()); return alc.LoadFromAssemblyName (new AssemblyName (name)); }

尽管上下文反射上下文在允许旧代码运行方面很有用,但更可靠的解决方案(如前所述)是修改调用 Assembly.Load 的代码,使其改为在调用方传入的 ALC 上调用 LoadFromAssemblyName。

注意

.NET Framework 没有等效的 EnterContextualReflection,也不需要它,尽管具有相同的程序集。加载方法。这是因为使用 .NET Framework,隔离主要通过而不是 ALC 实现。应用程序域提供了更强的隔离模型,其中每个应用程序域都有自己的默认加载上下文,因此即使仅使用默认加载上下文,隔离仍然可以工作。

加载和解析非托管库

ALC 还可以加载和解析本机库。调用标有 [DllImport] 属性的外部方法时,将触发本机解析:

[DllImport ("SomeNativeLibrary.dll")] static extern int SomeNativeMethod (string text);

由于我们没有在 [DllImport] 属性中指定完整路径,因此调用 SomeNativeMethod 会在包含定义 SomeNativeMethod 的程序集的任何 ALC 中触发解析。

ALC 中的虚拟方法称为 LoadUnmanagedDll ,方法称为 LoadUnmanagedDllFromPath:

protected override IntPtr LoadUnmanagedDll (string unmanagedDllName) { // Locate the full path of unmanagedDllName... string fullPath = ... return LoadUnmanagedDllFromPath (fullPath); // Load the DLL }

如果找不到该文件,可以返回 IntPtr.Zero 。然后,CLR 将触发 ALC 的 ResolvevingUnmanagedDll 事件。

有趣的是,LoadUnmanagedDllFromPath 方法受到保护,因此通常无法从 ResolvevingUnmanagedDll 事件处理程序调用它。但是,您可以通过调用静态 NativeLibrary.Load 来获得相同的结果:

someALC.ResolvingUnmanagedDll += (requestingAssembly, unmanagedDllName) => { return NativeLibrary.Load ("(full path to unmanaged DLL)"); };

尽管本机库通常由 ALC 解析和加载,但它们并不“属于”ALC。加载后,本机库将独立存在,并负责解析它可能具有的任何传递依赖项。此外,本机库是进程的全局库,因此如果它们具有相同的文件名,则无法加载两个不同版本的本机库。

程序集依赖解析程序

在中,我们说过默认 ALC 会读取 .deps.json 和 . 文件(如果存在),以确定在何处查找以解析特定于平台和开发时 NuGet 依赖项。

如果要将程序集加载到具有特定于平台或 NuGet 依赖项的自定义 ALC 中,则需要以某种方式重现此逻辑。可以通过分析配置文件并仔细遵循特定于平台的名字对象上的规则来实现此目的,但这样做不仅困难,而且如果规则在更高版本的 .NET 中发生更改,则编写的代码将中断。

程序集依赖解析器类解决了这个问题。若要使用它,请使用要探测其依赖项的程序集的路径实例化它:

var resolver = new AssemblyDependencyResolver (@"c:\temp\foo.dll");

然后,若要查找依赖项的路径,请调用 ResolveAssemblyToPath 方法:

string path = resolver.ResolveAssemblyToPath (new AssemblyName ("bar"));

在没有 . 文件的情况下(或者如果 . 不包含任何与 bar.dll) 相关的内容,这将计算为 。

同样,可以通过调用 ResolveUnmanagedDllToPath 来解析非托管依赖项。

说明更复杂的方案的一个好方法是创建一个名为 ClientApp 的新控制台项目,然后添加对 的 NuGet 引用。添加以下类:

using Microsoft.Data.SqlClient; namespace ClientApp { public class Program { public static SqlConnection GetConnection() => new SqlConnection(); static void Main() => GetConnection(); // Test that it resolves } }

现在构建应用程序并查看输出文件夹:您将看到一个名为 的文件。但是,此文件在运行时加载,尝试显式加载它会引发异常。实际加载的程序集位于(或)子文件夹中;默认 ALC 知道加载它,因为它解析 文件。

如果要尝试从另一个应用程序加载 .dll 程序集,则需要编写一个可以解析其依赖项的 ALC,。这样做时,仅查看 所在的文件夹是不够的(就像中所做的那样)。相反,您需要使用 AssemblyDependencyResolver 来确定该文件对于正在使用的平台的位置:

string path = @"C:\source\ClientApp\bin\Debug\netcoreapp3.0\ClientApp.dll"; var resolver = new AssemblyDependencyResolver (path); var sqlClient = new AssemblyName ("Microsoft.Data.SqlClient"); Console.WriteLine (resolver.ResolveAssemblyToPath (sqlClient));

在 Windows 计算机上,这将输出以下内容:

C:\source\ClientApp\bin\Debug\netcoreapp3.0\runtimes\win\lib\netcoreapp2.1 \Microsoft.Data.SqlClient.dll

我们在中给出了一个完整的示例。

卸载铝型铝

在简单的情况下,可以卸载非默认的 AssemblyLoadContext ,释放内存并释放它加载的程序集上的文件锁。为此,ALC 必须已使用 isCollectible 参数 true 进行实例化:

var alc = new AssemblyLoadContext ("test", isCollectible:true);

然后,可以在 ALC 上调用 Unload 方法来启动卸载过程。

卸载模型是合作的,而不是抢占的。如果 ALC 的任何程序集中的任何方法正在执行,则卸载将延迟到这些方法完成。

实际卸载发生在垃圾收集期间;如果来自 ALC 外部的任何内容对 ALC 内部的任何内容(包括对象、类型和程序集)有任何(非弱)引用,则不会发生这种情况。API(包括 .NET BCL 中的 API)在静态字段或字典中缓存对象或订阅事件的情况并不少见,这使得创建防止卸载的引用变得容易,尤其是在 ALC 中的代码以非平凡的方式使用其 ALC 外部的 API 时。确定卸载失败的原因很困难,需要使用 WinDbg 等工具。

旧版加载方法

如果您仍在使用 .NET Framework(或编写面向 .NET Standard 的库,并希望支持 .NET Framework),您将无法使用 AssemblyLoadContext 类。加载是通过使用以下方法完成的:

public static Assembly LoadFrom (string assemblyFile); public static Assembly LoadFile (string path); public static Assembly Load (byte[] rawAssembly);

LoadFile 和 Load(byte[]) 提供隔离,而 LoadFrom 不提供。

解析是通过处理应用程序域的 AssemblyResolve 事件来实现的,该事件的工作方式类似于默认 ALC 的解析事件。

Assembly.Load(string) 方法也可用于触发解析,并以类似的方式工作。

加载自

LoadFrom 将程序集从给定路径加载到默认 ALC 中。这有点像调用AssemblyLoadContext.Default.LoadFromAssemblyPath,除了:

  • 如果默认 ALC 中已存在具有相同简单名称的程序集,则 LoadFrom 将返回该程序集,而不是引发异常。
  • 如果默认 ALC 中具有相同简单名称的程序集,并且发生了加载,则会为该程序集提供特殊的“LoadFrom”状态。此状态会影响默认 ALC 的解析逻辑,因为如果该程序集在同一有任何依赖项,则这些依赖项将自动解析。

注意

.NET Framework 有一个 (GAC)。如果程序集存在于 GAC 中,则 CLR 将始终从那里加载。这适用于所有三种加载方法。

LoadFrom 自动解析传递相同文件夹依赖项的能力可能很方便 – 直到它加载不应该加载的程序集。由于此类方案可能难以调试,因此最好使用 Load(string) 或 LoadFile 并通过处理应用程序域的 AssemblyResolve 事件来解析传递依赖项。这使您能够决定如何解析每个程序集,并允许调试(通过在事件处理程序中创建断点)。

加载文件和加载(字节[])

LoadFile 和 Load(byte[]) 将程序集从给定的文件路径或字节数组加载到新的 ALC 中。与 LoadFrom 不同,这些方法提供隔离,并允许您加载同一程序集的多个版本。但是,有两个注意事项:

  • 使用相同的路径再次调用 LoadFile 将返回以前加载的程序集。
  • 在 .NET Framework 中,这两种方法都首先检查 GAC 并从那里加载(如果程序集存在)。

使用 LoadFile 和 Load(byte[]) ,您最终会得到每个程序集的单独 ALC(请注意)。这样可以实现隔离,尽管它可能会使管理更加尴尬。

要解析依赖关系,请处理 AppDomain 的解析事件,该事件在所有 ALC 上触发:

AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => { string fullAssemblyName = args.Name; // return an Assembly object or null ... };

args 变量还包括一个名为 请求程序集 ,它告诉您哪个程序集触发了解析。

找到程序集后,可以调用程序集 。加载文件以加载它。

注意

可以使用 AppDomain.CurrentDomain.GetAssemblies() 枚举已加载到当前应用程序域中的所有程序集。这也适用于 .NET 5,它等效于以下内容:

AssemblyLoadContext.All.SelectMany (a => a.Assemblies)

编写插件系统

为了充分演示本节中介绍的概念,让我们编写一个插件系统,该系统使用可卸载的 ALC 来隔离每个插件。

我们的演示系统最初将包含三个 .NET 项目:

插件.通用 (库)

定义插件将实现的接口

资本化器(图书馆)

将文本大写的插件

Plugin.Host (控制台应用程序)

查找和调用插件

假设项目驻留在以下目录中:

c:\source\PluginDemo\Plugin.Common c:\source\PluginDemo\Capitalizer c:\source\PluginDemo\Plugin.Host

所有项目都将引用 Plugin.Common 库,并且不会有其他项目间引用。

注意

如果 Plugin.Host 引用 Capitalizer,我们就不会编写插件系统;中心思想是插件是在 Plugin.Host 和 Plugin.Common 发布后由第三方编写的。

如果您使用的是 Visual Studio,为了本演示,将所有三个项目放入一个解决方案中会很方便。如果这样做,请右键单击 Plugin.Host 项目,选择“构建依赖项”>“项目依赖项”,然后勾选 Capitalizer 项目。这会强制 Capitalizer 在运行 Plugin.Host 项目时构建,而不添加引用。

插件.常见

让我们从Plugin.Common开始。我们的插件将执行一个非常简单的任务,即转换字符串。以下是我们如何定义接口:

namespace Plugin.Common { public interface ITextPlugin { string TransformText (string input); } }

这就是Plugin.Common的全部内容。

资本化器(插件)

我们的 Capitalizer 插件将引用 Plugin.Common 并包含一个类。现在,我们将保持逻辑简单,以便插件没有额外的依赖项:

public class CapitalizerPlugin : Plugin.Common.ITextPlugin { public string TransformText (string input) => input.ToUpper(); }

如果同时生成这两个项目并查看 Capitalizer 的输出文件夹,您将看到以下两个程序集:

Capitalizer.dll // Our plug-in assembly Plugin.Common.dll // Referenced assembly

插件主机

Plugin.Host 是一个具有两个类的控制台应用程序。第一个类是用于加载插件的自定义 ALC:

class PluginLoadContext : AssemblyLoadContext { AssemblyDependencyResolver _resolver; public PluginLoadContext (string pluginPath, bool collectible) // Give it a friendly name to help with debugging: : base (name: Path.GetFileName (pluginPath), collectible) { // Create a resolver to help us find dependencies. _resolver = new AssemblyDependencyResolver (pluginPath); } protected override Assembly Load (AssemblyName assemblyName) { // See below if (assemblyName.Name == typeof (ITextPlugin).Assembly.GetName().Name) return null; string target = _resolver.ResolveAssemblyToPath (assemblyName); if (target != null) return LoadFromAssemblyPath (target); // Could be a BCL assembly. Allow the default context to resolve. return null; } protected override IntPtr LoadUnmanagedDll (string unmanagedDllName) { string path = _resolver.ResolveUnmanagedDllToPath (unmanagedDllName); return path == null ? IntPtr.Zero : LoadUnmanagedDllFromPath (path); } }

在构造函数中,我们传入主插件程序集的路径以及一个标志,以指示我们是否希望 ALC 是可收集的(以便可以卸载它)。

Load 方法是我们处理依赖项解析的地方。所有插件都必须引用 Plugin.Common,以便它们可以实现 ITextPlugin 。这意味着 Load 方法将在某个时候触发以解析 Plugin.Common。我们需要小心,因为插件的输出文件夹可能不仅包含 .dll,还包含它自己的 副本。如果我们要把 的副本加载到 PluginLoadContext 中,我们最终会得到程序集的两个副本:一个在主机的默认上下文中,另一个在插件的 PluginLoadContext 中。程序集将不兼容,主机会抱怨插件没有实现 ITextPlugin!

为了解决这个问题,我们显式检查此条件:

 if (assemblyName.Name == typeof (ITextPlugin).Assembly.GetName().Name) return null;

返回 null 允许主机的默认 ALC 改为解析程序集。

注意

我们可以返回typeof(ITextPlugin),而不是返回null。组装 ,它也可以正常工作。我们如何确定 ITextPlugin 将在主机的 ALC 上解析,而不是在我们的 PluginLoadContext 上解析?请记住,我们的 PluginLoadContext 类是在 Plugin.Host 程序集中定义的。因此,从此类静态引用的任何类型都将在 Plugin.Host 的 ALC 上触发程序集解析。

检查公共程序集后,我们使用 AssemblyDependencyResolver 来查找插件可能具有的任何私有依赖项。(现在,不会有。

请注意,我们还重写了 LoadUnamangedDll 方法。这可确保如果插件具有任何非托管依赖项,这些依赖项也将正确加载。

在 Plugin.Host 中编写的第二个类是主程序本身。为简单起见,让我们对 Capitalizer 插件的路径进行硬编码(在现实生活中,您可能会通过在已知位置查找 DLL 或从配置文件中读取来发现插件的路径):

class Program { const bool UseCollectibleContexts = true; static void Main() { const string captializer = @"C:\source\PluginDemo\" + @"Capitalizer\bin\Debug\netcoreapp3.0\Capitalizer.dll"; Console.WriteLine (TransformText ("big apple", captializer)); } static string TransformText (string text, string pluginPath) { var alc = new PluginLoadContext (pluginPath, UseCollectibleContexts); try { Assembly assem = alc.LoadFromAssemblyPath (pluginPath); // Locate the type in the assembly that implements ITextPlugin: Type pluginType = assem.ExportedTypes.Single (t => typeof (ITextPlugin).IsAssignableFrom (t)); // Instantiate the ITextPlugin implementation: var plugin = (ITextPlugin)Activator.CreateInstance (pluginType); // Call the TransformText method return plugin.TransformText (text); } finally { if (UseCollectibleContexts) alc.Unload(); // unload the ALC } } }

让我们看一下 TransformText 方法。我们首先为插件实例化一个新的 ALC,然后要求它加载主插件程序集。接下来,我们使用 Reflection 来定位实现 ITextPlugin 的类型(我们将在第 中详细介绍)。然后,我们实例化插件,调用 TransformText 方法,并卸载 ALC。

注意

如果需要重复调用 TransformText 方法,更好的方法是缓存 ALC,而不是在每次调用后卸载它。

下面是输出:

BIG APPLE

添加依赖项

我们的代码完全能够解析和隔离依赖项。为了说明这一点,让我们首先添加对 的 NuGet 引用。您可以通过Visual Studio UI或将以下素添加到文件来执行此操作:

 <ItemGroup> <PackageReference Include="Humanizer.Core" Version="2.6.2" /> </ItemGroup>

现在,修改资本插件,如下所示:

using Humanizer; namespace Capitalizer { public class CapitalizerPlugin : Plugin.Common.ITextPlugin { public string TransformText (string input) => input.Pascalize(); } }

如果重新运行该程序,输出现在将是这样的:

BigApple

接下来,我们创建另一个名为 Pluralizer 的插件。创建一个新的 .NET 库项目,并添加对 的 NuGet 引用:

 <ItemGroup> <PackageReference Include="Humanizer.Core" Version="2.7.9" /> </ItemGroup>

现在,添加一个名为 复数器插件 。这将类似于 资本化插件 ,但我们调用 Pluralize 方法,改为:

using Humanizer; namespace Pluralizer { public class PluralizerPlugin : Plugin.Common.ITextPlugin { public string TransformText (string input) => input.Pluralize(); } }

最后,我们需要在 Plugin.Host 的 Main 方法中添加代码来加载和运行 Pluralizer 插件:

 static void Main() { const string captializer = @"C:\source\PluginDemo\" + @"Capitalizer\bin\Debug\netcoreapp3.0\Capitalizer.dll"; Console.WriteLine (TransformText ("big apple", captializer)); const string pluralizer = @"C:\source\PluginDemo\" + @"Pluralizer\bin\Debug\netcoreapp3.0\Pluralizer.dll"; Console.WriteLine (TransformText ("big apple", pluralizer)); }

输出现在将如下所示:

BigApple big apples

若要完全了解发生了什么,请将 UseCollectibleContexts 常量更改为 false,并将以下代码添加到 Main 方法以枚举 ALC 及其程序集:

foreach (var context in AssemblyLoadContext.All) { Console.WriteLine ($"Context: {context.GetType().Name} {context.Name}"); foreach (var assembly in context.Assemblies) Console.WriteLine ($" Assembly: {assembly.FullName}"); }

在输出中,您可以看到两个不同版本的 Humanizer,每个版本都加载到自己的 ALC 中:

Context: PluginLoadContext Capitalizer.dll Assembly: Capitalizer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=... Assembly: Humanizer, Version=2.6.0.0, Culture=neutral, PublicKeyToken=... Context: PluginLoadContext Pluralizer.dll Assembly: Pluralizer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=... Assembly: Humanizer, Version=2.7.0.0, Culture=neutral, PublicKeyToken=... Context: DefaultAssemblyLoadContext Default Assembly: System.Private.CoreLib, Version=4.0.0.0, Culture=neutral,... Assembly: Host, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ...

注意

即使两个插件都使用相同的 Humanizer 版本,隔离单独的程序集仍然是有益的,因为每个程序集都有自己的静态变量。

2024最新激活全家桶教程,稳定运行到2099年,请移步至置顶文章:https://sigusoft.com/99576.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。 文章由激活谷谷主-小谷整理,转载请注明出处:https://sigusoft.com/17584.html

(0)
上一篇 2024年 9月 16日
下一篇 2024年 9月 16日

相关推荐

关注微信