Creating Custom NAnt Tasks

This article is the fourth in a series of articles on the build tool called NAnt. It describes how to use C# to create custom NAnt tasks. The information provided here builds on the previous articles, which can be found here, here and here. In particular, see the last two articles. The source code for this article, and for the last two articles, is available here.

The techniques provided for extending NAnt represent an excellent example of modern code design. The whole concept of plug and play, extensible, architectures is very much at the apex of current application design theory, and the folks who brought us NAnt demonstrate exactly how this kind of architecture should behave. As you will see while reading this article, the concepts involved are easy to understand, and easy to use. This is an excellent example of the kind of intuitive, cutting edge program design that lies behind the best modern software.

NAnt Tasks

Since this article focuses on building custom NAnt tasks, it seems appropriate to spend a few sentences defining our terms. NAnt is based on XML scripts. Each script contains XML code built around a series of tasks. A task plays a similar role in NAnt to a method or class in C#. It is a chunk of executable code.

Most people who use NAnt rely on the built in tasks that come with NAnt. For instance, the echo task writes a string to the console.

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

	<target name="talk">
		<echo message="This is a simple echo task."/>
	</target>

</project>

If you save this file as echo.build, and then run it with this command nant -buildfile:echo.build, the output would look like this:

[D:\temp\bar]nant -buildfile:echo.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/echo.build
Target framework: Microsoft .NET Framework 1.1
Target(s) specified: talk


talk:

     [echo] This is a simple echo task.

BUILD SUCCEEDED

Total time: 0 seconds. 

Note the highlighted line near the bottom of this listing where our text is echoed to the console. Besides the echo task, there are many other built in tasks, such as the csc task, used to build CSharp projects, or the exec task, used to run a system command.

Sometimes the built in tasks do not provide all the functionality you might need. In that case, you can create a custom task of your own. The purpose of this article is to show you the simple steps necessary to create your own custom task. After you have created the task, it will be saved to a DLL, where it can be easily reused in other projects.

Putting the Config Script in a Task

In the previous article in this series, you saw how to write C# script that can be embedded directly in a NAnt build file. In that article, the goal was to make identical changes to a series of .NET app.config or web.config files. This article will take that C# script from the previous article, embed it in a task called ConfigTask. In particular, the ConfigTask will be stored in a .NET assembly. Once you have built the assembly, you will be able to reuse it in multiple projects.

The relevant code from the last article is shown in Listings 0, 1 and 2. In particular, the code highlighted in dark blue in Listing 1 is the part we need to move into our .NET assembly. Listing two shows a second build file that this file relies upon. These files also assume that there are three files called app.config in subdirectories of the current directory. These config files are the files that are to be updated by this code. For clarification, please read the previous articles and download the source code.

Listing 0: The app.config file which is modified by the code shown in Listings 1 and 2.

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

Listing 1: Here is the build file, called default.build, from the previous article. It contains, highlighted in blue, the code that we want to reuse.

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

	<target name="setValues">
		<property name="fileName" value="config01\app.config"/>
		<property name="attributeName" value="DatabaseName"/>
		<property name="valueName" value="Zen"/>
	</target>  

	<target name="configWrite" depends="talk">

		<nant buildfile="nant.build" target="configWrite">
			<properties>
				<property name="fileName" value="${fileName}"/>
				<property name="attributeName" value="${attributeName}"/>
				<property name="valueName" value="${valueName}"/>
			</properties>
		</nant>

	</target>


	<target name="fancyCode">

		<script language="C#">
			<code>
			<![CDATA[
			public static void NewProp(
				Project project, string fileName, 
				string attributeName, string valueName)
			{
				string fileNameProperty = "fileName";
				string attributeNameProperty = "attributeName";
				string valueNameProperty = "valueName";

				project.Properties.Remove(fileNameProperty);
				project.Properties.Add(fileNameProperty, fileName);

				project.Properties.Remove(attributeNameProperty);
				project.Properties.Add(attributeNameProperty, attributeName);

				project.Properties.Remove(valueNameProperty);
				project.Properties.Add(valueNameProperty, valueName);
			}

			public static void DoFileNames(Project project, 
				string attributeName, string valueName)
			{
				String[] fileNames = {"config01\\App.config", 
					"config02\\App.config", 
					"config03\\App.config"};

				for (int i = 0; i < 3; i++) 
				{
					NewProp(project, fileNames[i], 
						attributeName, valueName);
					project.Execute("configWrite");
				}
					
			}

			public static void ScriptMain(Project project) 
			{   
				project.Execute("setValues");
				DoFileNames(project, "DatabaseName", "Zen");
				DoFileNames(project, "AppName", "TradeMaster");
				DoFileNames(project, "FtpAddress", "localhost");
			}
			]]>
			</code>
		</script>

	</target>

</project>

Listing 2: This file, called nant.build, is a dependency of the default.build file shown in Listing 1.

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

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

   	<target name="configWrite" depends="talk">

		<xmlpoke
			file="${fileName}"
			xpath="/configuration/appSettings/add[@key='${attributeName}']/@value"
			value='${valueName}'>
		</xmlpoke>

</target>

</project>

Since these three files were exhaustively discussed in the previous article, I will say no more about them here. Instead, I will show you, in Listing 3, the complete code for the .NET assembly that we are going to create.

Listing 3: Here is the first version of the code for the .NET assembly which will contain our new task. You should save this code to disk as ConfigTask.cs.

using System;
using NAnt.Core;
using NAnt.Core.Attributes;


namespace NAnt.Examples.Tasks 
{
	[TaskName("configtask")]
	public class ConfigTask : Task 
	{   
		public void NewProp(Project project, 
			string fileName,
			string attributeName,
			string valueName)
		{
			const string fileNameProperty = "fileName";
			const string attributeNameProperty = "attributeName";
			const string valueNameProperty = "valueName";

			project.Properties.Remove(fileNameProperty);
			project.Properties.Add(fileNameProperty, fileName);

			project.Properties.Remove(attributeNameProperty);
			project.Properties.Add(attributeNameProperty, attributeName);

			project.Properties.Remove(valueNameProperty);
			project.Properties.Add(valueNameProperty, valueName);
		}

		public void DoFileNames(Project project, string attributeName, string valueName)
		{
			String[] fileNames = {"config01\\App.config", 
				"config02\\App.config", 
				"config03\\App.config"};

			for (int i = 0; i < 3; i++) 
			{
				NewProp(project, fileNames[i], attributeName, valueName);
				project.Execute("configWrite");
			}

		}

		public void RunConfig(Project project) 
		{   
			project.Execute("setValues");
			DoFileNames(project, "DatabaseName", "Zendo");
			DoFileNames(project, "AppName", "TradeMaster");
			DoFileNames(project, "FtpAddress", "localhost");
		}

		// Override the ExecuteTask method.
		protected override void ExecuteTask() 
		{
			RunConfig(Project);
		}
	}
}

Here, with all extraneous code removed, is the core structure around which this code is build:

using System;
using NAnt.Core;
using NAnt.Core.Attributes;


namespace NAnt.Examples.Tasks 
{
	[TaskName("configtask")]
	public class ConfigTask : Task 
	{   
		// Override the ExecuteTask method.
		protected override void ExecuteTask() 
		{
			RunConfig(Project);
		}
	}
}

As you can see, we use the TaskName attribute at the top of our class, and descend the class itself from the NAnt class called Task. We then have to override a single method called ExecuteTask. From inside that method, we call our custom method named RunConfig. In the previous article in this series, the RunConfig method was called ScriptMain. This new version of our code is identical to what you saw in the previous article, except for this one name change, and the deletion of the static declarations for each of the methods.

Building and Calling the Task

The build file which will compile our assembly , load it into memory, and execute our task is shown in Listing 4.

Listing 4: The file config.build compiles our assembly, loads it into memory, and executes our task. Type nant -buildfile:config.build to run this file.

<?xml version="1.0"?>
<project name="NAnt" default="run">

	
	<property name="libName" value = "bin\ConfigTask.dll"/>

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

	<target name="setValues">
		<property name="fileName" value="config01\app.config"/>
		<property name="attributeName" value="DatabaseName"/>
		<property name="valueName" value="Zen"/>
	</target>  

	<target name="configWrite" depends="talk">

		<nant buildfile="nant.build" target="configWrite">
			<properties>
				<property name="fileName" value="${fileName}"/>
				<property name="attributeName" value="${attributeName}"/>
				<property name="valueName" value="${valueName}"/>
			</properties>
		</nant>

	</target>

    <!-- Compile the test task and add it then use it. -->
    <target name="build">
        <mkdir dir="bin" />
        <csc target="library" output="${libName}">
            <sources>
                <include name="ConfigTask.cs"/>
            </sources>
            <references basedir="${nant::get-base-directory()}">
                <include name="NAnt.Core.dll"/>
            </references>
        </csc>
    </target>

    <target name="run" depends="build">
        <!-- Dynamically load the tasks in the Task assembly. -->
        <loadtasks assembly="${libName}" />

        <!-- Call our new task -->
        <configtask/>
    </target>

    <target name="clean">
        <!-- Delete the build output. -->
        <property name="configFull" value="${directory::get-current-directory()}\${libName}"/>
		<echo message="Deleting ${configFull}"/>
        <delete file="${configFull}" if="${file::exists('bin\ConfigTask.dll')}" />
    </target>
</project>

The build target found in this code is straightforward NAnt code for creating an assembly.

	<property name="libName" value = "bin\ConfigTask.dll"/>

 <!-- Compile the test task and add it then use it. -->
    <target name="build">
        <mkdir dir="bin" />
        <csc target="library" output="${libName}">
            <sources>
                <include name="ConfigTask.cs"/>
            </sources>
            <references basedir="${nant::get-base-directory()}">
                <include name="NAnt.Core.dll"/>
            </references>
        </csc>
    </target>

The built in csc task is used to perform the compilation, and the library is saved to the relative path bin\ConfigTask. If the bin directory does not exist, it will be created automatically using the built in mkdir task. Note that the code references NAnt.Core.dll. This library contains the Task class referenced by our C# code. Any developer who wants to create a large number of sophisticated tasks should become familiar with this DLL.

Once the library is built, it needs to be loaded into memory. The following code performs that task:

<property name="libName" value = "bin\ConfigTask.dll"/>

<target name="run" depends="build">
	<!-- Dynamically load the tasks in the Task assembly. -->
	<loadtasks assembly="${libName}" />

	<!-- Call our new task, converts the message attribute to all caps and displays it. -->
	<configtask/>
</target>

As you can see, there is a specific task, called loadtasks, which is designed to load an assembly into memory. Once it is loaded, we are free to call our task:

 <configtask/>

Assuming that nant.build is in place, and you have three app.config files like the one shown in Listing 0 are in the appropriate place, then you should find changes made to the config files after running the task.

Passing Information into Our Task

As it exists now, our task has a considerable amount of hard coded information in it. For instance, the names of the attributes in the config files that we want to change are hard coded, as well as the names of the config files themselves. There are several ways to remedy this situation, but perhaps the simplest would be to pass in a file name, and then have the C# code in our task read data from that file.The file, would, of course, contain data such as the paths to our config files, as well as the attribute and value pairs that we want to modify.

The code shown in Listing 5 extends our assembly to include an attribute for our task called fileName. You can use this attribute to pass in the file name which will contain configurable data such as the names of the config files and the attributes inside them that you want to change. The code shown here demonstrates how to pass the file name into the task, but the rest of the work will be left either to a future article, or as an exercise for the reader.

Listing 5: Code for passing in data to our assembly. In particular, a new attribute called fileName has been added to this code.

using System;
using NAnt.Core;
using NAnt.Core.Attributes;

namespace NAnt.Examples.Tasks 
{
	[TaskName("configtask")]
		public class ConfigTask : Task 
	{
		private string _fileName;

		[TaskAttribute("fileName", Required=true)]
		public string FileName 
		{
			get { return _fileName; }
			set { _fileName = value; }
		}

		public static void NewProp(Project project, 
								   string fileName,
								   string attributeName,
								   string valueName)
		{
			const string fileNameProperty = "fileName";
			const string attributeNameProperty = "attributeName";
			const string valueNameProperty = "valueName";

			project.Properties.Remove(fileNameProperty);
			project.Properties.Add(fileNameProperty, fileName);

			project.Properties.Remove(attributeNameProperty);
			project.Properties.Add(attributeNameProperty, attributeName);

			project.Properties.Remove(valueNameProperty);
			project.Properties.Add(valueNameProperty, valueName);
		}

		public static void DoFileNames(Project project, string attributeName, string valueName)
		{
			String[] fileNames = {"config01\\App.config", 
				"config02\\App.config", 
				"config03\\App.config"};

			for (int i = 0; i < 3; i++) 
			{
				NewProp(project, fileNames[i], attributeName, valueName);
				project.Execute("configWrite");
			}

		}

		public static void RunConfig(Project project) 
		{   
			project.Execute("setValues");
			DoFileNames(project, "DatabaseName", "Zendo");
			DoFileNames(project, "AppName", "TradeMaster");
			DoFileNames(project, "FtpAddress", "localhost");
		}

		protected override void ExecuteTask() 
		{   
			RunConfig(Project);
			Log(Level.Info, "You passed in: " + _fileName);
		}
        
	}
}

The code that we care about in this new version of our assembly is really very simple:

private string _fileName;

[TaskAttribute("fileName", Required=true)]
public string FileName 
{
	get { return _fileName; }
	set { _fileName = value; }
}

A private variable named _fileName is declared at the top of the file. Then the attribute for our task is declared, and finally we create a simple property called FileName which implements the new attribute. Please note that we have set the Required flag to true, which means that the fileName attribute must be included when you call this task.

Making use of this attribute is very simple. As you recall, in Listing 4 we called our task from a build file with this code:

<configtask/>

Now we add our new TaskAttribute to the XML which calls out task:

<configtask fileName="MyFile.txt"/>

At the very end of our new version of the assembly, shown in Listing 5, we print out the value of our attribute using the Log method of Task::Project. In actual working code, the task should open the file and read data from it. But for now, we will just print out the file name:

protected override void ExecuteTask() 
{   
	RunConfig(Project);
	Log(Level.Info, "You passed in: " + _fileName);
}

Summary

This short article has provided the basic information you need to create your own NAnt tasks. You have seen that tasks are stored into assemblies, and you have seen the code to create a simple assembly that contains a task. A build file was shown that can build the assembly, load it into memory, and then call it. The article ended with a description of how to pass information into a task.

If understand the code demonstrated in this and previous articles, then you should be master of enough information to really put NAnt to work. The NAnt project is beautiful designed, and the mechanisms it provides for either writing C# script or creating your own tasks are both elegant and highly usable.

Five or six years ago, I remember sitting in the LA airport talking to my friend Mark Miller. At the time, he was interested in creating a plug and play, extensible architecture for his CodeRush product. He was very fired up about the concept, and we talked about the idea for over an hour. All these years later, projects like Eclipse and NAnt have brought that vision to fruition. A lot of smart programmers have been thinking about these ideas for years, and it is great to see them as beautifully executed as they are in the NAnt project.

Using NAnt to Update .NET Config Files, Part 02

This article completes the two part series on using NAnt to alter .NET config files. The previous article in this series demonstrated how to use NAnt properties, how to use multiple NAnt build files in a single project, and how to use xmlpeek and xmlpoke. This article will show how to pass data back and forth between NAnt files, how to reuse NAnt code, and how to add some simple C# scripts into your build files. This later technique will allow you to add the expressive power of C# to a simple NAnt XML build script.

Code Reuse in NAnt

Let’s take a second look at the NAnt code for updating a .NET config file:

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

Notice the two areas shown in bold. The first is the name of an attribute in the config file that is marked for alteration. The second is the new value to be assigned to the attribute.

Now take a second look at the .NET config file we want to change:

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

It should be clear that the areas of the NAnt code shown in bold reference the areas in the config file shown in bold. Therefore, if we want to write code to change the DatabaseName in the config file, we would come up with something like this:

	<target name="configWrite">
		<xmlpoke
			file="Config01/app.config"
			xpath="/configuration/appSettings/add[@key='DatabaseName']/@value"
			value="Zen">
		</xmlpoke>
	</target>

After running both code fragments shown so far in this section, the config file would look like this:

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

If you compare the two NAnt code samples you can see that they are identical except for two words. If we were writing C# code, we would see immediately that there is no need to write out two nearly identical blocks of code. Instead, we would create a single block of code, put it in a method, and pass in the two variable parts as parameters. Our C# code would then support a simple form of code reuse: the same method would be called twice; it would be reused. As you shall see, the same thing can be accomplished in NAnt code through the use of properties.

Consider the block of NAnt code shown in Listing 0, which takes us part way to our goal:

Listing 0: This chunk of code abstracts parts of the xmlpoke task into two properties called attributeName and valueName. Though not true variables, these properties can, at least potentially, be changed dynamically.

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

	<property name="attributeName" value="DatabaseName"/>
	<property name="valueName" value="Zen"/>

	<target name="configWrite">
		<xmlpoke
			file="Config01/app.config"
			xpath="/configuration/appSettings/add[@key='${attributeName}']/@value"
			value='${valueName}'>
		</xmlpoke>
	</target>

</project>

Two properties are defined at the top of the file. Each of these properties are then used in the target called configWrite. The end result is that the configWrite target now has two "variables" in it, that can be changed depending on the value of our two properties. The problem we need to solve now is finding a way to dynamically change these properties.

Passing Data Between Two NAnt Files

We are now part way to our solution. What we need is a way to set the two properties shown in Listing 0 called attributeName and valueName from outside of this file. In particular, we want to be able to call the code shown in Listing 0 from our default.build file. To call one build file from another build file you should use a built in task called nant. When I say that nant is a built in task, I mean that it is part of NAnt itself, and can be called by anyone who uses the product.

Consider the following two files, the first called nant.build and the second default.build:

Listing 1: nant.build is the file that will be called. It is the callee.

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

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

   	<target name="configWrite" depends="talk">

		<xmlpoke
			file="Config01/app.config"
			xpath="/configuration/appSettings/add[@key='${attributeName}']/@value"
			value='${valueName}'>
		</xmlpoke>
	</target>

</project>

Listing 2: default.build is the main file. It is the one that calls into nant.build. It is the caller.

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

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

	<target name="configWrite" depends="talk">

		<nant buildfile="nant.build" target="configWrite">
			<properties>
				<property name="attributeName" value="DatabaseName"/>
				<property name="valueName" value="Velocity"/>
			</properties>
		</nant>

	</target>
	
</project>

Take a look at nant.build, shown in Listing 1. As you can see, it uses the properties called attributeName and valueName, but they are not defined anywhere inside the file. Looking at Listing 2, however, we see that there is a section of the target called configWrite that defines both of these properties:

<properties>
	<property name="attributeName" value="DatabaseName"/>
	<property name="valueName" value="Velocity"/>
</properties>

If you look closer, you can see that the nested element called properties is part of a larger task called nant. The built-in task called nant is used to call our second build file. The nant task explicitly supports a nested element called properties, which is designed to allow you to pass properties back and forth between NAnt build files.

	<nant buildfile="nant.build" target="configWrite">

For good measure, I specify the desired target in nant.build. This step is not entirely necessary since configWrite is the default target in nant.build. However, I wanted to show you how the syntax for calling a particular target works in case you ever need it. Here’s another way of looking at the syntax for the nant task:

	<target name="configWrite" depends="talk">

		<nant buildfile="nant.build" target="configWrite">
			.... Code omitted here.
		</nant>

	</target>

The code that is omitted defines the properties that will be passed to the second build file.

Take one more moment to look again at the code discussed in this section of the text:

<nant buildfile="nant.build" target="configWrite">
	<properties>
		<property name="attributeName" value="DatabaseName"/>
		<property name="valueName" value="Velocity"/>
	</properties>
</nant>

First we call the nant task, then we specify the properties that will be set in the targeted build file. Once you understand how to pass properties back and forth between NAnt build files, you will have mastered a key building block that allows you to tap into the power and flexibility of this tool.

Changing Multiple Config Files at Once

You now have enough information to come up with your own solutions to our problem. However, I want to show how to solve this problem by introducing C# script into our NAnt build files. Once you know how to add a little script of this type to your files, you will find that there is very little you cannot accomplish when writing NAnt build scripts.

Recall that we want to change three config files at one time. In the previous version of nant.build that I have shown you, the name of the config file was hard coded. To give us more flexibility, I’m going to slightly alter nant.build. In Listing 3, the fileName is specified as a property, and hence can be changed at run time.

Listing 3: A new version of NAnt.build, that will be called by default.build.

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

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

   	<target name="configWrite" depends="talk">

		<xmlpoke
			file="${fileName}"
			xpath="/configuration/appSettings/add[@key='${attributeName}']/@value"
			value='${valueName}'>
		</xmlpoke>

</target>

</project>

This code supports changing the file name of the config file, the name of the attribute, and the value of that attribute. This one short build file can thus be used to change multiple properties in multiple config files. All we have to do is find a way to call this file with the appropriate properties set to the appropriate values. Listing 4 shows you way to solve this problem.

Listing 4: Here is a new version of default.build. By changing three words in this script, you can change the values of attributes in multiple config files.

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

	<target name="setValues">
		<property name="fileName" value="config01\app.config"/>
		<property name="attributeName" value="DatabaseName"/>
		<property name="valueName" value="Zen"/>
	</target>  

	<target name="configWrite" depends="talk">

		<nant buildfile="nant.build" target="configWrite">
			<properties>
				<property name="fileName" value="${fileName}"/>
				<property name="attributeName" value="${attributeName}"/>
				<property name="valueName" value="${valueName}"/>
			</properties>
		</nant>

	</target>


	<target name="fancyCode">

		<script language="C#">
			<code>
			<![CDATA[
			public static void NewProp(
				Project project, string fileName, 
				string attributeName, string valueName)
			{
				string fileNameProperty = "fileName";
				string attributeNameProperty = "attributeName";
				string valueNameProperty = "valueName";

				project.Properties.Remove(fileNameProperty);
				project.Properties.Add(fileNameProperty, fileName);

				project.Properties.Remove(attributeNameProperty);
				project.Properties.Add(attributeNameProperty, attributeName);

				project.Properties.Remove(valueNameProperty);
				project.Properties.Add(valueNameProperty, valueName);
			}

			public static void DoFileNames(Project project, 
				string attributeName, string valueName)
			{
				String[] fileNames = {"config01\\App.config", 
					"config02\\App.config", 
					"config03\\App.config"};

				for (int i = 0; i < 3; i++) 
				{
					NewProp(project, fileNames[i], 
						attributeName, valueName);
					project.Execute("configWrite");
				}
					
			}

			public static void ScriptMain(Project project) 
			{   
				project.Execute("setValues");
				DoFileNames(project, "DatabaseName", "Zen");
				DoFileNames(project, "AppName", "TradeMaster");
				DoFileNames(project, "FtpAddress", "localhost");
			}
			]]>
			</code>
		</script>

	</target>

</project>

If you look at this code, you will see that it includes not only XML, but also C# code. NAnt gives you the ability to embed C# script in the midst of your XML build scripts. I will explain a little bit about writing C# script in a build file in one moment. Before doing that, however, I want to spend a moment discussing how to use the build file shown in Listing 4.

To run the build file itself, you need only type nant at the command line. Assuming that nant.exe is on your path, then this script will be executed, and the target called fancyCode will run by default.

If you want to change the values of attributes in your config files, you need focus on only one C# method from Listing 4. The method in question is called ScriptMain:

	public static void ScriptMain(Project project) 
	{   
		project.Execute("setValues");
		DoFileNames(project, "DatabaseName", "Zen");
		DoFileNames(project, "AppName", "TradeMaster");
		DoFileNames(project, "FtpAddress", "localhost");
	}

As shown, this method sets the values in three separate .NET App.config or Web.config files. In particular, it sets the DatabaseName to Zen, the AppName to TradeMaster and the FtpAddress to localhost. If you would rather set them to Velocity, TradeMonster, and ftp.elvenware.com, then you would alter the code so that it reads as follows:

	public static void ScriptMain(Project project) 
	{   
		project.Execute("setValues");
		DoFileNames(project, "DatabaseName", "Velocity");
		DoFileNames(project, "AppName", "TradeMonster");
		DoFileNames(project, "FtpAddress", "ftp.elvenware.com");
	}

As you can see, we only had to change three simple string literals, and then rerun the script. The result was that all three config files were updated. The first time we ran the script the config files looked like this:

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

After changing our three constants and re-running the script, the files looked like this:

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

Understanding C# Script Use in NAnt

Let’s take a moment to try to understand how C# scripting in a NAnt file works. Consider the following very simple script.

Listing 5: A very simple NAnt build file that contains a simple script section.

<project name="SeeProperties" default="yourTest">

	<target name="yourTest">
	
		<script language="C#">
			<code>
			<![CDATA[
			public static void ScriptMain(Project project) 
			{
				project.Log(Level.Info, "What we think, we become");
			}
			]]>
			</code>
		</script>
	
	</target>

</project>

The default target for this simple build file contains C# script. There is one C# method called ScriptMain. It takes a parameter of type Project. If you want to create a method in a target that will be called by default, then you should define it as shown here. That is, it should have the name ScriptMain and it should take a parameter of type Project. It will then be called by default.

NOTE: There is also a way to call a method explicitly from your code that is not called ScriptMain and does not take Project as a parameter. For an example, see the section called Other Options, found at the end of this file.In the code we are creating, however, we will need an instance of class Project, so ScriptMain suits our purposes.

In the simple example shown in Listing 5, we call a method of the built in Project class called Log. It writes information to the screen, much like the C# method called Console.Write.

We need to use not the Log method, but a property of the Project class called Properties. It gives us access to all the properties in our project, including the properties called attributeName, valueName, and fileName. But before we do that, let’s see how to use Project.Properties.

Using Project.Properties

Project.Properties is of type PropertyDictionary. As you can see from studying the following code, the NAnt class called PropertyDictionary supports the C# IEnumerator interface. Using this interface, we can enumerate over all the members of the Properties class. This enumeration lets us see the dozens of properties found in even the smallest NAnt project. I can’t show you all of them in this article, but here is a simple build script that will allow you to view the properties of a NAnt project on your own machine:

<target name="yourTest">

	<script language="C#">
		<code>
		<![CDATA[
		public static void ScriptMain(Project project) 
		{
		  IEnumerator enums = project.Properties.GetEnumerator();
		  while(enums.MoveNext())
		  {
			  DictionaryEntry entry = (DictionaryEntry)enums.Current;
			  project.Log(Level.Info, entry.Key.ToString());
			  project.Log(Level.Info, entry.Value.ToString());
		  }
		  project.Execute("talk");
		}
		]]>
		</code>
	</script>

</target>

This code first accesses the IEnumerator interface of Project.Properties. It then uses the MoveNext method of the interface to iterate over all the properties in the project. As each property is discovered, the code prints out its name and the value assigned to it.

Calling Project.Properties to Access our Config Files

By now you should have enough theory under your belt to understand the code you saw in Listing 4. Notice that our ScriptMain method begins by calling Project.Execute:

public static void ScriptMain(Project project) 
{   
	project.Execute("setValues");
	DoFileNames(project, "DatabaseName", "Zen");
	DoFileNames(project, "AppName", "TradeMaster");
	DoFileNames(project, "FtpAddress", "localhost");
}

As you can probably guess, calling project.Execute allows us to execute a particular target in our build file. In this case, we want to call setValues so that we can be sure that instances of the properties called fileName, attributeName and valueName exist:

<target name="setValues">
	<property name="fileName" value="config01\app.config"/>
	<property name="attributeName" value="DatabaseName"/>
	<property name="valueName" value="Zen"/>
</target>  

It is not important what values are in these properties, we only want to ensure that the properties exist. Exactly why this properties must exist will become clear in just one moment.

The next step is to call a method we defined called DoFileNames:

public static void DoFileNames(Project project, string attributeName, string valueName)
{
	String[] fileNames = {"config01\\App.config", 
		"config02\\App.config", 
		"config03\\App.config"};

	for (int i = 0; i < 3; i++) 
	{
		NewProp(project, fileNames[i], attributeName, valueName);
		project.Execute("configWrite");
	}				
}

This method first defines an array of fileNames. Here we can list all the .NET app.config or web.config files that we want to alter. The code then enters a loop which calls a method we defined named NewProp. The code then execute the target called configWrite. The loop calls NewProp three times, one for each of the config files that we want to alter.

NewProp begins by declaring string constants that define the attribute values in the config files that we want to change. We then need to set these attributes to the appropriate value. One simple way to do this is to delete any existing property of that name, and then add the property in again with a new value:

public static void NewProp(Project project, 
   string fileName,
   string attributeName,
   string valueName)
{
	const string fileNameProperty = "fileName";
	const string attributeNameProperty = "attributeName";
	const string valueNameProperty = "valueName";

	project.Properties.Remove(fileNameProperty);
	project.Properties.Add(fileNameProperty, fileName);

	project.Properties.Remove(attributeNameProperty);
	project.Properties.Add(attributeNameProperty, attributeName);

	project.Properties.Remove(valueNameProperty);
	project.Properties.Add(valueNameProperty, valueName);
}

The first time we call this method, we are in danger of trying to remove a property that does not exist. That is why I called the setValues target before calling this method. setValues ensures that the value we want to delete really exists.Then it is safe to call Remove::

 project.Properties.Remove(fileNameProperty); 

We are then free to add the property in again, this time associating it with a new value:

 project.Properties.Add(fileNameProperty, fileName); 

Once we have added in all the properties set to the values we want, then the DoFileNames method calls configWrite.

 project.Execute("configWrite")

As you recall configWrite is a target in the default.build file:

<target name="configWrite" depends="talk">

	<nant buildfile="nant.build" target="configWrite">
		<properties>
			<property name="fileName" value="${fileName}"/>
			<property name="attributeName" value="${attributeName}"/>
			<property name="valueName" value="${valueName}"/>
		</properties>
	</nant>
</target>

By the time configWrite is called, each of the three properties, fileName, attributeName, and valueName, are set to the appropriate values. Now we can call nant.build and update the attributes found in the config files.

Everything should make sense to you by now. But just in case, let’s go back now to the beginning and take a second look at ScriptMain.

public static void ScriptMain(Project project) 
{   
	project.Execute("setValues");
	DoFileNames(project, "DatabaseName", "Zen");
	DoFileNames(project, "AppName", "TradeMaster");
	DoFileNames(project, "FtpAddress", "localhost");
}

Here you can see that we call DoFileNames three times. Each time we pass in a pair of attributeNames and valueNames. Each pair sets a different attribute in all three config files. If we wanted to add more configurable attributes to our files, we would just call DoFileNames once more for each new configurable attribute.

Other Options

You have now seen all the important code that I wanted to show you in this article. In this section I will simple tack on a few more tips that you might find useful if you want to write code like that shown in this article.

You have seen you how to write C# code with an entry point called ScriptMain. Here is how to write code that includes a method with an arbitrary name that can be called from an arbitrary point in a task:

<project name="SimpleFunction" default="main">

	<target name="main">

		<script language="C#" prefix="MyMethods" >
			<code>
			<![CDATA[
			[Function("SimpleFunction")]
			public static string SimpleFunction() 
			{
				String buddhaSaying = "Holding on to anger is like " +
					"grasping a hot coal with the intent of throwing "+ 
					"it at someone else; you are the one getting burned.";

				return buddhaSaying;
			}
			]]>
			</code>
		</script>

		<echo message='${MyMethods::SimpleFunction()}'/>

	</target>

</project>

Save this file as simpleFunction.build and call it by writing the following at the command prompt:

nant -buildfile:simpleFunction.build

Note the echo task near the end of this listing. It calls the method we have created called SimpleFunction. The function returns a String, which is in turn displayed by the echo task.

By creating your own functions, or by working with a NAnt feature called an Expression, you can do many amazing things with NAnt build files. For instance, I could have gotten the values for the properties in the config file from the DOS environment. I did this in an earlier version of the script shown in this article. Here is an example of how that code works.

	<target name="configWrite" depends="talk">

		<nant buildfile="nant.build" target="configWrite">
			<properties>
				<property name="fileName" 
					value="${environment::get-variable('fileName')}"/>
				<property name="attributeName" 
					value="${environment::get-variable('attributeName')}"/>
				<property name="valueName" 
					value="${environment::get-variable('valueName')}"/>
			</properties>
		</nant>

	</target>

Sometimes you might need a little more room. In a case like that, you can create your own task using C# script:

<target name="myTask">
<script language="C#" prefix="test" >
            <code>
              <![CDATA[
                [TaskName("usertask")]
                public class TestTask : Task {
                  #region Private Instance Fields

                  private string _fileName;

                  #endregion Private Instance Fields

                  #region Public Instance Properties

                  [TaskAttribute("FileName", Required=true)]
                  public string FileName 
				  {
                      get { return _fileName; }
                      set { _fileName = value; }
                  }

                  #endregion Public Instance Properties

                  #region Override implementation of Task

                  protected override void ExecuteTask() 
				  {
					  Project.Execute("talk");
                      Log(Level.Info, _fileName.ToUpper());
                  }
                  #endregion Override implementation of Task
                }
              ]]>
            </code>
        </script>
        <usertask FileName='app.config'/>
	</target>

You can also write a C# assembly that defines a new task. However, I will leave that subject for another article. For now, you can find out more about writing Scripts here.

Summary

In this article, you have learned how to use some of the more advanced NAnt features. In particular, you have learned about:

  1. Passing data back and forth between NAnt files by using properties
  2. Writing C# script to add more flexibility to your build files.

Future article on this subject may tackle the job of creating new NAnt tasks in separate C# assemblies that can be easily shared with other developers. Such an undertaking is in many ways similar to the C# scripting tasks shown here, only your C# code goes into a standard .NET assembly, and not into a NAnt build file.

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.

Nick Shreds TRex’s Blog Post

Steve Teixeira, now a supplicant, er, sorry, employee for Microsoft — yes, sadly, it is true, he’s shaken the Delphi dust off his boots and drunk deeply from the MS Kool-Aid Stand — responded to my recent CodeFez article about MS not quite getting it in the area of OOPishness. 

The first thing I want to say to Steve is "Hey, thanks!"  It’s been my goal for months now to get a reference from a Microsoft blog to one of my CodeFez articles.  Mission accomplished!

Now that I’ve been nice and appreciative to Steve for fulfilling a long unrealized dream, I’ll proceed to rip his blog post to shreds like a puppy with a Sports Illustrated rubbed in bacon.

Steve discusses five points from my article.  I’ll respond to his comments about those five points, destroying each in turn.  It’ll be like Perry Mason vs. That District Attorney Guy who never won a case. (By the way, That District Attorney dude was named Hamilton Burger.  Wouldn’t you love to have a friend named Ham Burger? The laughs would just keep on rollin’, hanging around in the bar after Ham lost every case to Mason. Good times.)

Point 1:  Steve "refutes" my complaint about the poor design of the myriad of Connection objects in ADO.NET by saying "that isn’t an OOP design issue it’s a product functionality decision."  I must say, my initial response to that was "Huh…? What does ‘product functionality decision’ mean?"  How is this not an OOP design issue?  Instead of designing a single class to do the job, they design multiple classes that can’t replace one another.  In addition, each class requires the use of a database-specific implementation of the IDBDataAdapter which can’t be interchanged either. And don’t even get me started on things like OracleDateTime and OracleParameter.  You can’t even call that stuff "somewhat abstracted and decoupled".  To his credit, Steve concedes that point when he says "Nick also goes on to point out, correctly I think, that BDP is better at insulating the developer from different database vendors."  Well, yeah, exactly! That’s what the Borland Data Provider does:  it uses good OOP technique to encapsulate and abstract ADO.NET so that you don’t have to hard code things like OracleString into your code.  That’s what good OOP design is supposed to do. Steve says it’s not a fundamental design problem, but I say a total lack of abstraction to interfaces and a tight coupling of a specific implementation to the interfaces is bad design.  The BDP doesn’t do this.  That’s good design.

Point 2:  Steve misses the boat altogether when talking about the Style class.  Sure, you can create your own Style class and implement it in your ASP.NET controls.  But to do that, you have to completely abandon the built-in functionality that the framework supplies for dealing with Styles, and do everything "by hand".  The class System.Web.UI.WebControls.Style has a property called ControlStyle which is of type Style.  If you want your control to have a style that doesn’t descend from the Style class — and thus include stuff that you might not want — you are out of luck.  You can’t partake of the WebControl class’s style handling.  You have to do it all yourself.  This is a perfect example of MS not getting it.  They’ve provided a base style for you that you must use no matter what — even if you don’t want some of that functionality in the Style class.   Again — bad design.  It’s poorly designed because it makes assumptions about descendent controls that shouldn’t be made. 

I’ll have to agree with Steve when he laments my example for defining an IStyle.  I did say that I was "designing on the fly."  I didn’t propose my example as the perfect solution, but merely as an example of how it might be done:  i.e. the ControlStyle property should have been an interface instead of a class which requires certain type to be used.

Point 3:  Steve, Steve, Steve, Steve, Stevey, Steve-aroo, Steve-arama!  Wow, you are playing right into my hands, just like that poor cop schmuck in The Usual Suspects when Kevin Spacey played him like a concert piano.  Steve argues "Okay, now we’re getting into framework functionality, not OOP design"  Well, no, the lack of OOP design is exactly what I’m talking about here. How is "framework functionality not OOP design?" Loading, reading, and writing text files is a very basic and common functionality.  One would think that there might be a class that encapsulates that functionality.  For instance, the description of such a class that would be used to do what I discussed might go like this:

  1. Create an instance of the TextFileManager class by passing the filename to the constructor
  2. Alter the third line in the text file, as described in the original problem set, described in the previous articles.
  3. Save the Result.

Simple, clean, neat, orderly, and — dare I say it! — well designed.

Steve’s example drones on and on like this:

  1. Create an instance of something called a StreamReader (you use a StreamReader to handle text?), passing the filename to the constructor
  2. Allocate memory for an array of strings
  3. Read through to the end of the stream, and when you get done doing that, peruse over the result, chopping the string into array entries every time you run into a "return" character
  4. Close the StreamReader
  5. Alter the third item in the array
  6. Create an instance of something called a StreamWriter (you use a StreamWriter to handle text?), passing the proposed filename to the constructor.
  7. Iterate over each item in the array we created above and write each one out to the StreamWriter.

Imagine my mirth, my barely controlled giggles, when Steve follows this up with "You might be able to do this a little more briefly in other languages but not by much." Uh huh.  First, this isn’t a question of language, but a question of OOP.   I’ll leave it to the imagination of the reader to determine which of the two above examples is a better example of encapsulation.  (Hint:  One uses a single class, the other uses two classes and an array.) 

Point 4:  Steve then goes on to write about my "getting data out of a dataset" argument. He writes, "Okay, so the first one returns a System.Object that you have to explicitly convert and the second returns a TField object that has conversion methods hanging off of it.  I admit the TField is handy and maybe even nicer from a usability standpoint, but I have trouble seeing this as a huge issue or an indication that the implementer doesn’t quite get OOP."

Doesn’t quite get OOP? The designer here returns an instance of the root object. Very, very helpful.  Not! The class itself could provide easy conversion to other types, but it doesn’t.  Easy to do, obviously needed, but there’s nothing there.  All you get is an instance of System.Object. And this says to us that the designer "gets OOP"?  What it says to me is that the designer thought that it was quite OOPish to have you go off and call some other function from some other class to perform the basic functionality of retrieving data from a dataset.  Thinking it OOpish to need to call another class to get the original class to perform basic functionality of the field is not my idea of "Thinking in OOP".  As Steve says, the TField class is handy.  It’s handy because its a sound implementation of OOP design to perform the task at hand!  We can call the Convert class a class, and technically it is, but it’s really just a container for library functions.  Needing another class to perform the basic functionality of your class — and yes, returning values is a basic functionality of a dataset — isn’t OOPish.

Point 5:  My Point 5 here wasn’t really meant to be an indictment of the OOPishness (I love that word!) of ADO.NET, but just a general lament.  Sure, you can iterate over a result set with a DataReader, if you are connected to the server! If not, you are pretty much stuck doing the foreach thing over the rows. The notion of a current record requires binding the data to the interface.  Very uncool. The general point stands — the concept of a current record is a basic database concept, and totally devoid from ADO.NET.

I appreciate Steve’s general agreement about sealed classes. (I didn’t even talk about adding final to a method.  Argh, how unfriendly can you get? "Hey, this method used to be virtual, but I’ve categorically decided that you can’t descend from it anymore! Neener, neener, neener!") I think it hard to argue that sealed classes are anything other than totally lame.  I can even go so far as to grant Steve’s basic point that, lame though they are, a programmer should be able to seal a class.  Such a programmer would be a big weenie to do so, but,  hey, that’s the programmer’s decision. And of course, I can mock such a decision.

Steve does argue a few points in favor of sealed — that a class may need to be sealed to help the compiler.  I counter that OOPishness knows not, and should not know,  of compilers.  If you are making OOP design decisions based on what a specific compiler needs, then you aren’t making good OOP design decisions. In fact, the FCL is supposed to be language neutral, so OOP decisions based on compilers isn’t supposed to even be a factor.  He also argues that there might be security reasons for sealing a class.  Well, maybe, but I can’t think of any right now. I’m happy to be educated on that point.

Steve summarizes:  "In summary, I think there is very little evidence in Nick’s anecdotes that points to some fundamental misunderstanding of OOP." Weeeeeeeeeelllll, I beg to differ.  I think every single one of my anecdotes speaks directly to the issue of a lack of good OOP decisions, as illustrated above in my stunning repartee to Steve. Each of my examples speaks about nothing but design decisions made by Microsoft designers that either limit your ability to implement a descendent class, couple tightly your code to a specific class, or force you to use a class that you don’t necessarily want to use. And they are, of course, merely a sampling of things that I could have talked about.  For instance, try to descend from the StringCollection.

Hey, now that Steve is a Microsoftie, I expect him to defend the home field.  But since he is a Microsoftie, I also expect him not to quite get it.