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.

No comments yet

Leave a Reply

You must be logged in to post a comment.