Add Chuck Norris to Your Build!

12/01/2012

In this post I’ll show how to write a somewhat more complex and hopefully fun activity for TFS 2010 Team Build (code is available here).  In particular, I’ll make use of the following features:



  • The ActivityTrackingAttribute class

  • Build Extensions (a specialization of WF4 Workflow Extensions)

Since our builds are sometimes long and dull processes, we need some stuff to talk about while they run.  What could be better than discussing the prowess of the one-and-only Chuck Norris?  We’ll have Team Build produce a new Chuck Norris (CN) fact every time a build runs:


image


And now we have a good conversation starter!  Who knows, maybe this is a good way to convince team members to run the build more often during the day…


Design of the CN Activity


In order to implement the activity we need to deal with the following issues:



  1. Finding a source of Chuck Norris facts to display and choosing a random fact each time

  2. Displaying the fact in the Build Log View

  3. Maintaining good Separation of Concerns (SOC) – that is, making sure the code is well-factored

Let’s tackle these one at a time.



Finding the Data


This was actually rather easy.  A quick Google search turned up The Internet Chuck Norris Database, a hilarious site which even contains a REST API!   Great, so this means we can get a single joke back each time by writing appropriate WCF service and data contracts and using the WebHttp binding for interacting with REST services (many thanks to Yaniv Rodensky for helping me out with the WCF details).


Displaying the Data


We want to display the data in the Build Log View – that is, the log in which TFS shows the on-going activity of the build process.  Fortunately, we have the built-in WriteBuildMessage activity for doing precisely this.  It would probably make sense to structure our code in terms of two separate activities:



  • GetChuckNorrisFact – Performs the Web access itself and returns the joke text.  Since this is in fact an IO operation, we want to derive it from AsyncCodeActivity<string> in order to take advantage of the WF4 runtime’s ability to run activities in an asynchronous manner for making the workflow as efficient as possible.

  • DisplayChuckNorrisFact – Compose the GetChuckNorrisFact and WriteBuildMessage activities into a single building block, which we’ll use in our builds.  This enclosing activity will also include error handling by way of the built-in TryCatch activity.   Since DCNF is the only user of GCNF, it makes sense that we make the latter internal rather than public, which in turn means that the WCF interfaces and contracts can be internal as well.  Encapsulation galore!

The interesting question here is why would we choose to create DCNF as a composition of activities rather then simply deriving from CodeActivity and writing everything out in code.  I would say that the reason for doing it this way has to do with staying at the proper abstraction level and making good use of the underlying WF4 technology.  If we consider Team Build 2010 to be a domain-specific language (where our domain is the automated building of software), than activity composition is in fact the creation of compound words.  Taking the language metaphor even further –  GCNF and DCNF are content words while the other activities (TryCatch, WriteBuildMessage, etc.) are function words and fortunately for us, have already been implemented by Microsoft.  I find this similarity to natural language to probably be the least-understood aspect of WF4, but that’s for some other time (and some other post).


Another interesting point with the composite activity has to do with what exactly is displayed in the log.  The structure of DCNF is as follows, with the Try/Catch/Finally boxes being properties in the TryCatch activity:


image


Recall the way TFS displays the activities in the template – the DisplayName of each activity is used in the Build Log View and indentation is determined by how deep it is in the build workflow template.  So what we would have expected to see is the name for each one of the internal composed activities, like such:


image


The fact that this is not so is due to the use of the ActivityTrackingAttribute class.  Notice the attribute on the DisplayChuckNorrisFact activity class:

    [ActivityTracking(ActivityTrackingOption.ActivityOnly)]
public sealed class DisplayChuckNorrisFact : Activity
{
private const string FailureText =
“Chuck Norris must have brought down the Internet, ” +
“that’s why you can’t have any facts!”;

public DisplayChuckNorrisFact()
{
Implementation = () => CreateBody();
}

private Activity CreateBody()
{
var factTextVariable = new Variable<string>(“factText”);
var tryCatch = new TryCatch();
tryCatch.Variables.Add(factTextVariable);

tryCatch.Try = new GetChuckNorrisFact {Result = factTextVariable};
tryCatch.Catches.Add(new Catch<Exception>
{
Action =
new ActivityAction<Exception>
{
Handler =
new Assign<string>
{
To = factTextVariable,
Value = FailureText
}
}
});
tryCatch.Finally = new WriteBuildMessage
{
Importance = BuildMessageImportance.High,
Message = factTextVariable
};

return tryCatch;
}
}
}

This will ensure that only the top-level activity’s DisplayName will be shown in the Build Log View.  Other options are:

  • ActivityTrackingOption.ActivityTree – Output each and every activity’s DisplayName, both top-level and composed  (this is the default option)

  • ActivityTrackingOption.None – Do not output any activity’s DisplayName, neither top-level nor composed

Using this attribute adds a level of control that is more involved (but still possible) to achieve with ‘plain’ C# code (using the CodeActivity route).


Maintaining Separation of Concerns


We already saw some SOC-related issues – namely, the separation of the logical ‘display a random fact’ operation into multiple physical WF4 activities.  We also saw that there are some pretty good reasons to do so, even if it means additional work.  In a similar manner, we would also like to separate the GetChuckNorrisFact activity into two separate parts – the WCF service proxy and a WF4 activity that uses it.


If we were calling a regular SOAP service it would have been very easy to use Visual Studio’s Add Service Reference functionality (or the equivalent svcutil.exe) to generate code for this service’s proxy so we could use it as a black box.  Unfortunately this isn’t currently possible with a REST service, so we need to create this proxy ourselves.  So here’s what we end up with in the Visual Studio project:



  • IChuckNorrisFactService.cs: This file contains several DataContracts and a ServiceContract.  Note that the latter is decorated with:

    • A WebInvoke attribute – signifies that this method is activated using an HTTP GET verb, along with the URI template which maps method parameters (on the client) to the URL passed to the service

    • An AsyncPattern property in the OperationContract – signifies that this method is part of an APM method pair that will be used for asynchronous communication with the service

  • ChuckNorrisWebServiceConsumer.cs:  This is our service proxy.  It uses the WebChannelFactory<T> class along with a WebHttpBinding to connect to the service and retrieve the raw data.  It then uses a DataContractJsonSerializer to deserialize the returned JSON text into .NET objects that we can pass back to the caller.  (As a side note,  the reason for the existence of this class is because I could not get WCF to deserialize the JSON text automatically.  This might have to do with the fact that the service returns its response with a Content-Type header of ‘text\html’ rather than ‘application\json’ or something similar.)

  • The two activities discussed earlier, implemented to use the APM pattern

So now we need to make sure that the GetChuckNorrisFact activity has access to the service proxy.  The easiest way to do this is to simply create a new instance of the proxy inside the activity.  However, this would increase our coupling as GCNF would now need to know how to create an instance of the proxy (not really an issue with this particular implementation, but I hope the point is clear).  Instead, we opt to use a different mechanism, whereby we expect the WF4 host – TFS, in our case – to supply us with an instance of the proxy. 


Note the attribute on the ChuckNorrisWebServiceConsumer class:

   [BuildExtension(HostEnvironmentOption.All)]
public class ChuckNorrisWebServiceConsumer
{
private const string ServiceAddress = “http://api.icndb.com”;
private IChuckNorrisFactService channel;

public IAsyncResult BeginGetFact(AsyncCallback callback, object state)
{
using (
var cf =
new WebChannelFactory<IChuckNorrisFactService>(
new WebHttpBinding(), new Uri(ServiceAddress)))
{
channel = cf.CreateChannel();
return channel.BeginGetFacts(“1”, callback, state);
}
}

public string EndGetFact(IAsyncResult result)
{
var serializer = new DataContractJsonSerializer(typeof (Data));
var data = (Data) serializer.ReadObject(channel.EndGetFacts(result));

return data.Jokes.First();
}
}

The attribute instructs TFS to register this class as a WF4 workflow extension at the build server level, where it can be requested by any activity (think ‘Service Locator Pattern’).  Similar to the BuildActivityAttribute class, it takes a HostEnvironmentOption enumeration which tells it whether the extension may be used by activities that run on the build controller, the build agent or both.

An activity requests the extension by doing two things (see GetChuckNorrisFact.cs):


1.  In the CacheMetadata method, it must notify the workflow host that it expects to use the extension:

protected override void CacheMetadata(CodeActivityMetadata metadata)
{
base.CacheMetadata(metadata);
metadata.RequireExtension<ChuckNorrisWebServiceConsumer>();
}

Since CacheMetadata is run before the workflow starts executing, WF4 can verify that the activity has indeed been registered with the server and is available for use.  If it has not (for example, we forgot to deploy the custom activity DLL to the build server), then the WF4 runtime will throw an exception.  In our terms, this means we will immediately get a build failure – before any  build-related activities have run.


2.  Inside the activity’s execution methods (Execute for CodeActivity and BeginExecute for AsyncCodeActivity), it must request the extension from the activity’s context:  

protected override IAsyncResult BeginExecute(AsyncCodeActivityContext context,
AsyncCallback callback, object state)
{
var serviceProxy = context.GetExtension<ChuckNorrisWebServiceConsumer>();
context.UserState = serviceProxy;
return serviceProxy.BeginGetFact(callback, state);
}

Summary


So there we have it – our very own Chuck Norris fact-retrieving activity for integration into the build.  We saw how to achieve this task using WF4 activity composition, workflow extensions and WCF REST bindings.  While not overly complex, it does show that there is some effort and knowledge required to properly write build activities.  Happy building!

Shout it kick it on DotNetKicks.com

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

2 comments

  1. CertKiller 13/01/2012 ב 13:04

    Excellent post, I really appreicate

    Reply
  2. Lightner24/01/2013 ב 02:40

    Well to me Martial Arts means respect and discipline. I have trained in different Martial arts now
    for well above 15 years. I have seen tons of people
    appear and disappear but one other thing that I have
    noticed happens to be the respect and discipline which
    has changed those peoples perception of life.
    Small children that have started that are now on the wrong side of the
    tracks, always in trouble and no idea how to respect other kids.
    Place them in a controlled environment with discipline and fighting and they
    soon start to understand.
    Martial arts is a great technique for kids in their teens and adults to get rid of their aggression without hurting or
    bullying anyone.

    Reply