|After laborious effort I was able to understand what was happening. Generally, I would consider the behavior in the OP 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:
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.
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.
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:
This method copies all content from the declarations section
of the original configuration file. It then calls
which writes any new declarations.
Herein lies the rub!
NEW SECTIONS AND SECTION GROUPS ARE WRITTEN AFTER EXISTING SECTIONS AND GROUPS.
Intuitive, yes? Wrong!
Consider the configuration file XML from the OP:
<sectionGroup name="ServiceConfiguration" type="My.ServiceConfiguration" >
<section name="CustomSection" type="My.CustomSection" />
<sectionGroup name="ServiceStepGroups" type="My.StepGroups" >
<sectionGroup name="MyGroup1" type="My.StepGroup" />
<sectionGroup name="ServiceConfiguration" >
<sectionGroup name="ServiceStepGroups" >
<sectionGroup name="MyGroup2" type="My.StepGroup" />
We have the main custom section group
ServiceConfiguration which holds all business-type configuration information which is unique or specific to the application (as opposed to standard types of configuration constructs such application settings, runtime, service model, etc.). Inside this main group we have the
ServiceStepGroups group which in turn declares various groups of steps for the application to perform (
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 .
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.
After all was discovered and understood I was able to produce a proper configuration file by implementing a post-processing step . 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 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 ) lesson in the underlying mechanisms of .NET configuration processing and its limitations.
So ending on a positive note 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:
> 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.
modified 28-Oct-21 16:29pm.