Scott Hanselman posted an entry yesterday about Managing Multiple Configuration File Environments with Pre-build Events. His design uses pre-build events in Visual Studio to copy specific configuration files to the default file name, such as having "web.config.debug" and a "web.config.release" configuration files and the pre-build copying the appropriate file to "web.config" based on which build configuration you are in. This is a great idea, but large web.config files can get tedious to maintain, and there is a lot of repeated code. Even using include files as Scott suggests would help, but major blocks, such as Application Settings, may have to be repeated even though only the values change. This exposes human error, since an app setting may be forgotten or misspelled in one of your web.config versions.
At Latitude, we manage this problem through NAnt. One of our former developers, Erik Nelsestuen–brilliant guy–authored the original version of what we call "ConfigMerge". Essentially, our projects have no web.config under source control. Instead, we have a web.format.config. The format config is nearly identical to the web.config, except all of the application settings and connection strings have been replaced with NAnt property strings. Rather than have a seperate web.config for each environment and build configuration, we simply have NAnt property files. Our build events (as well as our automated build scripts) pass the location of the format file and the location of the property file and the output is a valid web.config, with the NAnt property strings replaced with their values from the environment property file.
It's simple. It only takes one NAnt COPY command.
default.build
<project default="configMerge">
<property name="destinationfile"
value="web.config" overwrite="false" />
<property name="propertyfile"
value="invalid.file" overwrite="false" />
<property name="sourcefile"
value="web.format.config" overwrite="false" />
<include buildfile="${propertyfile}" failonerror="false"
unless="${string::contains(propertyfile, 'invalid.file')}" />
<target name="configMerge">
<copy file="${sourcefile}"
tofile="${destinationfile}" overwrite="true">
<filterchain>
<expandproperties />
</filterchain>
</copy>
</target>
</project>
For an example, lets start with a partial web.config, just so you get the idea. I've stripped out most of the goo from a basic web.config, and am left with this:
web.confg
<configuration>
<system.web>
<compilation defaultLanguage="c#" debug="true" />
<customErrors mode="RemoteOnly" />
</system.web>
</configuration>
In a debug environment, we may want to enable debugging and turn off custom errors, but in release mode disable debugging and turn on RemoteOnly custom errors. The first thing we will need to do is create a format file, and then convert the values that we want to make dynamic into NAnt property strings.
web.format.config
<configuration>
<system.web>
<compilation defaultLanguage="c#" debug="${debugValue}" />
<customErrors mode="${customErrorsValue}" />
</system.web>
</configuration>
Next, we need to make NAnt property files, and add in values for each NAnt property that we've created. These property files will include the values to be injected into the web.config output. For the sake of simplicity, I always give my property files a '.property' file extension, but nant will accept any file name; you can use '.foo' if you like.
debugBuild.property
<project>
<property name="debugValue" value="true" />
<property name="configMergeValue" value="Off" />
</project>
releaseBuild.property
<project>
<property name="debugValue" value="false" />
<property name="configMergeValue" value="RemoteOnly" />
</project>
Finally, we just execute the NAnt script, passing in the appropriate source,
destination and property file locations to produce our environment-specific
web.config.
nant configMerge -D:sourcefile=web.format.config -D:propertyfile=debugBuild.property -D:destinationfile=web.config
web.config output
<configuration>
<system.web>
<compilation defaultLanguage="c#" debug="true" />
<customErrors mode="Off" />
</system.web>
</configuration>
And that's all there is to it. This is extendable further using the powers of NAnt. Using NAnt includes, you can put all of your base or default values in one property file that's referenced in your debugBuild.property or releaseBuild.property, further minimizing code. You can use Scott's pre-build event idea to have each build configuration have its own NAnt command to make mode-specific configuration files.
Feel free to use the above NAnt script however you like; but, as always YMMV. Use it at your own risk.
Enjoy.