破解 .NET 2.0 配置之谜
最大化您对 .NET 2.0 配置框架的理解,避免常见陷阱,并深入了解配置在各种场景和环境中的工作细节。
引言
.NET 的一个出色特性是其 XML 配置功能。在 .NET 1.x 时代,常见的应用程序设置、数据库连接字符串、ASP.NET Web 服务器配置和基本自定义配置数据可以存储在 *.config 文件中。自定义配置节可以使用一些基本但自定义的结构,允许在 *.config 文件中存储少量信息。然而,更复杂的配置通常通过自定义 XML 结构和自定义解析代码来完成。虽然这样的代码可能变得相当复杂,并且有多种性能不同的方法可以完成同一件事。
有了 .NET 2.0,编写自己的(可能复杂、性能不佳且繁琐的)代码来管理自定义 XML 配置结构的时代已经结束。 .NET 2.0 中内置 XML 配置子系统的自定义配置功能得到了极大的改进,提供了非常有用的、节省时间的功能。通过相对简单的操作,您可能需要的几乎任何 XML 配置结构都可以通过少量的工作实现。此外,*.config 文件中 XML 的反序列化始终可以被覆盖。这使得在不丢失 .NET 2.0 配置其他高级功能的情况下支持任何必要的 XML 结构成为可能。
揭示细节
本文是 .NET 2.0 配置系列文章的延续。本文旨在揭示 .NET 2.0 配置框架的所有细节和内部工作原理,以便开发人员更好地利用提供的广泛功能。如果您是 .NET 2.0 配置新手,或者尚未掌握类型验证和转换等概念,您应该先阅读之前的文章,可以在以下链接找到:
请注意,本文提供的代码示例仅用于阐明文章中的观点。**可编译和可下载的代码示例已包含在本系列的头两篇文章中。** 本文的目的是,故意比前两篇更详细和高级,而不是提供可编译、可运行的代码示例。相反,目标是揭示 .NET 2.0 配置框架的核心底层理论和重要细节。期望的结果是,任何有兴趣充分利用 .NET 2.0 配置的人,在阅读完本系列文章中包含的信息后,都能做到这一点。
配置主题
欢迎来到 .NET 2.0 配置之谜系列第三部分。本文将详细介绍 .NET 2.0 配置框架。我将揭示框架所有方面的鲜为人知的功能、能力、怪癖和内部工作原理。还将提供几个架构图,为将要讨论的细节提供视觉锚点。
- 配置结构
- 配置管理
- 配置表示
- 配置元数据
- 配置序列化- 序列化过程- 元素属性- 数据验证
- 数据转换
 
- 锁定信息
- 子元素/集合
 
- 元素属性
- 自定义序列化- 处理预序列化
- 手动序列化
 
- 反序列化过程- 默认集合
- 元素属性- 数据验证
- 数据转换
- 解析锁定信息
 
- 子元素/集合
- 应用锁定信息
 
- 自定义反序列化- 处理未识别的元素
- 处理未识别的属性
- 处理缺失的必需属性
- 处理后反序列化
- 手动反序列化
 
 
- 序列化过程
- Web 配置和位置- 与应用程序的区别
- 位置特定配置- ConfigurationLocation
 
 
配置结构
现代 .NET 应用程序有多种配置形式可供选择。二进制数据、INI 文件、数据库、XML,甚至是任意文本结构。根据环境、应用程序类型、使用因素等,您的配置存储需求可能会有所不同。但绝大多数情况下,大多数 .NET 应用程序(以及编写它们的程序员)只需要存储配置的能力……具体如何存储并不重要。大多数配置倾向于分层结构,并且通常需要手动编辑。这使得 XML 成为构建配置存储框架的理想平台。这也使得 .NET 2.0 配置框架对 .NET 开发人员非常具有吸引力。
分层配置
就 .NET 2.0 配置框架而言,“分层”是关键,而且不止一个方面。除了作为 XML 的应用并提供一个分层媒介来存储实际的配置设置之外,.NET 2.0 配置框架本身也是分层的。根据应用程序的上下文,存在多个配置文件,它们自然地按照分层顺序排列,并在代码请求配置时合并在一起。这种配置文件的分层结构允许设置在不同级别应用,从整个机器,到应用程序,甚至到被请求的单个用户或资源。
.NET 2.0 配置框架的分层结构可以在下图(图 1)中看到。关于此图的几个重要注意事项是配置文件的合并顺序以及配置可用的上下文。根据上下文的不同,可以配置的内容的性质以及这些设置的合并方式可能存在重要差异。
上下文
在 .NET 世界中,有两种主要的应用程序类型:Windows 应用程序和 Web 应用程序。Windows 应用程序,或称为可执行文件,具有相对简单的四层配置结构和合并过程。另一方面,Web 应用程序具有更复杂的结构,用于为特定位置应用和合并配置。这两种主要应用程序类型各自创建了一个配置上下文,并带有控制配置如何应用的规则。
独立于任何上下文的是机器级别的配置。机器级别配置适用于在 CLR 中运行的任何应用程序,以及 .NET 框架本身使用的所有其他配置选项的基本设置。令许多人惊讶的是,.NET 框架使用与本系列文章讨论的相同的配置框架。令许多人进一步惊讶的是,.NET 框架中观察到的任何配置功能都不是特殊的或专有的……您可以做同样的事情。
快速查看 machine.config 文件(位于 %SYSTEMROOT%\Microsoft.NET\Framework\v2.0.50727\CONFIG)会发现,这里定义了所有“预配置”或“默认”的 ConfigurationSectionGroup 和 ConfigurationSection 元素。例如 appSettings、connectionStrings、system.web 等。许多默认设置,例如节加密提供程序、默认 ASP.NET 成员、配置文件和角色提供程序等,都在这里应用。在同一目录中,您还应该注意到该文件的注释版本、默认版本以及多个不同安全级别的默认 web.config 文件。虽然编辑 machine.config 文件可能很危险,但这也是全局应用设置到每个应用程序的唯一方法。在机器级别之下,配置分为两种可用上下文……Exe 和 Web。
Exe 上下文可用于任何可执行应用程序。这包括 Windows Forms、Windows 服务和控制台应用程序。在此上下文中,核心配置基础结构总共了解四个级别的配置:机器、应用程序、漫游用户和用户。每个级别在“上下文”上依次更具体。因此,每个后续级别都可以覆盖更高处定义的设置。需要注意的是,漫游用户配置的特异性低于用户配置。这允许用户特定的设置驻留在桌面和笔记本电脑上,彼此独立,而漫游设置则在两者之间共享并在两者上可用。
Web 上下文仅适用于在 ASP.NET 主机中运行的应用程序(不一定是 IIS……任何 ASP.NET 主机都可以,包括 VS2005 开发服务器或使用 System.Web.Hosting.ApplicationHost 的自定义 Web 服务器)。与仅依赖进程和用户的 Exe 上下文不同,Web 上下文依赖于位置。配置可以专门针对网站位置,无论是通过 web.config 文件显式配置,还是通过多个 web.config 文件隐式合并。在 Web 配置上下文中,通常无法实现按用户进行的配置,即使用户已成功通过身份验证。
合并 (Merging)
.NET 2.0 配置的分层特性提供了高度的灵活性,允许特定用户或位置拥有自己的配置设置。但是,这些配置设置并非孤立的,在更具体的级别上进行的重复设置有能力覆盖在不太具体的级别上进行的设置。正如在图 1 中所见,最具体的配置文件会合并到不太具体的配置文件中,最具体的设置会覆盖最不具体的设置。在 Exe 上下文中,用户(更准确地说,本地用户)设置是最具体的,然后是漫游用户(在两台或多台计算机之间共享)、应用程序,最后是机器。
在 Web 上下文中,合并过程稍微复杂一些。除了多个 *.config 文件从依次更具体的位置合并(例如,当合并时,\wwwroot\siteA\web.config 比 \wwwroot\web.config 更具体)之外,还可以直接在应用程序的单个 web.config 文件中定义特定于位置的配置。本文稍后将更详细地讨论 web.config 和位置合并的细微差别。
配置架构
.NET 2.0 配置框架提供了实现自定义配置的快速、简单的方法,但它本身相当复杂。配置框架不仅提供了实现自定义配置的手段,还提供了实现自定义数据验证和转换、自定义提供程序的手段,暴露了自定义序列化和反序列化的钩子,甚至允许加密配置。关于每个配置元素的元数据也已公开,提供了有关已加载数据的“什么”、“在哪里”和“如何”的详细信息。图 2 显示了 .NET 2.0 配置框架的架构结构。图中每个元素的具体细节将是本文后续讨论的主要话题。
在这个看起来复杂的图表中隐藏着一个优雅、逻辑且高效的管理配置的系统。我试图将此架构的各个组件组织成逻辑组,这些组构成了本文的核心章节:
- 配置管理
- 检索、存储和查找或映射配置文件。
- 配置表示
- 用于表示配置节、元素和属性的存储结构。
- 配置元数据
- 有关配置来源、可用上下文以及其他各种详细信息。
- 配置序列化
- 加载和保存配置数据,以及自定义这些过程。
配置管理
第一个将详细讨论的概念也是逻辑上您在项目中处理配置时的第一个起点。在本篇文章的范围内,配置管理指的是查找配置(无论是直接查找,还是通过映射到特定配置文件),检索配置节以及在使用期间存储这些节。可以直接加载配置文件,提供额外功能并允许加载特定 *.config 文件(而不是默认映射的文件)。映射到特定配置文件因您所处的上下文而异,但只要授予了读取资源的必要权限,就可以加载任何配置文件。
ConfigurationManager
ConfigurationManager 和随后的 WebConfigurationManager 是访问 .NET 2.0 配置的主要起点。ConfigurationManager 提供了检索配置节及其包含的设置所需的所有核心服务,并提供方法允许显式加载 Exe 上下文中的配置文件。WebConfigurationManager 提供了在 Web 上下文中显式加载配置的其他方法,同时充当常用 ConfigurationManager 方法的代理。以下图表展示了这两个静态类以供参考。
ConfigurationManager 类提供的主要功能 GetSection(string sectionName) 已在本系列前几篇文章中详细介绍,此处不再赘述。默认情况下,ConfigurationManager 类提供隐式的只读访问配置。通常,配置需求不仅仅是读取易失性设置,还需要保存。ConfigurationManager 提供了多种方法来允许在更显式的上下文中打开配置文件。打开配置的第一种方法是使用 ConfigurationManager.OpenExeConfiguration() 方法。这些方法通过 Configuration 类提供对配置文件(或文件)的读/写访问。
OpenExeConfiguration() 方法有两种重载。一种重载接受一个字符串,表示当前运行的可执行文件的路径,另一种重载接受一个 ConfigurationUserLevel 枚举值。第一种方法将“.config”附加到您提供的文件名,并加载该配置文件。重要的是要注意 OpenExeConfiguration(string exePath) 是一个非常具有误导性的方法,因为文件名不必是正在运行的 .exe 的文件名。通过此方法,可以实现配置爱好者(到目前为止,根据我的互联网研究,还无法实现)的“圣杯”之一,前提是正确使用它。请考虑以下场景:
- 要求
- 在应用程序级别定义配置设置。
- 从名为 ConfiguredClassLibrary 的类库中消耗代码。
- 允许类库拥有自己的 *.dll.config 文件。
- 问题
- ConfigurationManager.GetSection() 仅返回主 Application.exe.config 文件中的节。
- ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None) 仅打开主 Application.exe.config 文件。
上述问题的解决方案,这是开发人员最常问的问题之一,就是 ConfigurationManager.OpenExeConfiguration(string exePath)。通过提供一个**不同于** EXE 文件名的文件名,可以打开一个备用的 *.config 文件。考虑以下场景的解决方案:
- 解决方案
- 将 ConfiguredClassLibrary.dll.config 复制到 ConfiguredClassLibrary.dll 相同的目录。
- 使用 ConfigurationManager.GetSection() 访问主应用程序设置。
- 调用 ConfigurationManager.OpenExeConfiguration("ConfiguredClassLibrary.dll") 来加载 DLL 特定配置文件。
- 使用之前加载的 Configuration 对象访问 ConfiguredClassLibrary.dll.config 设置。
上述场景(看起来足够频繁)允许同时使用多个配置文件(特定于任何程序集,不仅仅是主 EXE)。尽管 OpenExeConfiguration(string exePath) 是在 DLL 文件上调用的,但 ConfigurationManager 访问的配置文件不会改变。任何仍需访问存储在 Application.dll.config 中的设置的代码都可以继续进行,而不会有任何更改或冲突。以下代码可以演示此概念的简单证明:
// Assumes Library.dll and its corresponding Library.dll.config file exist, 
// are referenced and properly colocated with the .exe
Console.WriteLine("App (before): " + 
    ConfigurationManager.AppSettings["test"]);
Console.WriteLine("Loading Library.dll.config...");
Configuration other = 
    ConfigurationManager.OpenExeConfiguration("Library.dll");
Console.WriteLine("App (after): " + 
    ConfigurationManager.AppSettings["test"]);
Console.WriteLine("Lib (after): " + 
    other.AppSettings.Settings["test"].Value);
// Expected output
App (before): application
Loading Library.dll.config...
App (after): application
Lib (after): library
<!-- Application.exe.config -->
<configuration>
    <appSettings>
        <add key="test" value="application" />
    </appSettings>
</configuration>
<!-- Library.dll.config -->
<configuration>
    <appSettings>
        <add key="test" value="library" />
    </appSettings>
</configuration>
尽管 OpenExeConfiguration(string exePath) 的签名和名称具有误导性,但它是一个功能强大的方法,能够解决一个更频繁的配置问题。此方法还可以从 HTTP 路径加载配置,这在 ClickOnce 智能客户端部署中是一个可能出现的场景。使用 OpenExeConfiguration("http://someserver.com/clickonce/someapp.exe"),可以识别并从其正确的版本化文件夹(在安装和更新期间自动生成)加载 ClickOnce 部署应用程序的 *.config 文件。
第二种方法 OpenExeConfiguration(ConfigurationUserLevel level) 将加载指定配置级别的适当配置文件。Exe 上下文中可用的配置级别允许您指定是想要 exe、漫游用户还是本地用户配置。ConfigurationUserLevel 枚举值在命名方式上也有点误导。这可能导致对该方法的作用以及调用结果可能出现的配置类型的误解。每个值的实际含义如下:
- 无
- 指定没有“用户级别”。
- 无用户级别默认使用主 exe 配置文件。
- 此级别的内部配置路径为 MACHINE/EXE。
- 存储在 [AppPath]\[AppName].exe.config
- PerUserRoaming
- 指定加载漫游用户配置。
- 漫游用户配置是共享配置,会在所有漫游用户的系统之间复制。
- 此级别的内部配置路径为 MACHINE/EXE/ROAMING_USER。
- 存储在 [ApplicationDataPath]\[AppName]\[CodedPath]\[Version]\User.config。
- PerUserRoamingAndLocal
- 指定加载本地用户配置。
- 此级别的内部配置路径为 MACHINE/EXE/ROAMING_USER/LOCAL_USER。
- 存储在 [LocalApplicationDataPath]\[AppName]\[CodedPath]\[Version]\User.config。
请记住,配置是分层的并且是合并的。在请求漫游或本地用户配置时,会合并到 machine.config 的上层,从而得出应用程序在给定用户级别可用的完整配置。这种合并的一个有趣结果是,即使 User.config 文件不存在,您也可以请求漫游或本地用户配置。返回的 Configuration 对象将包含一个不存在的 FilePath,并且 HasFile 属性为 false。但是,仍然可以访问来自更高配置级别的任何节,并在适当的允许下保存对这些节的更改。保存先前不存在的级别的更改将创建相应的 User.config 文件。
漫游和本地用户配置设置是一个有趣的领域。 .NET 2.0 配置的一些更微妙的行为(取决于您如何看待它)会显现出来(或显露其丑陋之处)。尽管存在这些丑陋之处,但这些微妙的行为是选择使用 .NET 2.0 配置而非自定义解决方案的另一个重要原因。 .NET 配置框架提供了一些广泛的安全功能,允许锁定设置、节甚至节组(绝对锁定,或取决于访问它们的级别)。请考虑以下代码:
Configuration roamingCfg = 
ConfigurationManager.OpenExeConfiguration(
ConfigurationUserLevel.PerUserRoamingAndLocal);
CustomSection customSection = 
roamingCfg.GetSection("customSection") as CustomSection;
if (customSection == null)
{
    customSection = new CustomSection();
    roamingCfg.Sections.Add("customSection", customSection);
}
customSection.CustomValue = "local user value";
customSection.Save(ConfigurationSaveMode.Minimal);
此示例应打开本地 User.config 文件并获取(如果不存在则创建)一个 CustomSection。编辑此自定义节上的一个值并保存更改。最终目标是加载任何现有设置,或者在本地 User.config 文件尚不存在时创建该文件并包含新节和设置。这看似简单,但根据“customSection”先前是在漫游、exe 还是机器级别定义的,添加该节或编辑现有节可能不可能。这种情况可能经常发生,并且当配置量很大时会变得相当复杂。将在本文的“配置元数据”部分详细讨论编辑/保存用户配置级别配置的原因和解决方案。
除了上面讨论的之外,还有其他几种打开配置文件的方法。与 OpenExeConfiguration() 方法(它们对您的配置文件所在的默认位置做了一些假设)不同,OpenMappedExeConfiguration() 和 OpenMappedMachineConfiguration() 允许您显式指定 *.config 文件在磁盘上的位置。使用这些方法,您可以加载一个备用的 machine.config,从您自己选择的位置加载 User.config 文件(而不是让 .NET 框架决定一个复杂的路径)等。访问 machine.config 时,如果不需要自定义版本,则应使用 OpenMachineConfiguration()。有关如何使用这些方法和相应的 ConfigurationFileMap 类,请参见下面的 ConfigurationFileMap 类子部分。
ConfigurationManager 类公开的最后一个方法 RefreshSection(string sectionName) 是另一个针对我经常收到的问题的答案。上面描述的许多 OpenConfiguration 方法允许以读/写方式打开配置并保存更改。然而,有时保存的对配置的更改不会被 ConfigurationManager 类拾取(尤其是在 Web 环境中)。有许多方法可以解决这类问题,但最简单的方法是在保存后立即调用 RefreshSection 并提供适当的节名称。这(在大多数情况下……在极少数情况下,我曾报告调用它无效)应该会强制 ConfigurationManager 在下次调用 GetSection() 时从磁盘加载并重新解析指定的配置节。
WebConfigurationManager
ConfigurationManager 类虽然是完全访问 Exe 上下文配置的第一步,但在 Web 上下文中却远远不够。与可执行应用程序不同,Web 应用程序没有一个明确定义的本地用户可以为其创建 User.config 文件。事实上,Web 应用程序中根本不存在用户特定的配置。尽管如此,当考虑位置特定配置时,Web 配置可能会变得更加复杂。位置特定配置以及如何使用 WebConfigurationManager 充分利用它几乎可以写成一整篇文章。关于 WebConfigurationManager 和 ConfigurationLocations 的完整讨论将在本文稍后的章节中进行。
ConfigurationFileMap
ConfigurationFileMap 是 ConfigurationManager 的 OpenMappedExeConfiguration 和 OpenMappedMachineConfiguration 方法的重要组成部分。这些类允许指定 *.config 文件的特定路径,并且 OpenMapped 方法将在创建 Configuration 对象时执行所有适当的合并。ConfigurationFileMap 类表示一个机器配置文件映射,并且是调用 OpenMappedMachineConfiguration 所必需的。额外的文件映射类,ExeConfigurationFileMap 用于 Exe 上下文,WebConfigurationFileMap 用于 Web 上下文,是加载超出机器级别的配置所必需的。
ExeConfigurationFileMap
ExeConfigurationFileMap 允许您在调用 OpenMappedExeConfiguration() 时,专门配置机器、exe、漫游和本地配置文件的确切路径名,可以全部配置,也可以分步配置。您不必指定所有文件,但所有文件将在创建 Configuration 对象时被识别和合并。在使用 OpenMappedExeConfiguration 时,重要的是要理解,您请求的级别之上的所有配置级别都将始终被合并。如果您指定了一个自定义的 exe 和本地配置文件,但没有指定机器和漫游文件,则默认的机器和漫游文件将被找到并与指定的 exe 和用户文件合并。这可能会产生意外的后果,如果指定的文件的同步程度不如默认文件。
string appData = Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData);
string localData = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData);
ExeConfigurationFileMap exeMap = 
new ExeConfigurationFileMap();
exeMap.ExeConfigFilename = 
"C:\Application\Default.config";
exeMap.RoamingUserConfigFilename = 
Path.Combine(appData, @"Company\Application\Roaming.config");
exeMap.LocalUserConfigFilename = 
Path.Combine(localData, @"Company\Application\Local.config");
Configuration exeConfig = 
ConfigurationManager.OpenMappedExeConfiguration(
exeMap, ConfigurationUserLevel.None);
Configuration roamingConfig = 
ConfigurationManager.OpenMappedExeConfiguration(exeMap, 
ConfigurationUserLevel.PerUserRoaming);
Configuration localConfig = 
ConfigurationManager.OpenMappedExeConfiguration(exeMap, 
ConfigurationUserLevel.PerUserRoamingAndLocal);
Console.WriteLine("MACHINE/EXE: " + exeConfig.FilePath);
Console.WriteLine(
"MACHINE/EXE/ROAMING_USER: " + roamingConfig.FilePath);
Console.WriteLine(
"MACHINE/EXE/ROAMING_USER/LOCAL_USER: " + localConfig.FilePath);
WebConfigurationFileMap
WebConfigurationFileMap 允许您配置配置文件的虚拟路径,就像 ExeConfigurationFileMap 允许您为每个配置级别配置 *.config 文件一样。这与 Web 上下文中可用的位置依赖配置相关,将在本文的最后一节中详细介绍。
配置
如果 ConfigurationManager 类是通往配置的翡翠城的黄砖之路的第一步,那么 Configuration 类绝对是第二步。对 ConfigurationManager 类公开的 OpenConfiguration 方法的任何调用都将返回一个 Configuration 对象。Configuration 对象代表了您请求的任何用户配置级别、位置或文件映射的已合并配置。与 ConfigurationManager 不同,Configuration 类以读/写模式公开了配置节的完整详细信息。通过 Configuration 类访问时,可以创建、删除节和节组,并调整它们的安全性设置。
在查看了 图 5 中的图表后,您应该会发现 Configuration 类公开的信息和功能比 ConfigurationManager 多得多。其中一些信息是相同的,包括 AppSettings、ConnectionStrings 和 GetSection() 方法。除了 GetSection() 方法之外,Configuration 类还公开了 GetSectionGroup(string sectionGroupName),它允许您加载配置文件中定义的 ConfigurationSectionGroup 类。Configuration 类还公开了所有定义的 ConfigurationSections 和 ConfigurationSectionGroups 的集合。Configuration 类还公开了重载的 Save 和 SaveAs 方法,允许将修改保存回现有配置文件,或创建新配置文件。通过阅读前面的文章,您应该已经熟悉加载和保存节和节组了。
Configuration 类的一些尚未讨论且不显而易见的功能,是通过节和节组集合实现的。除了允许您加载和保存现有的配置节和节组之外,您还可以添加和删除节组。这是一个强大的功能,允许使用代码以编程方式创建配置文件。这在使用需要非基本应用程序功能设置的漫游或本地用户配置时非常有用。创建节时需要注意的一个重要因素。默认情况下,节只能在机器级别和 exe 级别定义。如果您需要添加一个新的配置节,即使该节仅用于漫游或本地用户 *.config 文件,您也必须首先在 exe 级别添加该节,然后修改用户级别的节设置。请考虑以下代码:
Configuration exeConfig = 
ConfigurationManager.OpenExeConfiguration(
ConfigurationUserLevel.None);
if (exeConfig.GetSection("customSection") == null)
{
    CustomSection section = new CustomSection();
    section.SectionInformation.AllowExeDefinition = 
ConfigurationAllowExeDefinition.MachineToLocalUser;
    exeConfig.Sections.Add("customSection", section);
    exeConfig.Save(ConfigurationSaveMode.Minimal);
}
Configuration userConfig = 
ConfigurationManager.OpenExeConfiguration(
ConfigurationUserLevel.PerUserRoamingAndLocal);
CustomSection section = 
userConfig.GetSection("customSection") as CustomSection;
section.SomeSetting = "some value";
userConfig.Save(ConfigurationSaveMode.Minimal);
在上面的代码第一次执行之前,EXE 和用户 *.config 文件应该如下所示:
<!-- EXE .config file -->
<configuration>
</configuration>
<!-- USER .config file -->
<!-- DOES NOT EXIST YET! -->
在上面的代码第一次执行之后,exe 和用户 *.config 文件应该如下所示:
<!-- EXE .config file -->
<configuration>
    <configSections>
        <section name="test" type="Example.CustomSection, Example" 
allowExeDefinition="MachineToLocalUser" />
    </configSections>
</configuration>
<!-- USER .config file -->
<configuration>
    <test>
        <add key="key" value="value" />
    </test>
</configuration>
定义权限(AllowDefinition 和 AllowExeDefinition)是在处理多个配置级别时的一个重要因素。它们是前面提到的“微妙行为”中的两种,可能会使处理 .NET 2.0 配置复杂化。定义权限的详细解释,以及其他可能产生类似微妙影响的设置,将在本文的配置元数据部分讨论。
ContextInformation
Configuration 类的有一个重要属性是 EvaluationContext 属性。此属性公开了 ContextInformation 类的实例,该实例提供了对上下文对象的访问以及一个标志,指示 Configuration 对象是否代表 machine.config,或应用程序或用户级别的 *.config 文件。上下文对象公开了基本但有用的信息,可用于简化可能需要更复杂逻辑的任务。将在本文的配置元数据部分详细讨论可用上下文类的详细解释。
配置表示
配置管理是访问配置的第一步,但远非整个过程中最重要的组成部分。配置的最终目标是以简单、逻辑的方式表示设置结构和设置数据。 .NET 2.0 配置框架的终极核心是 ConfigurationElement 类。ConfigurationElement 类对应于 *.config 文件中的实际 XML 元素。它提供了表示配置设置元素、其属性以及各种元数据所需的所有功能。ConfigurationElement 可以促进开发人员存储足够配置所需的大部分(如果不是全部)复杂需求。
在本系列的上一篇文章中,我们讨论了如何创建自定义 ConfigurationSections,并解释了在对象模型配置系统的范围内属性和元素是如何工作的。那些文章提供了对配置是什么以及 ConfigurationElement 所起作用的基本概述。然而,ConfigurationElement 不仅仅是访问您在 XML 文件中定义的设置的手段。在本节中,我将提供对 ConfigurationElement 类、其派生类及其提供的功能更低级别的视图,这些功能适用于您作为配置的创建者和使用者。
ConfigurationElement 类的主要目的是提供对强类型、已验证配置设置的访问。这些设置可能很简单,存储在当前元素的属性中,也可能很复杂,存储在子 ConfigurationElements 中。然而,设置表示只是 ConfigurationElement 提供的访问功能的一半。通过 ConfigurationElement 还可以访问几种类型的配置元数据,包括当前配置上下文、元素和属性的锁定信息、XML 源信息以及可能的解析错误列表。ConfigurationElement 类还为序列化和反序列化过程提供了几个钩子。元数据和序列化将在接下来的两个部分中详细讨论。
.NET 2.0 配置框架允许以两种方式定义配置“规范”:声明式和命令式。声明式方法通过在类属性上放置描述该属性如何对应于 XML 文件中某个元素的属性来实现。命令式方法是通过在静态构造函数中以编程方式预定义属性来实现。最终,这两种方法都完成了相同的事情,但它们实现的方式却大不相同。声明式方法通常被认为更简单,需要开发人员更少的工作。然而,这种实现上的简单性是以更高的处理成本为代价的,每当配置元素刷新时。
ConfigurationElement
ConfigurationElement 类的方法通常可以分为四组:解析(序列化和反序列化)、配置基础结构、设置数据和元数据。如果您快速浏览 图 6,您可能会注意到该类的大部分是非公共的。ConfigurationElement 的大多数方法和属性仅供派生类访问,只留下锁定信息、索引器和 IsReadOnly 方法是公共的。大多数受保护的成员也标记为 virtual,允许比前几篇文章中已经讨论的更大的扩展性。
讨论解析和配置元数据的章节将在本文稍后提供,定义和访问配置设置数据已在前面的文章中讨论过,此处不再赘述。虽然 ConfigurationElement 类的最后一个方面——基础结构,很少需要,但它可能是许多小问题的解决方案。我们将讨论的 ConfigurationElement 的基础结构方法如下:
- void Init()
- void InitializeDefault()
- bool IsModified()
- void ResetModified()
- bool IsReadOnly()
- void SetReadOnly()
- void ListErrors(IList errorList)
- void SetPropertyValue(ConfigurationProperty prop, object value, bool ignoreLocks)
- void Reset(ConfigurationElement parentElement)
- void Unmerge(ConfigurationElement sourceElement, ConfigurationElement parentElement, ConfigurationSaveMode saveMode)
尽管 Init() 和 InitializeDefault() 方法的文档明显不足,但它们有一些特定的且经常必要的用途。作为配置子系统基础结构的一部分,这些方法虽然没有明显的返回值,但在序列化和其他过程中会被内部调用。由于 ConfigurationElement 中的数据被缓存到内存中,可能需要很长时间,因此由于保存或重置,初始化可能会发生不止一次。有时,构造函数中的基本初始化不足以确保 ConfigurationElement 的内部状态在其生命周期内得到正确维护。为了确保您的 ConfigurationElement 的内部状态在适当的时候得到维护和更新,初始化应该在这两种方法中的一种或两种中执行。
Init() 方法是最常用的初始化方法,通常是自定义初始化所在的位置。在自己的类中实现 Init 方法时,必须小心地跟踪 ConfigurationElement 是否已经初始化。单个 ConfigurationElement 实例的重新初始化很少见,但是 Unmerge 操作(见下文讨论)和重置通常会复制 ConfigurationElements 并从原始元素重新填充它们。当初始状态对后续操作很重要时,实现 Init() 方法至关重要。了解何时调用此方法对于正确实现它很重要,并且可能调用它的主要原因列在下面:
- 在节反序列化期间
- 在初步元素创建期间,设置值之前- 在 ConfigurationSection 的序列化过程中,在 Unmerge 操作之前,始终会发生
- 在父元素属性首次创建 ConfigurationElement 时,在填充数据之前,可能会发生。
 
- 当新元素在内部创建并添加到 ConfigurationElementCollection 时- 可能在反序列化、元素重置或通过 Unmerge 操作期间发生
 
- 当手动将元素添加到 ConfigurationElementCollection 时
虽然关于 Init() 方法如何以及何时被调用的理论讨论对于填补空白和回答问题很重要,但它并没有回答所有问题。对于那些需要更全面了解该方法功能和可能用法的人,我建议使用 Reflector 来检查 System.Configuration 中的 KeyValueConfigurationElement 和 KeyValueConfigurationCollection 类。这两个类提供了何时可能在对象构造以外的时间进行初始化的最清晰示例,也是 .NET 2.0 框架中 Init 的一种实现方式。
与可能随时响应各种触发器调用的 Init 方法不同,InitializeDefault() 方法仅在一种情况下被直接调用。每当重置方法(通常响应 Unmerge 操作)被调用时,如果 ConfigurationElement 是 ConfigurationElement 层级的根元素(或唯一元素)(通常是 ConfigurationSection),就会调用 InitializeDefault。
修改和只读状态修改和只读状态是相当不言自明的常见对象状态。然而,就配置框架而言,理解修改和只读状态何时可以设置和重置是有帮助的。修改状态仅由 SetPropertyValue() 方法设置为 true,并仅由 ResetModified() 方法设置为 false。ResetModified 方法通常在保存配置节时调用。
SetReadOnly() 方法在初始配置节设置期间调用,并且几乎总是被调用。此方法是导致保存配置时最常见问题之一(只读异常)的原因。除了少数情况外,访问配置节及其元素(以读/写模式)的唯一方法是通过对 ConfigurationManager 或 WebConfigurationManager 类的 Open*Configuration() 方法进行调用来直接打开配置文件。此方法调用的另一个实例是通过重写的 ConfigurationSection.GetRuntimeObject() 方法。此方法的两个已知用途是 AppSettingsSection 和 ConnectionStringsSection 类,用于通过 ConfigurationManager 类公开 AppSettings 和 ConnectionStrings 集合的只读版本(通过 ConfigurationManager 访问的所有配置数据始终是只读的)。
列出配置错误ListErrors() 方法只有一个用途,而且很简单。在配置框架内部,它用于在稍后发生错误时,向抛出的异常提供错误列表(确切地说是 ConfigurationException 实例)。您可以覆盖此方法以将自己的错误列表添加到集合中。此列表仅在两种情况下填充:在节反序列化期间(解析错误通常不会单独抛出,而是被收集并包装在 ConfigurationErrorsException 中抛出),或通过调用 ElementInformation.Errors 属性。在节反序列化过程中,添加到此列表的最常见时间是自定义反序列化,这将在后面详细讨论。
设置属性值SetPropertyValue() 方法的自定义用途也很有限,但有一个非常特定的用途可以解决一些问题。通常,当设置您在自定义配置类中定义的配置属性时,此方法会被内部使用。在常规使用中,第三个参数设置为 false,确保在更改属性值之前应用到配置元素的任何锁定都生效。然而,某些情况可能需要无论应用了什么锁定都设置一个属性(例如,在重置期间)。SetPropertyValue 的自定义用法的最佳示例是 AppSettingsSection.Reset 重写,供那些对实际示例感兴趣的人。
重置元素重置是恢复锁定信息到默认值,重新应用继承的锁定信息并将属性值设置为从配置文件加载的值的基本过程。对 ConfigurationSection 的重置调用将级联到所有子元素等,递归地强制重置构成配置节的所有元素。在大多数情况下,理解 Reset 的工作原理并不重要。配置通常是只读使用的,或者当配置必须是读/写时,大多数需求都是基本的。然而,拦截 Reset 调用并修改它通常是必要的,当配置属性中存储的数据被缓存为另一种形式时(即,字符串被解码并在请求时缓存,需要在节重置时删除缓存的副本)。配置元素重置的基本工作原理通常不需要修改,但正如删除手动缓存数据的情况一样,有时需要对其进行增强,以确保元素被完全正确地重置。Reset 方法仅在两种情况下被调用……在 Unmerge 操作期间,或在配置元素的各种创建方法期间。
Unmerge 操作Unmerge 操作是配置节保存时执行的另一个基本过程。您应该还记得,在文章前面,配置文件是分层的,更具体的配置文件与不那么具体的配置文件合并。这种合并提供了从机器级别到特定用户配置(或在 Web 环境的情况下,通过特定网站或 Web 应用程序)的所有配置设置的单一统一视图。在较低级别定义的配置节可以在较高级别更改其设置。配置元素的集合可以由更高级别删除、更改甚至清除其子项。在应用程序中使用配置时,这种合并视图使得消耗配置非常简单,并且消除了对完全理解配置设置来源的许多需求。然而,当需要保存运行时修改的配置设置时,必须正确地取消合并。
考虑到,根据您正在处理和保存的配置级别,您可能正在添加、删除、清除和更改可能存在也可能不存在于您正在处理的级别(但**应**保存回当前级别)的设置,取消合并操作相当复杂。此处的具体细节(因为我自己并不完全清楚确切的工作原理)将不被讨论,但可以说,此过程处理将较低级别的继承设置与当前工作级别的设置分离。更重要的是理解,上面讨论的许多方法都在取消合并期间被调用,因为当前元素经常被克隆、重置或初始化,并用应该保存的适当数据重新填充。这些数据是什么取决于调用 Configuration.Save() 时选择的 ConfigurationSaveMode。
ConfigurationSection
ConfigurationSection 类是 ConfigurationElement 的派生类,这意味着它的行为可能相同,包含其自己的属性和子元素。除了继承 ConfigurationElement 的所有功能和核心行为外,ConfigurationSection 还添加了其自己的特定于配置文件根节点的功能。唯一公开的功能是 SectionInformation 元数据属性。此属性将在稍后详细讨论,它提供了有关配置节的详细信息,包括对其原始 XML 的访问。ConfigurationSection 添加了一些新的受保护方法,包括 DeserializeSection 和 SerializeSection。
DeserializeSection 方法仅调用基类 ConfigurationElement.DeserializeElement() 方法。在派生类中,可以覆盖此方法以提供自定义反序列化过程。AppSettingsSection 会覆盖此方法,通过使用 file="" 属性提供更精细的外部配置文件版本。自定义反序列化的详细讨论将在本文稍后进行。SerializeSection 方法更有趣。这是调用 Configuration.Save() 时调用的方法,它执行数据验证,然后执行取消合并,之后序列化包含配置节的取消合并版本的元素。可以在派生类中覆盖此方法以提供高级多文件配置,这将在本文稍后讨论。
ConfigurationElementCollection
ConfigurationElementCollection,与 ConfigurationSection 一样,也是 ConfigurationElement 的派生类。ConfigurationElementCollection 已在本系列之前的文章中讨论过,因此此处不作详细讨论。查看类图应该可以清楚地了解该类为派生集合提供的功能,最值得注意的是 Base* 方法。有一个方法值得注意,那就是 OnDeserializeUnrecognizedElement() 重写。当 ConfigurationElement 的反序列化代码遇到一个名称与 ConfigurationProperty 不直接对应的元素名称时,就会调用此方法。由于 ConfigurationElementCollection 类允许您自定义添加、删除和清除元素名称,因此必须覆盖此方法才能正确处理这些元素。我建议那些对如何处理未识别元素感兴趣的人(无论原因如何),使用 Reflector 审查此方法。的代码。
非元素容器
除了直接对应于 *.config 文件中配置元素的类之外,还有一些基本的组织类,它们松散地对应于节组。与 ConfigurationSection 不同,ConfigurationSectionGroup 在加载后不直接映射到配置元素,并且不能直接保存。节组和相关的子集合直接从配置记录填充,配置记录是一个类,它是用于处理原始 XML 数据并生成配置节的工厂、记录和各种支持类型的隐藏框架的一部分。另外两个类 ConfigurationSectionCollection 和 ConfigurationSectionGroupCollection 与 ConfigurationSectionGroup 以相同的方式处理。考虑到处理这些元素 XML 并填充它们的代码大部分是隐藏的,详细讨论它们可能不是必需的。它们只是提供了一种组织结构,使配置文件更整洁,更容易维护。
配置元数据
如果配置需要 .NET 2.0 提供的系统,那么它通常相当复杂。这种配置通常是分层的、相关的,并且对于支持一个或多个应用程序的可重用、易于维护的灵活架构至关重要。通常,在如此复杂的应用程序架构中,除了配置设置本身之外,还需要关于配置的更多信息。这些附加信息要么是为了支持配置本身的正确实现,要么是为了支持消费该配置的代码的使用。值得庆幸的是,.NET 2.0 配置框架充满了元数据,公开了您正在处理的配置的每一个可想象的细节。
ContextInformation
本文开头,我介绍了两种可能环境中的基本配置分层结构:可执行文件的配置和 Web 应用程序的配置。这两种配置环境定义了配置加载、合并、保存和取消合并的上下文。然而,上下文不仅仅是一个上下文,并且可以通过 ContextInformation 对象访问当前配置上下文的特定详细信息。ContextInformation 对象仅出现在两个位置……由 Configuration 对象和 ConfigurationElement 对象(及其所有派生类)公开。ContextInformation 类很简单,它提供了对更详细的上下文信息对象(HostingContext)的访问,以及一个指示当前配置对象是否为机器级别配置的标志。请注意,机器级别配置本质上是无上下文的,在 Exe 上下文和 Web 上下文中都存在并表现相同。
ExeContext
ExeContext 类是一个非常简单的类,只提供有关当前可执行应用程序路径和加载配置对象的当前 UserLevel 的信息。通常在加载 Configuration 对象时就知道这些信息,但如果通过 ConfigurationElement 对象访问上下文,则不一定知道 ConfigurationUserLevel,除非访问此对象。
WebContext
WebContext 类比 ExeContext 提供了更多细节,因为 Web 环境中的配置层级可能更复杂。通过 Web 上下文,您可以访问配置对象或元素加载的站点名称、虚拟路径、应用程序路径和位置子路径。此外,您还可以访问配置文件本身所在的 WebApplicationLevel。根据配置是 由根页面、子目录中的页面还是子 Web 应用程序访问的,实际的 *.config 文件可能位于比使用该配置的代码更高的或更低目录级别。
ConfigurationProperty
在 .NET 2.0 配置框架的所有元数据类中,ConfigurationProperty 应该是对阅读本系列前两篇文章的人来说最熟悉的。该类提供了定义给定配置节及其子元素中实际可配置设置的方法。尽管它很熟悉,但应该明确的是,ConfigurationProperty 确实是关于配置元素设置的元数据,并不直接表示存储在 *.config 文件中的 XML。除了在自定义配置类中用于定义哪些设置可用之外,ConfigurationProperty 类仅供配置框架内部使用。
关于 ConfigurationProperty 类的一个有趣的说明是,它提供了对 .NET 2.0 配置框架中仅有的两个非元数据、非配置支持类型的访问。每个配置属性都可以关联一个类型转换器和一个验证器,以方便本机类型数据和字符串之间的转换,并在属性值处于本机类型时进行验证。这两种支持类型为配置框架提供了很大的灵活性,使得可以在 XML 文件中存储特定的字符串结构,并在反序列化时将其转换为本机(可能是复杂的).NET 类型(并在序列化时转换回字符串)。
ConfigurationElementProperty
在 .NET 2.0 配置框架提供的所有元数据类中,ConfigurationElementProperty 可能是最奇怪的。该类只公开一个属性 Validator,您可能认为可以直接公开该属性。无论其奇怪性如何,ConfigurationElementProprety 为 ConfigurationElement 提供了直接访问分配的验证器的途径,以确保元素数据是正确的。在反序列化和序列化过程中,都会调用元素的验证器,以确保在任何时候都不会读取或写入无效数据。ConfigurationElementProperty 通常在构造期间分配,此时内部配置管理框架读取 *.config 文件并从定义的属性生成 ConfigurationElements。
ElementInformation
与 ConfigurationElementProperty 不同,ElementInformation 提供了一些非常有用的配置元素元数据。要访问 ElementInformation 对象,您必须引用 ConfigurationElement.ElementInformation 属性,因为这是它在框架中唯一暴露的位置。
此对象提供了一些基本标志,指示元素是否是集合元素 (IsCollection),是否已锁定 (IsLocked)(只能读取而不能以任何方式修改),以及元素是否在文件中存在 (IsPresent)。最后一个属性 IsPresent 乍一看可能有点奇怪。在绝大多数情况下,配置只会读取,几乎总是意味着配置元素是从配置文件读取的。然而,当您修改配置文件时,可以以编程方式添加元素、集合,甚至定义和添加整个配置节。当一个节已被添加但尚未保存到 *.config 文件时,IsPresent 将为 false。ElementInformation 对象还提供了另外两个有用的信息:Source 和 LineNumber。Source 属性返回从磁盘读取元素的 *.config 文件路径和文件名。使用 configSource 属性或 
除了基本标志和文件信息之外,ElementInformation 对象还提供了两个集合:Properties 和 Errors。Properties 集合公开了有关元素定义的每个属性的元数据列表。此 PropertyInformation 类与 ElementInformation 类类似,提供了关于每个单独属性的基本标志和源文件信息,以及一些特定于配置属性的附加详细信息。可以检查属性的默认值并将其与其当前值进行比较,这是一个非常有用的小功能。PropertyInformation 公开的大部分信息应该对任何使用过 ConfigurationPropety 类的人以及阅读过本系列前几篇文章的人来说都是熟悉的。
ElementInformation 公开的最后一个集合 Errors 也是一个非常有用的细节。通常,在反序列化过程中,当发生解析错误时,不会立即抛出硬异常。相反,会创建一个 ConfigurationException 并将其添加到集合中,该集合稍后会通过一个包含反序列化期间发生的所有错误的 ConfigurationErrorsException 抛出。ElementInformation 的 Errors 集合提供了对同一错误列表的访问。根据反序列化(或序列化,或配置元素可能执行的其他活动)期间发生的错误的具体性质,列表可能不完整,可能存在其他问题。但是,它有助于确定运行时配置发生了什么,从而可以自动解决或通知配置问题。
SectionInformation
SectionInformation 是另一个非常有用的元数据对象。此对象提供了相当多的权限和锁定信息,并提供了一些有趣的加密和加载机制。在大多数情况下,此类的权限和锁定属性可以使用各种属性在 XML 中设置。配置节权限和锁定是一个重要概念,在 machine.config 中得到了相当广泛的使用,并且也可能是更改继承设置或以编程方式编辑它们时遇到的许多奇怪问题的根源。
SectionInformation 通用属性讨论,即将推出!
ProtectedConfigurationProvider
加密配置节,即将推出!
致谢
在我终于结束这个系列(或者至少是这三篇文章……可能还会有更多)之前,我必须将功劳归于应得的人。我研究 .NET 2.0 配置已经有一年半的时间了,学习和撰写这些文章是一件非常有趣的事情。然而,如果没有一个叫做 Reflector 的小程序,所有这些都不可能实现,它是由 Lutz Roeder 编写的。作为一个需要了解**一切**细节的人,没有这个完美的宝石,我将在 .NET 软件开发的世界上迷失方向。它比任何文档提供的都让我对 .NET 框架有了更清晰、更深入的理解。更不用说,如果我没有在 .NET 源代码中探索,我今天可能仍然对 .NET 2.0 配置框架一无所知。
其次,我想感谢所有在之前的文章中提问或通过电子邮件向我发送问题的人。许多问题我能够凭先前知识直接回答,但许多问题带来了有趣的挑战,迫使我深入挖掘 .NET 2.0 配置源代码来找出答案。如果没有提供答案的持续挑战,我永远不会找到它们。希望本文能回答未来可能出现的任何问题,但对于那些喜欢冒险、生活在边缘的冒险家,我始终欢迎您的加入。
我还想感谢 **CodeProject 员工**帮助我维护这些文章以及他们的辛勤编辑工作。我是一个非常健谈的人,我只能想象保持我这些喋喋不休的内容的水平需要多少工作。;) 这些文章的工作肯定会继续下去,我必须感谢 CodeProject 员工随时发布未来的更新。我还想感谢 CodeProject 编辑在他们首次发布时选择了我写的文章和第二篇文章为“编辑之选”。他们对我的作者的信心激励我继续写这个系列,有时我甚至不确定自己是否有精力继续下去。
最后,我必须感谢微软提供 .NET 2.0 配置框架。在多年徒劳地尝试创建自己的可重用、简单、灵活、类型安全配置框架(并常常在一个迷雾突然出现的砖墙前从头开始)之后,我不再需要操心了。微软在 .NET 2.0 中提供的配置框架非常棒,提供了我希望的关于配置一个接一个应用程序的所有功能。我越了解 .NET 2.0 配置框架,它就越显示出 .NET 2.0 最成熟的方面之一,肯定是我最喜欢的工具之一。
文章修订历史
本文目前正在进行大量、积极的编辑。请经常回来查看新章节和更新。在此活跃期间,部分内容可能会更改或章节重新排列。如果这带来不便,我深表歉意,但许多人一直在要求这篇文章。考虑到本文的范围和细节,我认为最好在我完善和最终确定章节时分块发布。
- 1.2 [2007.08.25 09:15 PM] - 第二次更新,添加了配置元数据部分。
- 1.1 [2007.08.09 11:31 PM] - 第一次更新,修复了一些语法和拼写错误,并添加了配置表示部分。添加了一个致谢部分,感谢那些在我前进的道路上帮助过我的人。
- 1.0 [2007.07.21 12:04 AM] - 原始文章,光荣的错误、遗漏和所有!经过数月的调研,其他数月无暇调研,额外的调研,修订,编辑,更多修订和修订,是的,是的,调研 *喘气* *喘气* ... 希望您喜欢并从中受益!敬请期待更多……很多,很多,很多!:)





 
  
  
  
  
  
  
  
  
  
  
  
 