Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / F#

.NET Configuration File Corrupts On Write

4.33/5 (2 votes)
9 Nov 2021CPOL7 min read 5.6K  
Programmatically Adding New ConfigurationSectionGroup Corrupts Declaration Section With Multiple Group Declarations.
.NET configuration files are designed to be customized with custom sections (System.Configuration.ConfigurationSection) and section groups (System.Configuration.ConfigurationSectionGroup). To do so, developers subclass these base classes to implement their custom functionality and incorporate them into the configuration file. .NET configuration files are XML files and follow a defined schema. It is somewhat complex but the relevant schema element in this article is the declaration section at the beginning () which declares the layout and types of sections and section groups used in the configuration file. This allows the .NET configuration mechanisms to properly instantiate your custom classes with configuration information. When modifying the configuration file programmatically, sometimes the addition of new section groups corrupts the configuration file when re-written preventing it from being read in the future. This article is a case study of such a situation.

Introduction

We have a Windows service application which is driven from configuration information read from an application configuration file.  The purpose of the service is to run various steps at pre-scheduled times to do such things as generate static/cached data tables.  These steps are run sequentially within step groups and step groups are run in parallel.  The service is designed to be customizable via the configuration file both in execution parameters and the definitions of step groups and the steps within each group.  To help manage the configuration file a Windows GUI application was developed.  When adding new step groups to an existing configuration file and re-saving, the resultant configuration file becomes corrupt which prevents it from being read in the future viz. what the .NET mechanisms write out the .NET mechanisms can not read back in.

Background

.NET configuration files are XML files and follow a defined schema which allows for customization.  There are many standard configuration constructs which are designed to integrate the various functionalities supported by .NET, such as application settings, runtime serialization, service models, etc.  Customization is achieved by declaring the custom sections (System.Configuration.ConfigurationSection) and section groups (System.Configuration.ConfigurationSectionGroup) in the beginning of the configuration file as follows:

XML
<configuration>
  <configSections>
            <!-- Custom Declarations Go Here... -->
  </configSections>
</configuration>

In our case the original configuration file (before attempting to add a new group) looked something like:

XML
<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>

Here, we declare in configSections the top-level custom section group ServiceConfiguration which contains all the business type configuration information unique to our service application.  We then have an Administrators section which is irrelevant for this discussion but is left in as a visual to demonstrate how custom sections are defined.

The ServiceStepGroups is defined as a custom section group which will in turn host other section groups like MyGroup1, each of which will define the steps within that group. 

Following the configSections declarations we start with the ServiceConfiguration definition.  The content is omitted for brevity and is not relevant.  What is relevant are the requirements that 1) custom sections and section groups must be both declared and defined, 2) the structure of the definition layout must match that of the declaration, and 3)  section groups must have unique names, even if they are of the same .NET type.

The Problem

Adding a new My.StepGroup (MyGroup2) to ServiceStepGroups programmatically in code and then re-saving corrupts the configuration file by violating requirements 2 and 3 (mentioned previously) as follows:

XML
<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 Deep Dive

After laborious effort I was able to understand what was happening. Generally, I would consider this behavior as a bug with the .NET code since it can't read in what it writes out; however, the code seems structured and designed to produce this behavior; therefore, the behavior doesn't appear to be a mistake, just a sore and undocumented design with unintuitive behavior. Since this is such odd, undocumented and unintuitive behavior I felt compelled to share my findings with posterity.

Here is what I found:

Public APIs:

System.Configuration.ConfigurationSectionGroup

This is the configuration construct used to group sections and other groups, essentially a container. It can be subclassed but itself derives from nothing and offers little functionality so it offers little opportunity for customization.

System.Configuration.Configuration

Similar to ConfigurationSectionGroup in that it essentially acts as a container for sections and groups and derives from nothing; however, it is sealed and cannot be subclassed. It provides contextual information, some fundamental constructs for processing connection strings and application settings and the API for saving the configuration to file. It provides some flexibility for customization via delegate actions.

So, by and large, most of the processing of configuration files is behind the scenes and internal to the .NET framework. Therefore, to study these mechanisms required looking into the .NET code base and decompiling the necessary code in the VS debugger.

System.Configuration.MgmtConfigurationRecord
System.Configuration.BaseConfigurationRecord

These classes are internal to the .NET framework and provide much of the guts of configuration management. Simply, each configuration [file] is associated with one configuration record. The configuration record provides bookkeeping of what is in the configuration file and how the configuration information is written.

SaveAs(string filename, ConfigurationSaveMode saveMode, bool forceUpdateAll)

This method is called whenever a user wants to save the configuration file. It manages the save process, essentially providing a wrapper around save functionality and providing error handling. It calls CopyConfig(...) whose job it is to copy over all original configuration file content (comments and all) and integrate the new changes. It calls the following methods:

CopyConfigDeclarationsRecursive(...)

This method copies all content from the declarations section of the original configuration file. It then calls

WriteUnwrittenConfigDeclarations(...)

which writes any new declarations.

Herein lies the rub! WTF | :WTF:

NEW SECTIONS AND SECTION GROUPS ARE WRITTEN AFTER EXISTING SECTIONS AND GROUPS.

Intuitive, yes? Wrong!

Consider again the corrupt configuration file introduced previously:

XML
<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>

We have the main custom section group ServiceConfiguration which holds all business-type configuration information which is unique or specific to the application.  Inside this main group we have the ServiceStepGroups group which in turn declares various groups of steps for the application to perform (MyGroup1 and MyGroup2).

However, there are two (2) ServiceConfiguration declarations, a clear violation of the configuration specification which requires uniquely named groups. In the original configuration file we only had MyGroup1 defined where it and its containing groups were declared with the type attribute, necessary for declaring the .NET group type when loading a configuration file. But, we added MyGroup2 after loading the configuration file so that we could re-save the configuration file with the new group.

Here begins the unintuitive behavior Confused | :confused: .

Because CopyConfigDeclarationsRecursive(...) is called first it writes a complete ServiceConfiguration declaration as copied from the original file. When WriteUnwrittenConfigDeclarations(...) is called it has no choice but to write a second ServiceConfiguration declaration in order to maintain the declaration hierarchy; however, it does so without type attributes, presumably since the .NET type is not required for writing but only when reading. If a declaration (such as ServiceStepGroups) was not present in the original declaration then the type is written; if a declaration was present a type is not written.

So, this behavior would seem to be a design flaw in SaveAs(...). However, in the interest of open-mindedness I will hold back eternal judgment since there may be competing objectives or other behavioral scenarios of which I am not aware which drove Microsoft's design. Nonetheless, I think we can all agree that better documentation would have been helpful in understanding the behavior and function of programmatically modifying and saving configuration files. If there is better documentation I could not find it.

The Solution

After all was discovered and understood I was able to produce a proper configuration file by implementing a post-processing step Shucks | :-\ . It was a workaround: manually modify the XML of the new configuration file (I used the System.Xml libraries) to move new group declarations to their proper place within the original ServiceStepGroups declaration and remove the duplicate hierarchical constructs.

I could have done this in the beginning but I had assumed D'Oh! | :doh: I didn't need to (based on expectations of intuitive functionality and lack of documentation) and that the problem was with me and my code utilizing the .NET configuration mechanisms. While vindicated technically it was a good (but agonizing Dead | X| ) lesson in the underlying mechanisms of .NET configuration processing and its limitations.

So ending on a positive note Big Grin | :-D maybe this technical dive and case study can provide some documentation or reference to others experiencing a similar issue in the future. I am reminded of a quote from John Adams:

Quote:

Posterity! you will never know how much it cost the present generation to preserve your freedom! I hope you will make a good use of it. If you do not, I shall repent in Heaven that I ever took half the pains to preserve it.

Posterity, I hope you make good use of this information. Smile | :)

Points of Interest

The original post of this issue can be found at: https://www.codeproject.com/Messages/5839778/Debrief-Configuration-Save-Writes-Invalid-Configur

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)