Wednesday, July 29, 2009

Writing a Custom NAnt Task (Part 3)

Over the past couple of months, I have been working on writing the SchemaSpy Task for NAnt located on CodePlex.  When I began to create a few custom tasks for NAnt in the past, I found very little documentation about how to create one.  Much of the documentation surrounding the process how to create a custom task is based solely on examples without any supporting information on how to expand such.  While this works very well for basic things, there were some experiences I had to work through while writing the task for SchemaSpy that I would like to talk about here.  In this, the third of four parts, we'll be diverting away from the example created by part 1 and 2 and dive straight into examples from the SchemaSpy Task for NAnt code in order to examine how to create a custom element collection for your task.  In this post, we'll be reviewing the Schemas property of the SchemaSpy Task for NAnt.

 

Why Do I Need a Custom Element Collection?

For a very large number of tasks, basic attribute-based task properties should be enough.  However, if you find yourself where you need to allow a list of inputs into your task, you are stuck with two different options.  The first option is to simply create a basic property just like those described in the first two parts in this series in order to set a delimited string from the build file.  While this is easy to code and implement, it is not the most user friendly after the list grows in size.  The second option is to create a child element that represents a collection of elements.  This is a little bit more complex to develop; however, it provides a much better experience for the people who have to maintain the build file.  Unlike a simple decorated class property, a custom element collection requires things to be implemented in the code.

 

Creating Collection Items

The first item that we need to create is a simple object that will represent the children nodes of your custom element collections.  Looking at the code, this is represented by the Schema class in the Schema.cs file.  This class has to inherit from the NAnt.Core.Element class and be decorated with the ElementName attribute, the attribute is used to identify the name of the XML node in the build file.  In the example (see the code below), the name of the element will be "schema".  After the class is established, we are able to add decorated properties that will represent the different attributes for the element. 

   1:  [ElementName("schema")]
   2:  public class Schema : Element 
   3:  { 
   4:      /// <summary>
   5:      /// The name of the schema to analyze and document. 
   6:      /// </summary> 
   7:      [TaskAttribute("schemaName")]  
   8:      [StringValidator(AllowEmpty = false)]  
   9:      public string SchemaName { get; set; } 
  10:  }
 

Creating the Strongly-Typed Collection

The second item we need to add to our task is a strongly-typed collection to store instances of our Schema object we just created. Looking at the code, this is represented by the SchemaCollection class in the SchemaCollection.cs file.  Like many strongly-type collections created in .Net, this class inherits from the System.Collections.CollectionBase class.  Simply by implementing the abstract base class and filling in the code, the custom, strongly-typed collection of Schema objects will be created and ready to use.

 

Implementing the Collection

The third and fourth tasks left to add our collection of custom elements to our NAnt task is to update the task code itself.  Now that we have the collection and child elements created, we have to first create a private variable to hold a new instance of our collection object.  If the collection is not created and instantiated, NAnt will not be able to add items to the collection and throw an error. 

The last thing to do is to create a new decorated property to tell NAnt to access and expect the collection.  Unlike the other properties that we declared that describe node elements, we need to decorate a property with the BuildElementCollection attribute.  The attribute requires three parameters.  The first describes the name of the collection node.  The second describes the name of the children nodes. The last parameter describes the task options.  Once all of these items are set, the new code should look like the below:

   1:  /// <summary>   
   2:  /// Instantiates a default collection to add elements to.   
   3:  /// </summary>   
   4:  private SchemaCollection _schemaCollection = new SchemaCollection();   
   5:       
   6:  /// <summary>   
   7:  /// Gets or sets the collection of schemas to use.  
   8:  /// </summary>   
   9:  [BuildElementCollection("schemas", "schema", Required = false)]  
  10:  public SchemaCollection Schemas   
  11:  {  
  12:       get { return _schemaCollection; }      
  13:       set { _schemaCollection = value; }  
  14:  }

 

A Look at the Build File

With everything completed, the code can be compiled and installed for NAnt to begin using it.  Once all of that is setup, you can access and update the build file to take advantage of the customer collection similar to the example below:

   1:  <example prop1="test">   
   2:      <schemas>   
   3:          <schema schemaName="dbo" />   
   4:          <schema schemaName="sys" />   
   5:      </schemas>   
   6:  </example>

 

Summary

In this section we reviewed the code implemented by the SchemaSpy Task for NAnt I wrote and placed out on CodePlex.  We looked at what it takes to implement a custom collection.  In the next and final installment of this series, we'll be looking at writing a custom task that is used to call an external application and look at what differences there are between it and the task we have been working with in the first two segments of this series.



kick it on DotNetKicks.comShout it

Thursday, July 23, 2009

Writing a Custom NAnt Task (Part 4)

In this final installment of a series of posts looking into how to create a custom NAnt task, we'll dive into how to create a task that executes an external application.  While NAnt has the built in <exec> task to handling command line programs, there may come a time where the possible arguments or the command itself is just too much for such a generic task.  This was the case that I ran into while trying to integrate SchemaSpy, the Java-based database analysis and documentation tool, into my build scripts.  With the number of arguments and complexities of the program, I decided to create my own custom task.  We'll be diving into this task while exploring the differences between a basic task and an executable task.  The source code for this project will refer to the SchemaSpy Task for NAnt located out on CodePlex.

 

A Look at the Default <exec> Task.

NAnt comes bundled with a task that can be used to call external programs as a way to extend the functionality of the build script.  For some applications that take only a few arguments, this is really easy to manage.  This can be seen using a command line program for the DotNetMigrations project also located out on CodePlex

   1:  <exec program="db">
   2:      <arg value="migrate dev 2" />
   3:  </exec>

However, if you are using an item like SchemaSpy, the cleanliness of the solution begins to deteriorate fairly quickly with only a partial set of its argument.

   1:  <exec program="java">
   2:      <arg value="-jar SchemaSpy.jar -t mssql05 -host serverName -db myDb -port 1433 -sso -all -o ..\output" />
   3:  </exec>

When there is a need to integrate a program like SchemaSpy into a tool like NAnt, creating a custom task to streamline the arguments into a more user-friendly manner can greatly ease script authoring and debugging.

 

Task vs. ExternalProgramBase

In previous examples in this series, we would create our custom tasks using the NAnt.Core.Task abstract base class.  This base class gave us everything we needed to do to handle properties, children elements, and similar items.  One item that the Task base class did not handle though was running external processes though.  If you were to attempt to start a new process, an error would occur or the script would hang since the Task base class isn't thread-safe by default.  If you wanted to create the multi-threading code in order to make the process run successfully and safely, then you can definitely do such while inheriting from the Task base class; however, NAnt has already done this for you by providing the ExternalProgramBase abstract base class.  The ExternalProgramBase class inherits from Task and enhances it by providing specific methods and properties for working with external programs.  In addition, it has all of the threading logic established for you.

 

Diving Deeper into ExternalProgramBase

Since ExternalProgramBase inherits from Task, every thing we can do when we inherited from Task previously can still be done when we inherit from ExternalProgramBase.  Where the differences come into play is when we overwrite the ExecuteTask() method.  This method is called by the NAnt engine to start the task's specific function.  In the ExternalProgramBase base class implementation, this method executes the actual external application we want to start.  It does so by looking at the values of 2 properties; ExeName and ProgramArguments.  The ExeName property provides the string name (with extension) of the executable program to run.  The ProgramArguments property provides a string representing the fully list of arguments.  We can still provide our own custom code inside of the ExecuteTask() method, and once we're ready for the external program to start, we can simply set the ExeName property and call base.ExecuteTask().  The base class's ExecuteTask() method will read both properties, create the necessary threads, and execute the program.  Below is a snippet from the SchemaSpy task that illustrates this process.

   1:  /// <summary>
   2:  /// The Program Arguments listing used by the ExternalProgramBase class.
   3:  /// </summary>
   4:  public override string ProgramArguments
   5:  {
   6:      get { return BuildArgumentList(); }
   7:  }
   8:   
   9:  /// <summary>
  10:  /// Executes the task's operation for NAnt.
  11:  /// </summary>
  12:  protected override void ExecuteTask()
  13:  {
  14:      bool isValid = ValidateAttributes();
  15:   
  16:      if (!isValid)
  17:      {
  18:          Log(Level.Error, "Task Attributes are not valid.");
  19:          return;
  20:      }
  21:   
  22:      this.ExeName = "java.exe";
  23:      base.ExecuteTask();
  24:  }

Notice that in the SchemaSpy task for NAnt, I did not directly set the ProgramArguments property prior to calling base.ExecuteTask().  Instead, I had the property execute a method that would do it when the base class is ready.  This method evaluates the various properties/attributes of the task in order to create all of the arguments required by SchemaSpy to run properly.

 

The Updated Build Script

With the custom task created, we can now update the build script to use it instead of relying on the <exec> task.  Below is an example of the SchemaSpy call previously shown.

   1:  <schemaSpy
   2:      jarPath="..\SchemaSpy.jar"
   3:      dbType="mssql-jtds"
   4:      host="MyDatabaseServer"
   5:      port="1433"
   6:      dbName="MyDatabase"
   7:      schemaName="dbo"
   8:      outputDirectory="..\MyDatabaseDocumentation"
   9:      singleSignOn="true" />

 

Summary

Throughout this series of blog posts, we've covered how to make very simple custom tasks for NAnt up through the ability to create complex tasks that contain child elements and/or simplifies running external programs.  Through the steps listed in this series, hopefully all of information you need to create a custom NAnt task will be available to you.  If a scenario comes up that you need assistance with, feel free to post a comment and I'll see what I can do to assist.


kick it on DotNetKicks.com Shout it