Using NAnt to Update .NET Config Files, Part 01

This is the first of a two part article on updating Microsoft app.config or web.config files with the open source tool called NAnt. This is a relatively advanced article designed to teach you some of the powerful features found in the NAnt build tool.

A previous article presented the reader with a basic introduction to the free build tool called NAnt. This article will focus on a technique for updating multiple .NET config files at one time. Technology covered includes:

  • Using multiple NAnt build files and calling one build file from another
  • Using xmlpoke and xmlpeek
  • Working with xpath statements. This is the most technical part of the article, and requires at least a passing understanding of XML syntax.

Hopefully, the technique shown for working with config files will prove to be useful in and of itself, but the main focus of this article will be the discussions of NAnt technologies such as xmlpoke and multiple build files.

I should point out that there are other articles on the web about using NAnt to change .NET config files. Indeed it is a natural task to perform with a tool like NAnt. However, the techniques shown here I developed myself. Indeed none of the articles that I found on this subject did more than discuss the theory behind this idea, that is, none of them showed a specific implementation of how to do it.

Defining the Problem Space

For the programming project I am currently working on, I have a single Visual Studio solution that contains about 12 different assemblies and programs. Almost all of the programs, and several of the unit test assemblies, have their own config files. As a rule, I want each project to share a large number, if not all, of the settings accessed by the other projects. In other words, I want to find a way to make identical minor changes to each of my multiple config files. One way to solve this kind of problem is to have multiple custom configuration files. One of the files, for instance, could contain only FTP settings. I could then have each of the programs access this single configuration file to get the FTP setttings, and access another single file to get the database settings. The team decided that was an awkward solution, so we switched to having one large config file for each project.

Given this new system, I now need to simultaneously update any one of various different sections of my numerous config files. For instance, I might want to change the database server listed in each config file to server A, and the FTP server to server B. Then later, I might want to set both the database server and the FTP server to machine B, and later, I might want the FTP server to be A and database server to be B. In practice, there are other sections of the config files that need to be changed independently of the other settings. The end result is a rather confusing set of changes that need to occur simultaneously to some four to six different config files.

There are probably a hundred different ways to use NAnt to change config files in the manner outlined in the last two paragraphs. In this article I will outline one particular solution.

Default NAnt Files

The solution I will propose involves creating two NAnt build files that reside in the same directory. These files will work in tandem to access multiple config files.

All NAnt build files end with the extension .build. For instance, a typical name for a NAnt file would be NAnt.build. To run NAnt, you simple need to have NAnt.exe on your path. You then type nant at the command prompt, and NAnt will seek out the build file in the current directory and run it. So what happens if there are multiple build files in the same directory? Does NAnt run them all? Does it select one at random?

It turns out that simply running NAnt with no parameters in a directory where there are multiple build files can be an error. For instance, consider this directory containing two build files:

[D:\temp\bar]dir

 Volume in drive D is ChasDiskD      Serial number is 4D89:2760
 Directory of  D:\temp\bar\*

 9/20/2005  13:17         <DIR>    .
 9/20/2005  13:17         <DIR>    ..
 9/20/2005  12:13             120  foo.build
 9/20/2005  12:13             120  nant.build
            240 bytes in 2 files and 2 dirs    8,192 bytes allocated
 15,964,147,712 bytes free

If you type nant in this directory, you get an error:

[D:\temp\bar]nant
NAnt 0.85 (Build 0.85.1932.0; rc3; 4/16/2005)
Copyright (C) 2001-2005 Gerry Shaw
http://nant.sourceforge.net


BUILD FAILED

More than one '*.build' file found in 'D:\temp\bar' and no default.build exists.
  Use -buildfile: to specify the build file to execute or  create a 
default.build file.

For more information regarding the cause of the build failure, run the 
build again in debug mode.

Try 'nant -help' for more information

[D:\temp\bar]

If you read the error message shown here, you can see what needs to be done. One solution is to create a build file called default.build, or else rename one of the existing build files to default.build. Let’s assume that we have created a new build file called default.build:

[D:\temp\bar]dir

 Volume in drive D is ChasDiskD      Serial number is 4D89:2760
 Directory of  D:\temp\bar\*

 9/20/2005  13:26         <DIR>    .
 9/20/2005  13:26         <DIR>    ..
 9/20/2005  13:27             136  default.build
 9/20/2005  13:27             132  foo.build
 9/20/2005  13:27             133  nant.build
            401 bytes in 3 files and 2 dirs    12,288 bytes allocated
 15,964,082,176 bytes free

Assume that each of the files has single task designed to print out the name of the build file. For instance, default.build looks like this:

<project name="testName" default="talk">

	<target name="talk">
		<echo message="You ran default.build"/>
	</target>

</project>

Likewise, the file called NAnt.build looks like this:

<project name="testName" default="talk">

	<target name="talk">
		<echo message="You ran NAnt.build"/>
	</target>

</project>

Both of these files represent a sort of "hello world" for NAnt. When you run them, they print out a simple string defined in an echo task.

When you type NAnt in the current directory, this is the result:

[D:\temp\bar]nant
NAnt 0.85 (Build 0.85.1932.0; rc3; 4/16/2005)
Copyright (C) 2001-2005 Gerry Shaw
http://nant.sourceforge.net

Buildfile: file:///D:/temp/bar/default.build
Target framework: Microsoft .NET Framework 1.1
Target(s) specified: talk

talk:

     [echo] You ran default.build

BUILD SUCCEEDED

Total time: 0 seconds.

In the output shown here, I have highlighted the line which shows which build file was run, and I have highlighted the output from the echo task.

As we learned from the error message which we received earlier, you can also use the -buildfile switch to select a particular build file:

[D:\temp\bar]nant -buildfile:foo.build
NAnt 0.85 (Build 0.85.1932.0; rc3; 4/16/2005)
Copyright (C) 2001-2005 Gerry Shaw
http://nant.sourceforge.net

Buildfile: file:///D:/temp/bar/foo.build
Target framework: Microsoft .NET Framework 1.1
Target(s) specified: talk


talk:

     [echo] You ran foo.build

BUILD SUCCEEDED

Total time: 0 seconds.

If you look at the highlighted elements in the output shown here, you can see that I used a simple command to select a particular build file. This technique requires a little more effort each time you run NAnt, but it is easy enough to wrap the NAnt command in a batch file. In the end, which technique you select is a matter of taste, and possibly a matter of convenience given a particular set of circumstances..

If you use the Windows Explorer to associated nant.exe with .build files, then you can also double click on a build file from the Windows GUI to run NAnt. It is also possible to integrate NAnt with many popular IDE’s. I generally choose to run NAnt files from the command prompt, but you should feel free to do whatever you find most convenient. I have found all three techniques useful at different times.

Working with Config Files

Now that we understand a little more about NAnt, it is time to move on to a discussion of using NAnt to read and write from sections of a .NET config file. Consider the following very simple app.config file for a .NET project:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="AppName" value="TradeMaster" />
    <add key="DatabaseName" value="Velocity" />
    <add key="FtpAddress" value="localhost" />
  </appSettings>
</configuration>

Suppose you want to change the value of the AppName tag from TradeMaster to TradeMonster. Here is a simple NAnt file that uses the xmlpoke task to make the change:

<project name="ConfigChanger" default="configWrite">

	<target name="talk">
		<echo message="You ran default.build"/>
	</target>

	<target name="configWrite">
		<xmlpoke
			file="config01/app.config"
			xpath="/configuration/appSettings/add[@key='AppName']/@value"
			value="TradeMonster">
		</xmlpoke>
	</target>

</project>

The xmlpoke task shown here has three attributes: file, xpath and value:

  • The file attribute designates the name of the config file to access. In this case we are accessing a file called app.config which is in a directory called config01. In our case, the path to the directory would be: D:\temp\bar\config01\app.config.
  • The xpath attribute is an xsl statement designed to access a particular node of the xml based config file.
  • The value attribute is the new value of the node designated by the xpath statement.

NOTE: Go to this link, to get to the complete docs on the xmlpoke task.

Thinking about XPath

In this section I will talk briefly about XPath. If you know at least a little bit about XML files, and if you have at least heard an introduction to the technology called xsl, then you should be able to follow this section of the text. If not, then don’t be afraid to skip ahead to the next section of the text.

xpath is a language designed to make it possible for you to single out a part of an XML file. The part you want to single out might be an attribute, a tag, an element, or a group of elements. In this case, we are interested in the value of a single attribute of a single element. You have already seen one way to specify the attribute value in XML:

<configuration>
  <appSettings>
    <add key="AppName" value="TradeMaster" />

Consider the following code excerpt which slightly re-arranges the syntax shown above:

<configuration> <appSettings> <add key="AppName" value="TradeMaster" />

Now let’s look again at our xpath statement:

xpath="/configuration/appSettings/add[@key='AppName']/@value"

Notice how the code uses slashes to dig down through the hierarchy of start tags from the outer configuration tag to the inner add tag, and then move on to the attribute named value.

Before going on, take a moment to recall that each attribute in an xml file consists of two parts. First comes the name, and then an equals sign, and then the value associated with the name. In this case Microsoft has saddled us with syntax that is a bit confusing. The name of the attribute we care about is value and it has a value of "TradeMaster". Compare it with the first attribute, which has a name of key, and a value of AppName:

<add key="AppName" value="TradeMaster" />

This is the way .NET config files are structured, and we have to work with the tools at hand.

It turns out that there are multiple add nodes in this xml file. We use the following syntax to say that we want to talk about the add element that has an attribute called key with a value of AppName:

[@key='AppName']

The ugly little @ symbol before the word key let’s us know that we are talking about an attribute. But we aren’t ultimately interested in the key attribute. Instead, we are focused on the attribute called value:

/@value

Again, we are digging down through a set of nested attributes which are separated by slashes. Recall that the @ symbol let’s use know that we are looking for an attribute. The name of this attribute is value. Look one more time at the portion of the XML file that interests us:

<configuration>
  <appSettings>
    <add key="AppName" value="TradeMaster" />

In this code excerpt, the part of this XML file that is pointed at by our xpath statement is highlighted.

Now that we have finally parsed our xpath statement and gotten down to the part of the xml based config file that interests us, we can look again at our xmlpoke statement from the NAnt file::

		<xmlpoke
			file="Config01/app.config"
			xpath="/configuration/appSettings/add[@key='AppName']/@value"
			value="TradeMonster">
		</xmlpoke>

The very last line in our xmlpoke task says that we want to set the value of the attribute named value to the string "TradeMonster."

To sum up: Our xmlpoke node tells NAnt to open up a particular config file, to search for a particular attribute of a particular element, and to set its value to the word "TradeMonster."

To run this code, we go to the command prompt and type nant.

[D:\temp\bar]nant
NAnt 0.85 (Build 0.85.1932.0; rc3; 4/16/2005)
Copyright (C) 2001-2005 Gerry Shaw
http://nant.sourceforge.net

Buildfile: file:///D:/temp/bar/default.build
Target framework: Microsoft .NET Framework 1.1
Target(s) specified: configWrite


configWrite:

  [xmlpoke] Found '1' nodes matching XPath expression '/configuration/appSetting
s/add[@key='AppName']/@value'.

BUILD SUCCEEDED

Total time: 0 seconds.

As usual, NAnt is very polite about providing us with plenty of useful feedback. You can use the -quiet option if you want less feedback from NAnt.

When we are done, the config file should be changed:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="AppName" value="TradeMonster" />
    <add key="DatabaseName" value="Velocity" />
    <add key="FtpAddress" value="localhost" />
  </appSettings>
</configuration>

Reading with XmlPeek

Let’s finish up this first section of our article by learning how to read from an XML file using the NAnt task called xmlpeek. Consider the following, improved version of our build file:

<project name="ConfigChanger" default="configWrite">

	<property name="appName" value="unknown"/>

	<target name="talk">
		<echo message="You ran default.build"/>
	</target>
	
	<target name="configWrite">
		<xmlpoke
			file="Config01/app.config"
			xpath="/configuration/appSettings/add[@key='AppName']/@value"
			value="TradeMonster">
		</xmlpoke>
	</target>

	<target name="configRead">
		<xmlpeek
			file="Config01/app.config"
			xpath="/configuration/appSettings/add[@key='AppName']/@value"
			property="appName">
		</xmlpeek>

		<echo message="${appName}"/>
	</target>

</project>

This file is similar to the one shown in previous sections of the text. We have, however, added two sections:

  • At the top of the file there is a property called appName.
  • At the bottom of the file there is a new target called configRead that contains an xmlpeek task.

The xmlpeek task is identical to the xmlpoke task, except that it reads instead of writes. In particular, the programmer should specify a property that will contain the value of the part of the XML file pointed to by the xpath statement:

<xmlpeek
	file="Config01/app.config"
	xpath="/configuration/appSettings/add[@key='AppName']/@value"
	property="appName">
</xmlpeek>

The property portion of this xmlpeek task points at a property we declared called appName. At the beginning of the file, appName is initialized to contain a value of unknown:

<property name="appName" value="unknown"/>

After the configRead target has been called, the value of appName should be automatically set to the value of the part of the XML file designated by our xpath statement. If that is the case, then the echo task should print out the word TradeMaster or TradeMonster. The best way to see if it works is to go to the command prompt and try it out. In this case, we don’t want to execute the default target, but instead the one called configRead:

[D:\temp\bar]nant configRead
NAnt 0.85 (Build 0.85.1932.0; rc3; 4/16/2005)
Copyright (C) 2001-2005 Gerry Shaw
http://nant.sourceforge.net

Buildfile: file:///D:/temp/bar/default.build
Target framework: Microsoft .NET Framework 1.1
Target(s) specified: configRead


configRead:

  [xmlpeek] Peeking at 'D:\temp\bar\Config01\app.config' with XPath expression '
/configuration/appSettings/add[@key='AppName']/@value'.
  [xmlpeek] Found '1' nodes with the XPath expression '/configuration/appSetting
s/add[@key='AppName']/@value'.
     [echo] TradeMonster

BUILD SUCCEEDED

Total time: 0 seconds.

The two highlighted section of this output tell the story. The first highlight shows that we called NAnt with syntax specifying that it execute the configRead section of default.build:

nant configRead

The second highlighted area shows that our property has been changed from its default value of unknown, to a new value picked up from our XML file. In particular, the word TradeMonster is printed out for our delictation.

Summary

In this article you have learned how to work with multiple NAnt files, and how to use xmlpoke and xmlpeek to read and write to a particular area in an XML file. Much of the article focused on an introduction to the very simplest possible XPath syntax.

In the next article, you will see how to work with these basic tools to design a simple system for changing multiple parts of a .NET config file with a minimum of syntactical fuss. That section of the article will include details on calling one NAnt build file from another NAnt build file, and the specifics about how to pass information back and forth between the two files.

No comments yet

Leave a Reply

You must be logged in to post a comment.