|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis 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 ProblemYou 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 SolutionMerge 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: <?xml version="1.0" encoding="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 <?xml version="1.0" encoding="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 If the If there is no corresponding element in the base file tree, then the diff file's element and children are added. So, the entire How it WorksThe 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:
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)
{
// Check each of the children to see if they match
foreach (XmlNode diffNodeChild in diffNode.ChildNodes)
{
. . .
// Look for corresonding nodes in the base document.
// If the child node has a name, key, or other recognized set of
// attributes, we'll look for a corresponding node based on that
bool namedPath = false;
string path = GetComparisonPath(diffNodeChild, out namedPath);
XmlNodeList children = baseNode.SelectNodes(path);
// Does the base document have corresponding nodes?
. . .
if (children.Count == 1)
{
// Replace the node if it is recognized (update with new
// information) or if it is an "endpoint" node.
// For endpoints, it is assumed that it wouldn't be in the diff
// file if it wasn't different somehow, even if there is no
// name attribute.
if (namedPath || !diffNodeChild.HasChildNodes)
{
XmlNode newNode = baseNode.OwnerDocument.ImportNode(
diffNodeChild, true);
baseNode.ReplaceChild(newNode, children[0]);
}
else
{
// Node is not named, and has children; process the
// children looking for differences
ProcessNode(children[0], diffNodeChild);
}
}
else
{
// No corresponding node; stick this whole node in there
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" }; // No space after comma!
Integrating into Your ProjectThis 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:
The Sample ProjectThe 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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||