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><?xml version="1.0" encoding="utf-16"?><BuildCompletionEvent xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><BuildUri>vstfs:///Build/Build/4818</BuildUri><TeamFoundationServerUrl>http://tfs:8080/tfs/02</TeamFoundationServerUrl><TeamProject>Project</TeamProject><Id>Test_20130815.10</Id><Url>http://tfs:8080/tfs/web/build.aspx?pcguid=88661c27-0916-48cb-8e39-9b40d8beb457&amp;builduri=vstfs:///Build/Build/4818</Url><Title>Build Test_20130815.10 Successfully Completed</Title><CompletionStatus>Successfully Completed</CompletionStatus><Subscriber>TFSSERVICE</Subscriber><Configuration>Test</Configuration><RequestedBy>Domain\user</RequestedBy><TimeZone>GMT Daylight Time</TimeZone><TimeZoneOffset>+01:00:00</TimeZoneOffset><BuildStartTime>8/15/2013 3:38:48 PM</BuildStartTime><BuildCompleteTime>8/15/2013 3:39:11 PM</BuildCompleteTime><BuildMachine>Master - Controller</BuildMachine></BuildCompletionEvent></eventXml>
<tfsIdentityXml><TeamFoundationServer url="http://tfs:8080/tfs/02/Services/v3.0/LocationService.asmx" /></tfsIdentityXml>
<SubscriptionInfo>
<Classification><PT N="management.isams.co.uk - Release build subscription." /></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 theDataContract
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 theDataMember
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.