Thursday, October 29, 2009

Multiple Web.config Management Utility

Something we all face on a regular basis is managing our Web.config files across multiple deployment environments each with their own specific set of options. The trivial, but repetitive task of managing multiple config files can become quite tedious during development and in order to take some of that stress away I have built some utilities into my own build process that automatically maintain my config files for me, allowing me to worry about more important things. Using a simple batch script which executes some basic NAnt commands I am able to rapidly generate multiple web.config files from a single template with unique values in each.

To do this, I make use of NAnt’s built-in file merging functionality which takes two files and substitutes values from one into specific locations in the other and produces a combined output. Applying this concept to creating web.config files, I have a single “web.config.format” file which looks like a normal configuration file, but contains ${Property.Name} markers in areas where a value with a given name should be substituted. I then create multiple “{deployment-type}.property” XML files which contain elements indicating each property name and its value. Finally, this is all tied together by a batch script which calls the appropriate merge commands on each file in a directory I specify.

default.build (NAnt Script)
<?xml version="1.0"?>
<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>
Sample .Property File
<?xml version="1.0"?>
<project>
 <property name="compilationDebug" value="true" />
 <property name="customErrorsMode" value="Off" />
 <property name="smtpServer" value="mail.website.com" />
 <property name="smtpFrom" value="admin@website.com" />
 <property name="googleAnalyticsKey" value="UA-12345678-9" />
</project>
Sample Web.config.format File
<?xml version="1.0"?>
<configuration>
 <configSections>
   <sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
     <section name="Project.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false"/>
   </sectionGroup>
 </configSections>
 <appSettings>
   <add key="GoogleAnalyticsKey" value="${googleAnalyticsKey}" />
 </appSettings>
 <system.web>
   <customErrors mode="${customErrorsMode}" defaultRedirect="AppError.aspx">
     <error statusCode="404" redirect="404.aspx"/>
   </customErrors>
   <compilation debug="${compilationDebug}" />
 </system.web>
 <system.net>
   <mailSettings>
     <smtp from="${smtpFrom}" deliveryMethod="Network">
       <network host="${smtpServer}" port="25" defaultCredentials="true" />
     </smtp>
   </mailSettings>
 </system.net>
</configuration>
ConfigMerge.bat (NAnt script invoker)
@ECHO OFF
SET formatFile=%2
SET outputDir=%1

IF "%2"=="" SET formatFile=%~dp0web.config.format

ECHO USING: %formatFile%
ECHO OUTPUT DIR: %outputDir%
ECHO.
IF NOT EXIST "%formatFile%" (
   ECHO Could not locate format file. Please provide a valid filename.
) ELSE (

   FOR /f %%p in ('dir /b "%~dp0*.property"') DO (
       ECHO GENERATING: web.%%~np.config FROM: %%p
       START /B /WAIT "NAnt" "%~dp0nant\nant.exe" -buildfile:"%~dp0default.build" configMerge -q+ -nologo+ -D:sourcefile="%formatFile%" -D:propertyfile="%~dp0%%p" -D:destinationfile=%outputDir%\web.%%~np.config
   )
)

To further simplify things, I built an additional batch script that invokes ConfigMerge.bat with it's first argument- output directory. By calling BuildWebConfigs.bat ..\Web I can tell the NAnt script to output the generated config files in my website project's "Web" directory (and additionally replace Web.config with my Web.debug.config).

BuildWebConfigs.bat (ConfigMerge.bat invoker)
@ECHO OFF
call "%~dp0build\ConfigMerge.bat" ..\Web
copy "%~dp0Web\web.debug.config" "%~dp0Web\web.config" /y
echo.
echo ********* BuildWebConfigs Complete *********

In the interest of convenience I added a call to the second batch script into my website project's build events and included it as an external tool which I can easily invoke from Visual Studio's UI:

 

Since Visual Studio doesn't let you define per-configuration build events in C# projects (as it does in C++ projects- or so I'm told) I manually edited the CSProj file and added an instruction at the very bottom to call my batch script on all non-Debug builds:

Partial .CSProj File
<Project>
 ......
 .......
 ........
 <PropertyGroup Condition=" '$(ConfigurationName)' != 'Debug' ">
   <PostBuildEvent>
     call $(SolutionDir)BuildWebConfigs.bat
   </PostBuildEvent>
 </PropertyGroup>
</Project>

I have configured all of my solutions that use this utility to have BuildWebConfigs.bat and a folder called "build" in the $(SolutionDir). The "build" folder is laid out like so:

Folder Structure
------------------------------
+ nant
  - nant.exe
  - <related nant files>
- ConfigMerge.bat
- default.build
- {somename}.property
- {somename}.property
- {somename}.property
- web.config.format
------------------------------

I have also built a NAnt script which performs the same actions as the ConfigMerge batch script, but I find it runs significantly slower (about 4.5 seconds longer) than the batch because the NAnt script has to spawn additional instances of NAnt to perform the required actions. I've included it here in case you'd like to play with it:

Alternative NAnt-based NAnt script invoker
<project name="generate configs" default="generate ">
 <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">
   <echo message="GENERATING: ${path::get-file-name(destinationfile)} FROM: ${path::get-file-name(propertyfile)}." />
   <copy file="${sourcefile}" tofile="${destinationfile}" overwrite="true">
     <filterchain>
       <expandproperties />
     </filterchain>
   </copy>
 </target>

 <target name="generate ">
   <foreach item="File" property="file">
     <in>
       <items>
         <include name="*.property" />
       </items>
     </in>
     <do>
       <property name="propertyfile" value="${path::get-file-name(file)}" overwrite="true"/>
       <property name="destinationfile" value="web.${path::get-file-name-without-extension(propertyfile)}.config" overwrite="true"/>
       <property name="sourcefile" value="Web.format.config" overwrite="true"/>
       <echo message="Generating: ${destinationfile} from ${propertyfile}."/>

       <exec program="nant\nant">
         <arg value="configMerge"/>
         <arg value="-nologo+"/>
         <arg value="-q"/>
         <arg value="-D:sourcefile=${sourcefile}"/>
         <arg value="-D:propertyfile=${propertyfile}"/>
         <arg value="-D:destinationfile=${destinationfile}"/>
       </exec>
     </do>
   </foreach>
 </target>
</project>

Downloads:

Sources:

Tuesday, October 27, 2009

Shell Command - Delete Visual SourceSafe Files

Visual SourceSafe has a tendency to litter version-controlled directories with 3 types of project tracking files (*.scc, *.vssscc, *.vspscc) that may be undesirable when sharing a project with others. This simple "Delete SourceSafe Files" Shell command will quickly delete all instances of those files in a directory and its sub-directories. To use, run this registry script:
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\DeleteSourceSafe]
@="Delete SourceSafe Files"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\DeleteSourceSafe\command]
@="cmd.exe /c \"TITLE Removing SourceSafe Files in %1 && COLOR 9A && del *.vssscc /s /f && del *.vspscc /s /f && del *.scc /s /f\""
I got this idea from Jon Galloway's blog post on creating a shell entry to remove SubVersion files. (Yes, the code is almost identical.) Note: This will *not* remove source-control bindings from the SLN file. That must be done manually by opening Visual Studio. If this script is run on a source-controlled solution and the solution is run VS will produce an error message regarding missing source-control files. Confirming the default action will permanently remove the binding from the SLN file. For the lazy: Download Zipped Registry File. (My host refused to serve a .reg file, sorry!)