.NET 配置文件写入时损坏






4.33/5 (2投票s)
以编程方式添加新的 ConfigurationSectionGroup 会损坏带有多个组声明的声明节。
引言
我们有一个 Windows 服务应用程序,它由应用程序配置文件中的配置信息驱动。该服务旨在在预定的时间运行各种步骤,例如生成静态/缓存数据表。这些步骤在步骤组内按顺序运行,步骤组并行运行。该服务旨在通过配置文件进行自定义,包括执行参数以及步骤组和组内步骤的定义。为了帮助管理配置文件,我们开发了一个 Windows GUI 应用程序。当向现有配置文件添加新的步骤组并重新保存时,生成的配置文件会损坏,导致将来无法读取,即 .NET 机制写入的内容 .NET 机制无法读回。
背景
.NET 配置文件是 XML 文件,遵循定义的架构,允许自定义。有许多标准的配置结构,旨在集成 .NET 支持的各种功能,例如应用程序设置、运行时序列化、服务模型等。通过在配置文件开头声明自定义节(System.Configuration.ConfigurationSection
)和节组(System.Configuration.ConfigurationSectionGroup
)来实现自定义,如下所示:
<configuration>
<configSections>
<!-- Custom Declarations Go Here... -->
</configSections>
</configuration>
在我们的例子中,原始配置文件(在尝试添加新组之前)看起来有点像
<configuration>
<configSections>
<sectionGroup name="ServiceConfiguration" type="System.Configuration.ConfigurationSectionGroup, System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" >
<section name="Administrators" type="My.CustomSection" />
<sectionGroup name="ServiceStepGroups" type="My.StepGroups" >
<sectionGroup name="MyGroup1" type="My.StepGroup" />
</sectionGroup>
</sectionGroup>
</configSections>
<ServiceConfiguration>
<Administrators ... />
<ServiceStepGroups>
<MyGroup1 ... />
</ServiceStepGroups>
</ServiceConfiguration>
</configuration>
在这里,我们在 configSections
中声明了顶级自定义节组 ServiceConfiguration
,其中包含我们服务应用程序独有的所有业务类型配置信息。然后我们有一个管理员节,这与此讨论无关,但被保留下来作为演示如何定义自定义节的视觉效果。
ServiceStepGroups
被定义为自定义节组,它将依次托管其他节组,如 MyGroup1
,每个节组将定义该组内的步骤。
在 configSections
*声明*之后,我们从 ServiceConfiguration
*定义*开始。为简洁起见,内容被省略,与本文无关。相关的是以下要求:1) 自定义节和节组必须同时声明和定义;2) 定义布局的结构必须与声明匹配;3) 节组必须具有唯一名称,即使它们属于相同的 .NET 类型。
问题
以编程方式在代码中向 ServiceStepGroups
添加一个新的 My.StepGroup
(MyGroup2)然后重新保存,会通过违反(之前提到的)要求 2 和 3 来损坏配置文件,如下所示:
<configuration>
<configSections>
<sectionGroup name="ServiceConfiguration" type="System.Configuration.ConfigurationSectionGroup, System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" >
<section name="Administrators" type="My.CustomSection" />
<sectionGroup name="ServiceStepGroups" type="My.StepGroups" >
<sectionGroup name="MyGroup1" type="My.StepGroup" />
</sectionGroup>
</sectionGroup>
<sectionGroup name="ServiceConfiguration"> <!-- No type attribute, duplicate name and wrong place -->
<sectionGroup name="ServiceStepGroups"> <!-- No type attribute, duplicate name and wrong place -->
<sectionGroup name="MyGroup2" type="My.StepGroup" />
</sectionGroup>
</sectionGroup>
</configSections>
<ServiceConfiguration>
<Administrators ... />
<ServiceStepGroups>
<MyGroup1 ... />
<MyGroup2 ... /> <!-- This is in expected place -->
</ServiceStepGroups>
</ServiceConfiguration>
</configuration>
.NET 深度解析
经过艰苦的努力,我才理解了发生的事情。一般来说,我会认为这种行为是 .NET 代码的一个错误,因为它无法读回它写出的内容;然而,代码似乎是结构化和设计为产生这种行为的;因此,这种行为似乎并非错误,而是一种痛苦且未被记录的设计,行为直观性差。由于这种行为如此奇怪、未被记录且不直观,我感到有必要与后人分享我的发现。
以下是我发现的内容:
公共 API
System.Configuration.ConfigurationSectionGroup
这是用于分组节和其他组的配置构造,本质上是一个容器。它可以被子类化,但它本身不派生自任何东西,并且功能很少,因此它为自定义提供的机会很少。
System.Configuration.Configuration
与 ConfigurationSectionGroup
类似,它本质上充当节和组的容器,不派生自任何东西;然而,它是密封的,不能被子类化。它提供上下文信息、用于处理连接字符串和应用程序设置的一些基本构造,以及将配置保存到文件的 API。它通过委托操作为自定义提供了一些灵活性。
因此,总的来说,大多数配置文件处理都在 .NET 框架的后台和内部进行。因此,研究这些机制需要查看 .NET 代码库并在 VS 调试器中反编译必要的代码。
System.Configuration.MgmtConfigurationRecord
System.Configuration.BaseConfigurationRecord
这些类是 .NET 框架的内部类,提供了配置管理的大部分核心功能。简单来说,每个配置[文件]都与一个配置记录相关联。配置记录提供有关配置文件中内容以及如何写入配置信息的簿记。
SaveAs(string filename, ConfigurationSaveMode saveMode, bool forceUpdateAll)
每当用户想要保存配置文件时都会调用此方法。它管理保存过程,本质上提供了一个保存功能的包装器并提供错误处理。它调用 CopyConfig(...)
,该方法的任务是复制所有原始配置文件内容(包括注释)并集成新更改。它调用以下方法:
CopyConfigDeclarationsRecursive(...)
此方法复制原始配置文件声明节中的所有内容。然后它调用:
WriteUnwrittenConfigDeclarations(...)
该方法写入任何新声明。
症结就在这里!
新节和节组被写入现有节和组之后。
直观吧?错了!
再次考虑前面介绍的损坏的配置文件。
<configuration>
<configSections>
<sectionGroup name="ServiceConfiguration" type="System.Configuration.ConfigurationSectionGroup, System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" >
<section name="Administrators" type="My.CustomSection" />
<sectionGroup name="ServiceStepGroups" type="My.StepGroups" >
<sectionGroup name="MyGroup1" type="My.StepGroup" />
</sectionGroup>
</sectionGroup>
<sectionGroup name="ServiceConfiguration"> <!-- No type attribute, duplicate name and wrong place -->
<sectionGroup name="ServiceStepGroups"> <!-- No type attribute, duplicate name and wrong place -->
<sectionGroup name="MyGroup2" type="My.StepGroup" />
</sectionGroup>
</sectionGroup>
</configSections>
<ServiceConfiguration>
<Administrators ... />
<ServiceStepGroups>
<MyGroup1 ... />
<MyGroup2 ... /> <!-- This is in expected place -->
</ServiceStepGroups>
</ServiceConfiguration>
</configuration>
我们有主要的自定义节组 ServiceConfiguration
,它包含所有业务类型的配置信息,这些信息是应用程序特有的或特定的。在此主组内,我们有 ServiceStepGroups
组,该组又声明了应用程序要执行的各种步骤组(MyGroup1
和 MyGroup2
)。
然而,有两个(2)ServiceConfiguration
声明,这明显违反了配置文件规范,该规范要求组名称唯一。在原始配置文件中,我们只定义了 MyGroup1
,其中它及其包含的组都用 type 属性声明,当加载配置文件时,这是声明 .NET 组类型所必需的。但是,我们在加载配置文件后添加了 MyGroup2
,以便能够用新组重新保存配置文件。
这里就开始了不直观的行为 。
因为 CopyConfigDeclarationsRecursive(...)
首先被调用,它会写入一个完整的 ServiceConfiguration
声明,该声明是从原始文件中复制的。当调用 WriteUnwrittenConfigDeclarations(...)
时,它别无选择,只能写入第二个 ServiceConfiguration
声明以维护声明层次结构;然而,它这样做时没有 type 属性,大概是因为在写入时不需要 .NET 类型,而只在读取时需要。如果一个声明(例如 ServiceStepGroups
)不在原始声明中,则会写入 type;如果声明存在,则不会写入 type。
因此,这种行为似乎是 SaveAs(...)
中的设计缺陷。然而,为了保持开放的心态,我将暂缓永恒的审判,因为可能存在相互竞争的目标或其他我不知道的行为场景,这些场景驱动了微软的设计。尽管如此,我认为我们可以都同意,更好的文档将有助于理解以编程方式修改和保存配置文件的行为和功能。如果存在更好的文档,我未能找到。
解决方案
在发现并理解了一切之后,我通过实现一个后处理步骤 成功地创建了一个正确的配置文件。这是一个变通方法:手动修改新配置文件的 XML(我使用了
System.Xml
库)将新的组声明移动到原始 ServiceStepGroups
声明内的正确位置,并删除重复的分层构造。
我本可以一开始就这样做,但由于(基于对直观功能的期望和缺乏文档)我假设 我不需要这样做,并且问题在于我以及我使用 .NET 配置机制的代码。虽然在技术上得到了验证,但这给了我对 .NET 配置处理底层机制及其局限性一个(痛苦
)但很有价值的教训。
因此,以积极的姿态结束 ,也许这次技术深入和案例研究可以为将来遇到类似问题的其他人提供一些文档或参考。我想起约翰·亚当斯的一句话:
引用后代!你们永远不会知道,现在的世代付出了多少代价来保存你们的自由!我希望你们好好利用它。如果你们不这样做,我将在天堂后悔我曾经为此付出过一半的努力。
后代们,我希望你们好好利用这些信息。
关注点
此问题的原始帖子可以在这里找到: https://codeproject.org.cn/Messages/5839778/Debrief-Configuration-Save-Writes-Invalid-Configur