Become a Task Master - Customizing the build process using Visual Studio - Part 2
This is part 2 of a 3 part series aimed at understanding how to create and implement custom tasks using either MSBuild or TeamBuild. Click here for part 1. The overall presentation was given at the last Richmond .Net Code Camp.
In part 1 we covered how to add MSBuild to the Visual Studio IDE since the default build engine is not MSBuild. Team Build uses a simular build engine to MSBuild and the following code example will work in both. Now we move into creating custom tasks.
Custom tasks are created by utilizing Microsoft build assemblies. We reference those assemblies and then using those assemblies, (sorry couldn't help it
) we are able to develop our custom tasks by extending the Microsoft.Build.Utilities.Task class.
Start Visual Studio and select a C# project that is a class library. For this example we will call the project DemoCustomTasks. We will change the filename for our class to BuildLogger.cs and will also want to refactor the class name as needed. Set references to Microsoft.Build.Utilities and Microsoft.Build.Framework. Within the source code for my class I need to add 3 using statements:
using System.IO;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
So far my class essentially looks like this:
using System;
using System.IO;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
namespace DemoCustomTasks
{
public class BuildLogger
{
}
}
Since my class must inherit from Microsoft.Build.Utilities.Task, I make the following change...
using System;
using System.IO;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
namespace DemoCustomTasks
{
public class BuildLogger : Task
{
}
}
So far so good. Designing and coding our custom task is essentially the same as any other "normal" class with the exception that since we are inheriting from Microsoft.Build.Utilities.Task, we must conform to certain "inheritence rules" that we get from the base Task class.
The custom task "communicates" with the build via a publicly exposed method whose signature is public override bool Execute(). The Execute() method is derived from the base Task class and returns a boolean to the build, the build automatically uitlizes the Execute() method for the task, you don't directly call Execute() from the project file that contains build instructions, you only call the task. Logical implementation of the custom build functionality is obtained by creating one or more methods (access should be private) that get called within the implementation of Execute(). In our code example we have a LogBuild() method whose access is private and it gets called in Execute(). Consider:
public override bool Execute()
{
try
{
return LogBuild();
}
catch (Exception ex)
{
return false;
}
}
If the LogBuild() method fails then false gets returned to the build. If an exception is thrown then false gets returned to the build. Otherwise the result of LogBuild() is true.
In this example we are using a public property that the build will use to pass information to the task. We implement this property using standard class design.
private string _logFileLocation = String.Empty;
[RequiredAttribute]
public string LogFileLocation
{
get
{
return _logFileLocation;
}
set
{
_logFileLocation = value;
}
}
You will notice that we are decorating our property with the RequiredAttribute attribute (hee hee) that comes from Microsoft.Build.Framework. This forces the build author to set the property when executing the task.
The actual code for the LogBuild() method is not really that important, it is only there to demonstrate creating the custom task. It implements the logic for us to create a custom logging mechanism for our builds. (TFS logs everything about its builds, it may be useful to teams using a non-TFS version of VS??). Again, mainly for demo purposes.
private Boolean LogBuild()
{
bool hasLoggedBuild = false;
//would want to add logic to check for directory structure as well
if (!File.Exists(LogFileLocation))
{
File.CreateText(LogFileLocation);
}
FileStream fileStream = new FileStream(LogFileLocation,FileMode.Append, FileAccess.Write);
StreamWriter textWriter = new StreamWriter(fileStream);
try
{
textWriter.WriteLine("Build Completed @ " + DateTime.Now);
hasLoggedBuild = true;
}
catch (Exception ex)
{
textWriter.WriteLine("Logger Exception Message: " + ex.Message);
hasLoggedBuild = false;
}
finally
{
textWriter.Close();
textWriter.Dispose();
}
return hasLoggedBuild;
}
The full code for my task is below:
using System;
using System.IO;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
namespace DemoCustomTasks
{
public class BuildLogger : Task
{
private string _logFileLocation = String.Empty;
public override bool Execute()
{
try
{
return LogBuild();
}
catch (Exception ex)
{
return false;
}
}
[RequiredAttribute]
public string LogFileLocation
{
get
{
return _logFileLocation;
}
set
{
_logFileLocation = value;
}
}
private Boolean LogBuild()
{
bool hasLoggedBuild = false;
//would want to add logic to check for directory structure as well
if (!File.Exists(LogFileLocation))
{
File.CreateText(LogFileLocation);
}
FileStream fileStream = new FileStream(LogFileLocation, FileMode.Append, FileAccess.Write);
StreamWriter textWriter = new StreamWriter(fileStream);
try
{
textWriter.WriteLine("Build Completed @ " + DateTime.Now);
hasLoggedBuild = true;
}
catch (Exception ex)
{
textWriter.WriteLine("Logger Exception Message: " + ex.Message);
hasLoggedBuild = false;
}
finally
{
textWriter.Close();
textWriter.Dispose();
}
return hasLoggedBuild;
}
}
}
So the key points to remember when designing, creating and implementing custom tasks are:
-
Create a project that is a class library.
-
Reference the Microsoft.Build.Utilities and Microsoft.Build.Framework assemblies.
-
Use the referenced assemblies to implement a custom Task that inherits from Microsoft.Build.Utilities.Task.
-
Create a public method called Execute() that returns boolean to the build.
-
Call your implementation method from Execute();
-
Communicate from Build to Task and from Task to Build using Properties.
In the final part of this installment we will integrate our custom tasks into our project files to be used by MSBuild and Team Build (TFS). We will also look at CruiseControl.Net and NAnt. Talk to you then!
-Kevin