Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Config Customization at Build Time in Visual Studio

0.00/5 (No votes)
22 Jun 2008 1  
Modify config files with machine- or build-specific differences at compile time.

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:

<?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 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:

<?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 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)
{
    // 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!

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:

  1. 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"
  2. 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here