Introduction
This article describes a solution to the perennial problem of config file management for multiple developers and environments. The centerpiece is a command-line tool which merges the base (default) configuration file with a truncated file (the differences or "diff" file). This diff file contains only those elements which need to be added or changed.
The Problem
You know this one. Two, or ten, or twenty developers have a writeable local copy of the web and/or application config files. QA, staging, and production servers have their copies. Changes have to be propagated across all versions, some of which may not be in source control. Various work-arounds are implemented - sometimes, more than one on the same project.
Emails are flying. Developers and testers are wasting time, chasing phantom bugs because someone didn't get a config file updated.
The Solution
Merge the base config file with a "differences" file, based on the machine name or build configuration. With this technique, there is no need for locally maintained files; all files can be checked into source control, and everyone's file structure is the same.
Here's the app.config from the example, which demonstrates some of the different scenarios the merge can handle:
="1.0" ="utf-8"
<configuration>
<connectionStrings>
<add name="ApplicationConnString"
connectionString="Data Source=OtherBox; ... ;User ID=not_sa;Password=abc"
providerName="System.Data.SqlClient" />
</connectionStrings>
<appSettings>
<add key="GeneralSettingOne" value="ValueOne" />
<add key="GeneralSettingTwo" value="ValueTwo" />
</appSettings>
<system.web>
<httpHandlers>
<add verb="*" path="*.example" type="ExampleHandler" />
<add verb="GET,HEAD" path="*.specific" type="SpecificHandler"/>
</httpHandlers>
</system.web>
...
Pages and pages of other stuff
...
</configuration>
Now, let's say a developer wants to customize several aspects of this file. She'll need to modify the connection string, modify an appSetting
, and add a new appSetting
. She'll also add an entirely new diagnostics section, for tracing. The diff file, app.devbox1.config, might look like this:
="1.0" ="utf-8"
<configuration>
<connectionStrings>
<add name="ApplicationConnString"
connectionString="Data Source=MyBox; ... ;User ID=not_sa;Password=abc"
providerName="System.Data.SqlClient" />
</connectionStrings>
<appSettings>
<add key="GeneralSettingTwo" value="MyGeneralSettingValue" />
<add key="MySetting" value="MyValue" />
</appSettings>
<system.diagnostics>
<sources>
...
</sources>
<sharedListeners>
...
</sharedListeners>
</system.diagnostics>
</configuration>
This file contains only those elements that are to be added or changed, and not the pages of other stuff that will stay the same. During the post-build event, the diff file is merged into the source file and placed in the output folder.
Elements in the diff file that match a name
or key
attribute in the base file overwrite the base file values. So, ApplicationConnString
and GeneralSettingTwo
are overwritten.
If the name
or key
is new, the element is added to the base file, so MySetting
is added.
If there is no corresponding element in the base file tree, then the diff file's element and children are added. So, the entire <system.diagnostics>
section is added to the target file.
How it Works
The postbuild command specifies the base, diff, and target config files. It calls ConfigMerge.bat, which in turn calls the ConfigMerge.exe utility. The command line looks like this:
"$(SolutionDir)ConfigMerge.bat" "$(ProjectDir)"
app.config "$(OutDir)$(TargetFileName).config"
app.%COMPUTERNAME%.config "app.$(ConfigurationName).config"
The batch and utility files are safe for use with paths that contain spaces; that's the reason for all the quotes in the command line. Let's examine each of the elements:
$(SolutionDir)ConfigMerge.bat
- The batch file path. Placed in the solution root, it can be used for multiple projects.
$(ProjectDir)
- Project root. This is used to construct file paths and to find the ConfigMerge.exe file, which is assumed by the batch to be in the solution root.
app.config
- Name of the base config file (change to web.config for web projects.)
$(OutDir)$(TargetFileName).config
- The target file name. If this is the same as the base file, it will be overwritten.
app.%COMPUTERNAME%.config
- First diff file to check for; in this case, a file named after the computer doing the build. Useful for developer builds which will run locally.
app.$(ConfigurationName).config
- Optional second file to look for; in this case, a file named after the build configuration. Useful for builds which will run on a build server.
There's nothing terribly special about the batch file; it checks the parameters and calls the merge utility.
IF EXIST "%_diff%" GOTO DOMERGE
:: Optional second file to do a diff against, if it exists
IF NOT [%~5]==[] (
SET _diff=%_path%%~5
IF EXIST "%_diff%" GOTO DOMERGE
)
If neither diff file is found, it just copies the base file to the target.
:DOMERGE
:: Modify the config with the difference file
"%_path%..\ConfigMerge.exe" "%_config%" "%_diff%" "%_target%"
:: If ConfigMerge.exe reports an exception, pass this up to visual studio
IF ERRORLEVEL 1 EXIT 1
Along the way, messages are written to Visual Studio's output window about the progress of the merge and what's being done. An exception in ConfigMerge.exe will cause the build to fail.
Compile complete -- 0 errors, 0 warnings
Test Application -> C:\Source\ConfigMerge\Test Application\bin\Debug\Tes...
"C:\Source\ConfigMerge\ConfigMerge.bat" "C:\Source\ConfigMerge\Test Appl...
ConfigMerge: Modifying [C:\Source\ConfigMerge\Test Application\app.confi...
ConfigMerge: Wrote target file [C:\Source\ConfigMerge\Test Application\b...
========== Build: 2 succeeded or up-to-date, 0 failed, 0 skipped =========
The ConfigMerge.exe console application is also pretty straightforward. Because of its reliance on specific attributes, it is not suitable for generic XML merging, but it works for .NET config files.
ConfigMerge.exe recursively processes the nodes in the diff XML document:
static void ProcessNode(XmlNode baseNode, XmlNode diffNode)
{
foreach (XmlNode diffNodeChild in diffNode.ChildNodes)
{
. . .
bool namedPath = false;
string path = GetComparisonPath(diffNodeChild, out namedPath);
XmlNodeList children = baseNode.SelectNodes(path);
. . .
if (children.Count == 1)
{
if (namedPath || !diffNodeChild.HasChildNodes)
{
XmlNode newNode = baseNode.OwnerDocument.ImportNode(
diffNodeChild, true);
baseNode.ReplaceChild(newNode, children[0]);
}
else
{
ProcessNode(children[0], diffNodeChild);
}
}
else
{
XmlNode newNode = baseNode.OwnerDocument.ImportNode(
diffNodeChild, true);
baseNode.AppendChild(newNode);
}
}
}
The matching is done based on a set of recognized parameters. Currently, this consists of the following:
static string[] UniqueAttributes = new string[] {
"name",
"key",
"verb,path" };
verb
and path
are used for HttpHandler
. If some other set of attributes is needed, they can be added to this array.
Integrating into Your Project
This part is easy. As long as you put the ConfigMerge batch and console applications in your solution root, you should be able to use the batch file and the postbuild command-line examples without modification. Drop the files in your solution's root, add them to the project (they'll show under "solution items" in Solution Explorer,) and copy the postbuild command line to your project.
Now, copy your local app.config to app.mycomputername.config, cut all of the unmodified sections from it, and get the latest. Build. You're done. Other team members can copy an existing diff file, rename it, and modify it for their local machines.
For ASP.NET or WCF applications that use a web.config file, there is the little complication that the config file isn't transferred to an output directory.
There are two options for merging web.config files:
- Create a template file with a different name, and make the target of the merge web.config. The batch file copies the base file if no diff file is detected, so web.config will always be created.
"$(SolutionDir)ConfigMerge.bat" "$(ProjectDir)"
web.template.config web.config web.%COMPUTERNAME%.config
"web.$(ConfigurationName).config"
- Set both base and target to web.config in the postbuild command line, causing the merge to copy over the existing web.config. It may be overwritten on the next get from the source control, but will be re-created by the build.
"$(SolutionDir)ConfigMerge.bat" "$(ProjectDir)"
web.config web.config web.%COMPUTERNAME%.config "web.$(ConfigurationName).config"
The Sample Project
The Visual Studio 2005 solution available for download includes the ConfigMerge project with source code, and the ConfigMerge.bat file. It also includes a sample application project with config files, for which the postbuild is configured.
To see it in action, rename app.devbox1.config to your computer's name, or to app.debug.config (for a debug build, of course).
The project should upgrade to Visual Studio 2008 with no problems.
History
- 2008.06.22 - Initial publication.