The past couple of days I've been working on a command-line config file parser that will enable our build process to emit the correct web.config for any given environment. For example, we define environments called “model,” “fqa” (final qa) and “prod” (production). The idea is to embed the environment-specific settings in comments at the end of the web.config file, then run the parser, passing in the environment name as a command-line parameter. Here is a sample of what the environment-specific settings might look like:
<!-- [PROD]
<configuration>
<system.web>
<customErrors mode="On" defaultRedirect="Error.htm" replaceHere="true"/>
<authentication >
<forms loginUrl="login.aspx" name="PROD" timeout="60" path="/" replaceHere="true">
</forms>
</authentication>
<httpCookies httpOnlyCookies="false" requireSSL="false" domain="seibelsonline.com" replaceHere="true"/>
</system.web>
</configuration>
-->
<!-- [FQA]
<configuration>
<system.web>
<customErrors mode="RemoteOnly" defaultRedirect="Error.htm" replaceHere="true"/>
<authentication >
<forms loginUrl="login.aspx" name="FQA" timeout="60" path="/" replaceHere="true">
</forms>
</authentication>
</system.web>
</configuration>
-->
The replaceHere="true" attribute marks the location at which the parser is supposed to insert a replacement node. Once I have the code fully debugged, I hope to be able to post it here. (Please leave a comment if you are interested in seeing it.)
If you're like me, you check the Microsoft QuickStart samples when you want to do a task for the first time. The XML file samples Microsoft provides are quite straightforward. To read the document, you call the XmlDocument.Load(fileName) method; to save it, you simply call the XmlDocument.Save(fileName) method. So to read, modify, and save the document, you should be able to write code like this:
string fileName = args[0];
// load the XML document
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(fileName);
// TODO: parse the special comments and modify the in-memory document
// write the document back to the file system
xmlDoc.Save(fileName);
Wouldn't life be wonderful if code this simple would just work? Well, as my kids are probably tired of hearing, life is often unfair. When your code tries to call xmlDoc.Save(), it will throw a System.IOException in a pique of outrage. The exception message will be something like “another process has the file open.”
What? Another process? I'm looking at my code, and all I see is one process! How can another process have it open? Well, it's time to learn something about the NTFS file system. The Windows operating systems all have a special pool of kernel-mode threads that handle I/O requests asynchronously. So while the appearance of your code is that one process is doing the file operations synchronously, the reality is that the System.Xml code calls System.IO code which calls Windows API code which calls the IO subsystem asynchronously and waits for completion signals. You know, the knee bone's connected to the thigh bone and all that. The important point is that the kernel-mode threads that handle the xmlDoc .Load and .Save operations do not know how to cooperate with each other unless given specific instructions otherwise.
So how do we get the .Load and .Save methods to cooperate? The answer is to create a FileStream object that both methods can use. Your new and improved code might look something like this:
// load the XML document
XmlDocument configFile = new XmlDocument();
FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
configFile.Load(fs);
// TODO: parse the special comments and modify the in-memory document
// write the document back to the file system
configFile.Save(fs);
And that, friends, should be the end of the story.
Only it's not. Examine the output, and you'll be tempted to schedule an appointment with your optometrist because you're seeing double. The config file now has two root nodes; the old section was preserved and a new one got added at the end. Fortunately, it doesn't take too much effort to fix this little problem. You need to reset the FileStream object before you write to it, that's all. Your final code should look something like this:
// load the XML document
XmlDocument configFile = new XmlDocument();
FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
configFile.Load(fs);
// TODO: parse the special comments and modify the in-memory document
// write the document back to the file system
fs.Seek(0, SeekOrigin.Begin);
fs.SetLength(0);
configFile.Save(fs);
Pop the cork on the champagne bottle, folks, you've got a solution. And please do not hesitate to leave a comment if you've got any related insights to share with other readers.