【http://transbot.blog.163.com/blog/static/54213961201041801910149/】
本文是翻译初译稿,插图未能正确显示,内容在出版时也可能发生更改。点此返回《CLR via C#》中文版主页。
2.5 程序集版本资源信息
AL.exe或CSC.exe生成一个PE文件程序集时,还会在PE文件中嵌入一个标准的Win32版本资源。用户可以查看文件的属性来检查这个资源。在应用程序代码中,可以调用System.Diagnostics.FileVersionInfo的static方法GetVersionInfo来获取并检查这些信息。图2-4显示的是JeffTypes.dll[1]属性对话框的“详细信息”选项卡。
图2-4 “JeffTypes.dll属性”对话框的“详细信息”选项卡
生成程序集时,应该使用一些定制attribute来设置版本资源字段,这些attrbitue是在源代码中应用于assembly这一级别的。图2-4的版本信息是用以下代码来生成的:
using System.Reflection;
// FileDescription版本信息
[assembly: AssemblyTitle("JeffTypes.dll")]
// Comments版本信息
[assembly: AssemblyDescription("This assembly contains Jeff's types")]
// CompanyName版本信息
[assembly: AssemblyCompany("Wintellect")]
// ProductName版本信息
[assembly: AssemblyProduct("Wintellect (R) Jeff's Type Library")]
// LegalCopyright版本信息
[assembly: AssemblyCopyright("Copyright (c) Wintellect 2010")]
// LegalTrademarks版本信息
[assembly:AssemblyTrademark("JeffTypes is a registered trademark of Wintellect")]
// AssemblyVersion版本信息
[assembly: AssemblyVersion("3.0.0.0")]
// FILEVERSION/FileVersion版本信息
[assembly: AssemblyFileVersion("1.0.0.0")]
// PRODUCTVERSION/ProductVersion版本信息
[assembly: AssemblyInformationalVersion("2.0.0.0")]
// 设置Language字段(参见2.6节“语言文化”)
[assembly:AssemblyCulture("")]
重要提示:Windows资源管理器的属性对话框明显遗漏了一些attribute值。最遗憾的是没有显示AssemblyVersion这个attribute的值,因为CLR加载程序集时会使用这个值,详情将在第3章讨论。
表2-4总结了版本资源字段以及和它们对应的定制attribute。如果使用AL.exe来生成程序集,可以用命令行开关来设置这些信息,而不必使用定制attribute。表2-4的第二列显示了与每个版本资源字段对应的AL.exe命令行开关。注意,C#编译器没有提供这些命令行开关。在这种情况下,最好是用定制attribute来设置这些信息。
表2-4 版本资源字段和对应的AL.exe开关/定制attribute
版本资源 | AL.exe开关 | 定制attribute/说明 |
FILEVERSION | /fileversion | System.Reflection.AssemblyFileVersionAttribute |
PRODUCTVERSION | /productversion | System.Reflection.AssemblyInformationalVersionAttribute |
FILEFLAGSMASK | (无) | 总是设为VS_FFI_FILEFLAGSMASK (在WinVer.h中定义为0x0000003F) |
FILEFLAGS | (无) | 总是0 |
FILEOS | (无) | 目前总是VOS__WINDOWS32 |
FILETYPE | /target | 如果指定了/target:exe或/target:winexe,就设为VFT_APP;如果指定了/target:library,就设为VFT_DLL |
FILESUBTYPE | (无) | 总是设为VFT2_UNKNOWN(该字段对于VFT_APP和VFT_DLL无意义) |
AssemblyVersion | /version | System.Reflection.AssemblyVersionAttribute |
Comments | /description | System.Reflection.AssemblyDescriptionAttribute |
CompanyName | /company | System.Reflection.AssemblyCompanyAttribute |
FileDescription | /title | System.Reflection.AssemblyTitleAttribute |
FileVersion | /version | System.Reflection.AssemblyFileVersionAttribute |
InternalName | /out | 设为指定的输出文件的名称(无扩展名) |
LegalCopyright | /copyright | System.Reflection.AssemblyCopyrightAttribute |
LegalTrademarks | /trademark | System.Reflection.AssemblyTrademarkAttribute |
OriginalFilename | /out | 设为输出文件的名称(无路径) |
PrivateBuild | (无) | 总是为空 |
ProductName | /product | System.Reflection.AssemblyProductAttribute |
ProductVersion | /productversion | System.Reflection.AssemblyInformationalVersionAttribute |
SpecialBuild | (无) | 总是空白 |
重要提示:在Visual Studio中新建一个C#项目时,会自动创建一个AssemblyInfo.cs文件。这个文件除了包含本节描述的所有程序集版本attribute之外,还包括要在第3章讨论的几个attribute。可直接打开AssemblyInfo.cs文件,并修改你的程序集特有的信息。Visual Studio 还提供了一个对话框来帮助你编辑这个文件中的程序集信息。为了打开这个对话框,请打开项目的属性对话框,然后在“应用程序”选项卡中单击“程序集信息”。随后会看到如图2-5所示的一个对话框。
图2-5 Visual Studio的“程序集信息”对话框
2.5.1 版本号上一节指出可以将几个版本号应用于一个程序集。所有这些版本号都具有相同的格式,每个都由4个以句点分隔的部分构成,如表2-5所示。
表2-5 版本号格式[2]
| major(主版本号) | minor(次版本号) | build(内部版本号) | revision(修订号) |
示例 | 2 | 5 | 719 | 2 |
表2-5展示了一个示例版本号:2.5.719.2。前两个编号构成了公众对一个版本的理解。公众会将这个例子看成是程序集的2.5版本。第三个编号719是程序集的build号。如果公司每天都要生成程序集,那么每天都应该递增这个build号。最后一个编号2指出当前build的修订次数。如果因为某个原因,公司某一天必须生成两次程序集(可能是为了修复一个重大bug),revision号就应该递增。
Microsoft使用的就是这个版本编号方案,而且强烈建议你也使用它。CLR未来的版本会为程序集新版本的加载提供更好的支持,而且假如新版本会妨碍一个现有的应用程序,还允许方便地还原到之前的一个版本。为了实现这种程度的版本支持,CLR希望修正了一处或多处bug的程序集具有和修正之前相同的major/minor版本号,但build/revision号要反映出这是包含了bug修复的一个维护(servicing)版本。加载一个程序集时,CLR会自动查找与请求的程序集的major/minor版本相匹配的一个已安装的最新维护版本。
前面说过,一个程序集有三个版本号和它关联。这会使局面复杂化,并造成大量混淆。所以,有必要解释一下每个版本号的用途以及它们的正确用法:
l AssemblyFileVersion 这个版本号存储在Win32版本资源中。它仅供参考,CLR既不会检查,也不会关心这个版本号。通常,可以先设置好版本号的major/minor部分,这是希望公众看到的版本号。然后,每进行一次生成,就递增build和revision部分。理想情况是Microsoft的工具(比如CSC.exe或者AL.ex)能自动更新build和revision号(根据生成时的日期和时间)。但实情并非如此。使用Windows资源管理器可以看到这个版本号。对一个客户的系统进行故障诊断时,它通常用于标识程序集的一个特定的版本。
l AssemblyInformationalVersion 这个版本号也存储在Win32版本资源中,且同样仅供参考。CLR既不会检查它,也不会关心它。这个版本号的作用是指出包含该程序集的一个产品的版本。例如,产品的2.0版本可能包含几个程序集,其中一个程序集被标记为版本1.0,因为它是一个新开发的程序集,在产品的1.0版本中是不存在的。通常,可以设置这个版本号的major/minor部分来代表产品的公开版本号。然后,每次打包所有程序集来生成一个完整的产品,就递增build/revision部分。
l AssemblyVersion 这个版本号存储在AssemblyDef清单元数据表中。CLR在绑定到强命名程序集时(第3章讨论),会使用这个版本号。这个版本号非常重要,它唯一性地标识了一个程序集。着手开发一个程序集时,应设好major/minor/build/revision部分。而且除非着手开发程序集的下一个可部署的版本,否则不应该更改它们。生成一个程序集时,引用的程序集的版本号会嵌入AssemblyRef表的记录项中。这意味着一个程序集将与所引用的程序集的一个特定的版本紧密绑定到一起。
2.6 语言文化除了版本号之外,程序集还将语言文化(culture)作为其身份标识的一部分。例如,我们可能有一个程序集是只面向德语用户的,另一个程序集是只面向瑞士德语用户的,第三个程序集是只面向美国英语用户的,以此类推。语言文化是用一个字符串来标识的,该字符串包含一个主标记和一个副标记(根据RFC1766)。表2-6展示了一些例子。
表2-6 程序集语言文化标记的例子
主标记 | 副标记 | 语言文化 |
de | (无) | 德语 |
de | AT | 奥地利德语 |
de | CH | 瑞士德语 |
en | (无) | 英语 |
en | GB | 英国英语 |
en | US | 美国英语 |
通常,在创建包含代码的一个程序集时,不会为其指定一种语言文化。这是因为代码只讲“逻辑”,一般不涉及语言文化所特有的东西。没有指定具体语言文化的程序集称为语言文化中性(culture neutral)。
如果设计的应用程序包含一些语言文化特有的资源,Microsoft强烈建议专门创建一个程序集,并在其中包含代码和应用程序的默认(或者基本)资源。生成这个程序集时,不要指定一种具体的语言文化。其他程序集通过引用这个程序集来创建和操纵它公开的类型。
然后,可以创建一个或者多个单独的程序集,只在其中包含语言文化特有的资源——不要包含任何代码。标记了一种具体的语言文化的程序集称为附属程序集(satellite assembly)。为附属程序集指定的语言文化应准确反映程序集中包含的资源的语言文化。针对想要支持的每一种语言文化,都应该创建一个单独的附属程序集。
我们通常使用AL.exe工具来生成附属程序集。之所以不用编译器,是因为附属程序集中不应包含任何代码。使用AL.exe时,可以使用/c[ulture]:text开关来指定一种目标语言文化。其中,text是一个语言文化字符串,比如“en-US”代表的就是美国英语。部署一个附属程序集时,应该把它保存到一个专门的子目录中,子目录的名称应该与语言文化的文本相匹配。例如,假定应用程序的基目录是C:\MyApp,那么与美国英语对应的附属程序集应该放到C:\MyApp\en-US子目录中。在运行时,可以使用System.Resources.ResourceManager类来访问一个附属程序集的资源。
注意:虽然不建议这么做,但创建包含代码的一个附属程序集是完全可以的。如果愿意,可以使用System.Reflection.AssemblyCultureAttribute这个定制attribute来指定语言文化,而不是使用AL.exe的/culture开关。例如:
// 将程序集的语言文化设为瑞士德语
[assembly:AssemblyCulture("de-CH")]
一般不应生成引用了附属程序集的一个程序集。换言之,程序集的AssemblyRef记录项只应引用语言文化中性的程序集。想访问包含在一个附属程序集中的类型或成员,应使用第23章“程序集加载和反射”描述的反射技术。
2.7 简单应用程序部署(私有部署的程序集)本章已解释了如何生成模块,以及如何将模块合并成一个程序集。接着要解释的是如何打包和部署所有这些程序集,使用户能够运行应用程序。
程序集的打包方式没有任何特殊要求。打包一组程序集最简单的方式就是直接复制所有文件。例如,可将所有程序集文件放到一张光盘上,将光盘分发给用户,执行上面的一个批处理程序,将光盘上的文件复制到用户硬盘上的一个目录。由于已经包含了所有依赖的程序集和类型,所以用户能直接运行应用程序,“运行时”会在应用程序的目录中查找引用的程序集。不需要对注册表进行任何修改就能运行程序。要卸载应用程序,删除所有文件就可以了——就是那么简单!
当然,也可使用其他机制来打包和安装程序集文件,比如使用.cab文件(通常在需要从Internet下载的时候使用,目的是压缩文件并缩短下载时间)。还可将程序集文件打包成一个MSI文件,以便由Windows Installer服务(MSIExec.exe)使用。使用MSI文件可实现程序集的“按需安装”(install on demand)——在CLR首次尝试加载程序集的时候才安装这个程序集。但这并不是MSI的新功能;非托管EXE和DLL文件也能进行按需加载。
注意:使用批处理程序或其他简单的“安装软件”,足以将应用程序“弄”到用户的机器上。但是,要在用户的桌面和“开始”菜单上创建快捷方式,仍需使用一款较高级的安装软件。除此之外,可以方便地备份和还原应用程序,或者把它从一台机器移动到另一台机器,但快捷方式仍需特殊处理。
当然,也可利用Visual Studio内建的一个机制来发布应用程序。具体做法是打开项目的属性对话框,选择“发布”选项卡。利用这张选项卡中的选项,可以让Visual Studio生成一个MSI文件,并将它复制到一个网站、FTP服务器或者文件路径。这个MSI文件还能安装系统必备组件,比如.NET Framework或Microsoft SQL Server 2008 Express Edition。最后,利用ClickOnce技术,应用程序还能自动检查更新,并在用户的机器上安装更新。
部署到和应用程序相同的目录中的程序集称为私有部署的程序集(privately deployed assembly),这是因为程序集文件不和其他任何应用程序共享(除非其他应用程序也部署到这个目录中)。私有部署的程序集为开发人员、最终用户和管理员带来了许多便利,因为只需把它们复制到一个应用程序的基目录,CLR便会加载它们,并执行其中的代码。除此之外,要卸载一个应用程序,从目录中删除程序集即可。这使备份和还原也变得异常简单。
之所以能实现这种简单的安装/移动/卸载,是因为每个程序集都用元数据指明了自己引用的程序集,不需要依靠注册表设置。另外,引用(别的程序集的)程序集限定了每个类型的作用域。也就是说,一个应用程序总是和它生成和测试时的那些类型绑定。即便另一个程序集恰好提供了一个同名类型,CLR也不可能加载那个程序集。这一点有别于COM。在COM中,类型是在注册表中登记的,造成机器上运行的任何应用程序都能使用那些类型。
第3章将讨论如何部署可由多个应用程序访问的共享程序集。
2.8 简单管理控制(配置)用户或管理员经常需要对应用程序执行时的一些方面进行控制。例如,管理员可能决定移动用户硬盘上的程序集文件,或者覆盖程序集清单中的一些信息。还有一些策略涉及版本控制,详情请参见第3章。
为了实现对一个应用程序的管理控制,可在应用程序目录中放置一个配置文件。应用程序的发布者可创建并打包这个文件。这样一来,安装程序会将配置文件安装到应用程序的基目录中。另外,计算机管理员或最终用户也能创建或修改该文件。CLR会解析该文件的内容,从而更改程序集文件的定位和加载策略。
配置文件包含的是XML代码,它既能和某个应用程序关联,也能和整个机器关联。通过使用一个单独的文件(而不是注册表设置),用户可以方便地备份文件,管理员也能将应用程序方便地复制到另一台机器——只需复制必要的文件,管理策略就会被复制过去。
第3章将进一步更详细探讨这个配置文件,目前只需对它有一个基本的认识。例如,假定一个应用程序的发布者希望将JeffTypes程序集文件部署到与应用程序的程序集文件不同的目录中。要求的目录结构如下:
AppDir目录(包含应用程序的程序集文件)
Program.exe
Program.exe.config(在下面讨论)
AuxFiles子目录(包含JeffTypes的程序集文件)
JeffTypes.dll
FUT.netmodule
RUT.netmodule
由于JeffTypes这个程序集的文件不在应用程序的基目录中,所以CLR无法定位并加载这些文件。运行这个应用程序,会抛出一个System.IO.FileNotFoundException异常。为了解决这个问题,发布者创建了一个XML格式的配置文件,并把它部署到应用程序的基目录。该文件的名称必须是应用程序的主程序集文件的名称,并附加一个.config扩展名,也就是Program.exe.config。配置文件的内容如下:
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="AuxFiles" />
</assemblyBinding>
</runtime>
</configuration>
CLR尝试定位一个程序集文件时,总是先在应用程序基目录中查找。如果没有找到,就查找AuxFiles子目录。可以为probing元素的privatePath attribute指定多个以分号分隔的路径。每个路径都相对于应用程序的基目录。不能指定一个绝对或相对路径来表示在应用程序基目录外部的一个目录。这个设计的出发点是应用程序能控制它的目录及其子目录,但不能控制其他目录。
探测程序集文件
CLR需要定位一个程序集时,会扫描几个子目录。以下是加载一个语言文化中性的程序集时的目录探测顺序(其中,firstPrivatePath和secondPrivatePath是通过配置文件的privatePath attribute来指定的):
AppDir\AsmName.dll
AppDir\AsmName\AsmName.dll
AppDir\firstPrivatePath\AsmName.dll
AppDir\firstPrivatePath\AsmName\AsmName.dll
AppDir\secondPrivatePath\AsmName.dll
AppDir\secondPrivatePath\AsmName\AsmName.dll
...
在这个例子中,如果JeffTypes程序集的文件部署到一个名为JeffTypes的子目录中,就不需要配置文件,因为CLR能自动扫描与目标程序集的名称相符的一个子目录。
如果没有在上述任何一个子目录中找到目标程序集,CLR会从头再来,用一个.exe扩展名替换.dll扩展名。如果仍然找不到程序集,就抛出FileNotFoundException异常。
附属程序集(satellite assembly)遵循类似的规则,只是CLR会在应用程序基目录下的一个子目录中查找它,子目录的名称与语言文化的名称相符。例如,假定向AsmName.dll应用了“en-US”语言文化,那么会探测以下目录:
C:\AppDir\en-US\AsmName.dll
C:\AppDir\en-US\AsmName\AsmName.dll
C:\AppDir\firstPrivatePath\en-US\AsmName.dll
C:\AppDir\firstPrivatePath\en-US\AsmName\AsmName.dll
C:\AppDir\secondPrivatePath\en-US\AsmName.dll
C:\AppDir\secondPrivatePath\en-US\AsmName\AsmName.dll
C:\AppDir\en-US\AsmName.exe
C:\AppDir\en-US\AsmName\AsmName.exe
C:\AppDir\firstPrivatePath\en-US\AsmName.exe
C:\AppDir\firstPrivatePath\en-US\AsmName\AsmName.exe
C:\AppDir\secondPrivatePath\en-US\AsmName.exe
C:\AppDir\secondPrivatePath\en-US\AsmName\AsmName.exe
C:\AppDir\en\AsmName.dll
C:\AppDir\en\AsmName\AsmName.dll
C:\AppDir\firstPrivatePath\en\AsmName.dll
C:\AppDir\firstPrivatePath\en\AsmName\AsmName.dll
C:\AppDir\secondPrivatePath\en\AsmName.dll
C:\AppDir\secondPrivatePath\en\AsmName\AsmName.dll
C:\AppDir\en\AsmName.exe
C:\AppDir\en\AsmName\AsmName.exe
C:\AppDir\firstPrivatePath\en\AsmName.exe
C:\AppDir\firstPrivatePath\en\AsmName\AsmName.exe
C:\AppDir\secondPrivatePath\en\AsmName.exe
C:\AppDir\secondPrivatePath\en\AsmName\AsmName.exe
如你所见,CLR会探测具有.exe或.dll扩展名的文件。由于这个探测过程可能非常耗时(尤其是CLR需要通过网络来查找文件的时候),所以在XML配置文件中,最好指定一个或者多个culture元素,以便限制CLR查找附属程序集时的探测过程。
这个XML配置文件的名称和位置要取决于应用程序的类型:
l 对于可执行应用程序(EXE),配置文件必须在应用程序的基目录中,而且必须采用EXE文件的全名作为文件名,再附加一个.config扩展名。
l 对于Microsoft ASP.NET Web窗体应用程序,文件必须在Web应用程序的虚拟根目录中,而且总是命名为Web.config。除此之外,子目录也可包含自己的Web.config文件,而且配置设置会得以继承。例如,位于http://Wintellect.com/Training的Web应用程序既会使用虚拟根目录中的Web.config设置,也会使用Training子目录自己的Web.config设置。
本节开头说过,配置设置可应用于某个程序或整个机器。安装.NET Framework时,它会创建一个Machine.config文件。机器上安装的每个版本的CLR都有一个对应的Machine.config文件。Machine.config文件位于以下目录:
%SystemRoot%\Microsoft.NET\Framework\version\CONFIG
其中,%SystemRoot%是Windows目录(一般是C:\WINDOWS),version是.NET Framework的具体版本号(形如v4.0.#####)。
Machine.config文件中的设置是机器上运行的所有应用程序的默认设置,优先于应用程序专用配置文件中的设置。所以,管理员为了创建一个适用于整台机器的策略,修改一个文件即可。然而,管理员和用户一般应该避免修改Machine.config文件,因为该文件的许多设置都有着太多的牵连,使我们难免顾此失彼。另外,我们经常都要对应用程序的设置进行备份和还原,只有将这些设置保存到应用程序专用的配置文件中,才能方便地做到这一点。