TFS 2010 – Customizing Build Information (Part 1)

Before reading this post, you should take a moment to read Aaron Hallberg’s post on build process customization in TFS 2010 Beta 1. Now that you fully understand custom activities in Windows Workflow 4.0 (just kidding), I would like to focus on how we have made it much easier to customize the information created and stored along with the build. There are currently multiple mechanisms for getting additional information into the build process, made possible by some activities, extension methods, and integration with the tracking participant (synonymous to the MSBuild logger for those familiar with that framework). This post will focus on using the activity context to create custom information from code.

Build Information (VERY quick primer)

Build information is simply a hierarchical data store that is extensible. Each node in the hierarchy has a Type and a set of Fields (name/value pairs as strings). All information created since TFS 2008, including task logging, build steps, and compilation details, are stored using this mechanism. Since its introduction into the API this hierarchy has always been extensible, but with the introduction of Windows Workflow 4.0 we have tried to make it easier.

Tracking Custom Build Information using the ActivityContext

Tracking in Windows Workflow is the mechanism that a workflow may use to communicate information to the tracking participant(s). For each workflow run in TFS Build 2010 we hook in a custom implementation of a tracking participant, the BuildTrackingParticipant. Along with tracking general activity in the workflow, such as activity execution, we also provide a way to inject custom build information during the workflow in a safe manner by interaction with the tracking participant. In order to facilitate this we have provided a custom tracking record defined as follows (certain classes in the hierarchy have been omitted):

 public sealed class BuildInformationRecord<T>
{
    public T Value { get; set; }
}

This specialized record should be used for writing all custom build information during a workflow. The tracking participant creates information nodes from these records by analyzing the enclosed type via designer-based reflection and converts the object into an IDictionary<String, String> for storage. If complete customization of the data conversion is desired you may provide a custom TypeConverter for T which can take the object and convert it to an IDictionary<String, String> (in most instances this level of customization will not be necessary, but the hook exists if the need arises). The information node type name is extracted from the name of the managed type (e.g. typeof(T).Name), so there is no need to provide this explicitly. This mechanism of writing build information is how all built-in information nodes are written to the build log, excluding the activity tracking nodes themselves.

Illustration by example is typically the easiest way to understand, so next I will describe how we write information nodes of type BuildMessage to the log using this tracking record. The first thing we will need to do is define a class that describes the data we will be storing for a message: Message and Importance. You will find the definition of this class below.

 public sealed class BuildMessage
{
    public String Message { get; set; }
    public BuildMessageImportance Importance { get; set; }
}

As you can see, all we had to do is define a class with a type name and set of properties matching the information type we would like to create. Now, in order to inject a BuildMessage information node into the build log all we need to do is call the appropriate CodeActivityContext.Track(CustomTrackingRecord) or NativeActivityContext.Track(CustomTrackingRecord) method from code, depending on which activity type your custom activity derives. For instance, if you would like to track a message from a code activity you could do the following:

 public sealed class WriteMessage : CodeActivity
{
    protected override void Execute(CodeActivityContext context)
    {
        context.Track(new BuildInformationRecord<BuildMessage>()
        {
            Value = new BuildMessage()
            {
                Importance = BuildMessageImportance.Normal,
                Message = "This is a custom information node",
            },
        });
    }
}

When the tracking participant receives this record, it will essentially perform the following steps:

  1. Find the closest activity tracking node in scope (more details on how this is managed in a later post), using the root node of the current IBuildDetail if none is found.
  2. Create a new IBuildInformationNode as a child of the node from (1) with a type name of “BuildMessage” (derived from typeof(T).Name as described earlier).
  3. Retrieve the TypeDescriptor for T. If the type converter can convert directly to an IDictionary<String, String> , it simply allows the converter to do the heavy lifting. If the converter cannot perform this conversion, an information field will be created from each PropertyDescriptor retrieved from a call to TypeDescriptor.GetProperties(Object) , converting each value to a String using the converter of the PropertyDescriptor. In most cases this will simply invoke the ToString() method of the type. However, we provide explicit converter implementations which cannot be overridden for DateTime and Byte[] properties to ensure appropriate and consistent conversions of these data types.

While this mechanism works fine there is a pretty important caveat with tracking custom information from within a CodeActivity that you should be aware of. With the exception of the AsyncCodeActivity, every operation in a single workflow instance is executed on the same thread, including tracking. So, if you have a long-running execute method where you are attempting to track progress by logging multiple messages you will be surprised when nothing shows up until the activity completes. This behavior is due to the single thread; since your execute method is using the thread the tracking records will not be flushed to the tracking participant until your method returns. As a general rule of thumb, we have tried to keep away from creating long-running and complex code activities in favor of producing much smaller, more modular behavior that we then build into an activity through composition. Most of our activities in the shipping library derive from Activity or Activity<T> , with workflow driving the majority of the logical constructs. Due to this design we needed a way to create build information through composition, which drove us to create some custom activities for exactly this purpose. I plan to explore these activities and how to use them in a future post. Stay tuned …