ServiceStack and TFS soap alerts for build completion

I struggled with this for long enough to warrant a blog post.

The Goal

To wire up a TFS 2012 build completion event to a ServiceStack application. I want ServiceStack to receive the SOAP request from TFS
and do some work.

I thought this would be simple enough.

Challenges

I have never used SOAP before and to be honest I am accustomed to REST
based web services now. Using SOAP after using REST for so long, really
taught me to appreciate REST more.

My Solution

I'm not going to go through everything that I did incorrectly, so I will
jump to the solution.

The first challenge was determining what TFS actually sends in it's SOAP
request. This is an example of what TFS sends:

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
    <s:Header xmlns:w="http://www.w3.org/2005/08/addressing">
        <w:Action>http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03/Notify</w:Action>
        <w:To>http://192.168.232.51/ErrorReporting.Web/releasemanagement/tfsevents/buildcomplete</w:To>
    </s:Header>
    <s:Body>
        <Notify xmlns="http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03">
            <eventXml>&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;BuildCompletionEvent xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"&gt;&lt;BuildUri&gt;vstfs:///Build/Build/4818&lt;/BuildUri&gt;&lt;TeamFoundationServerUrl&gt;http://tfs:8080/tfs/02&lt;/TeamFoundationServerUrl&gt;&lt;TeamProject&gt;Project&lt;/TeamProject&gt;&lt;Id&gt;Test_20130815.10&lt;/Id&gt;&lt;Url&gt;http://tfs:8080/tfs/web/build.aspx?pcguid=88661c27-0916-48cb-8e39-9b40d8beb457&amp;amp;builduri=vstfs:///Build/Build/4818&lt;/Url&gt;&lt;Title&gt;Build Test_20130815.10 Successfully Completed&lt;/Title&gt;&lt;CompletionStatus&gt;Successfully Completed&lt;/CompletionStatus&gt;&lt;Subscriber&gt;TFSSERVICE&lt;/Subscriber&gt;&lt;Configuration&gt;Test&lt;/Configuration&gt;&lt;RequestedBy&gt;Domain\user&lt;/RequestedBy&gt;&lt;TimeZone&gt;GMT Daylight Time&lt;/TimeZone&gt;&lt;TimeZoneOffset&gt;+01:00:00&lt;/TimeZoneOffset&gt;&lt;BuildStartTime&gt;8/15/2013 3:38:48 PM&lt;/BuildStartTime&gt;&lt;BuildCompleteTime&gt;8/15/2013 3:39:11 PM&lt;/BuildCompleteTime&gt;&lt;BuildMachine&gt;Master - Controller&lt;/BuildMachine&gt;&lt;/BuildCompletionEvent&gt;</eventXml>
            <tfsIdentityXml>&lt;TeamFoundationServer url="http://tfs:8080/tfs/02/Services/v3.0/LocationService.asmx" /&gt;</tfsIdentityXml>
            <SubscriptionInfo>
               <Classification>&lt;PT N="management.isams.co.uk - Release build subscription." /&gt;</Classification>
               <ID>274</ID>
               <Subscriber>S-1-5-21-3680174039-3422584590-495469403-1397</Subscriber>
            </SubscriptionInfo>
        </Notify>
    </s:Body>
</s:Envelope>

A quick analysis of the XML reveals that we are only interested in
Notify element and everything under it. The rest is just the basic SOAP envelope. We are mainly interested in the eventXml element, as
this is another encoded XML document explaining exactly what happened in
the build. The contents of that will be outside of the scope of this
post.

From this I was able to build my ServiceStack DTO. This took a lot of
tinkering with, and what you see below is the result of any hours trial
and error.

[Route("/tfsevents/buildcomplete", "POST")]
[DataContract(Namespace = "http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03", Name = "Notify")]
public class Notify : IReturn<BuildCompleteResponse>
{
    [DataMember(Order = 1, Name = "eventXml")]
    public string EventXml { get; set; }

    [DataMember(Order = 2, Name = "tfsIdentityXml")]
    public string TfsIdentityXml { get; set; }
}

There are a few things to note here:

  • The Namespace element of the DataContract attribute is important.
  • The class name must be Notify. ServiceStack does not pick up the Name property of the DataContract attribute.
  • I was unable to get it to work without specifying the order of the properties in the DataMember attribute.
  • The properties must match the element names, including case. Alternatively, you can set the Name property of the DataMember attribute in order to set this correctly and keep the proper c# notation. This is what I have done.

Then comes the Service:

public class TfsBuildService : Service
{
    public object Post(Notify dto)
    {
        // do work here
    }
}

Finally, the last bit that caused me issues was that I was post-ing my
request to the Uri defined in the Route attribute, namely
/tfsevents/buildcomplete. This resulted in null properties on my DTO. I finally realised that I should be posing to /soap12 instead. Once I
was posting to the correct location, it all started working.

I hope this helps some people.