Sunday, February 28, 2010

Upgrading TFS event subscriptions to 2010 SDK

Some days ago I started to upgrade one of my TFS customization to the 2010 SDK. The application I moved uses WCF to host its services and automatically subscribes and consumes TFS events. It didn’t turn to be the easy, straight on upgrade I thought it should be. This is my findings during the upgrade.

Relocated TFS SDK assemblies
The first (and about the only) thing I expected was to be replace TfsServer with TfsTeamProjectCollection, and in some cases TfsConfigurationServer. Moving my solution over to a new and clean machine with only VS2010 on it, I discovered that the Tfs SDK Assemblies has moved away from its old locations. After some searching I found the new location C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0

Where did IEventService go ?
After finding the new assemblies, replaced TfsServer with TfsTeamProjectCollection I still had some compiler errors, the IEventService interface was unknown? I tried to search for any information about changes in 2010 but came up short. After some searching I found it again. It had simply moved around in the namespaces and is now located at Microsoft.TeamFoundation.Framework.Client namespace.

Windows 7 security
With all compiler errors fixed, it was time for a first test. I started the application and got direct failure. It turns out that you have to grant rights to url namespaces with
netsh http add urlact url=http://+:8001/ServiceUrl user=mydomain/mysuser
After granting rights to my user the the services starts and the application subscribes to events.

TFS2010 switched to Soap1.2
After some testing I don’t receive any incoming notifications. Following Grant holidays post TFS2010: Diagnosing Email and SOAP subscription failures (http://blogs.msdn.com/granth/archive/2009/10/28/tfs2010-diagnosing-email-and-soap-subscription-failures.aspx) shows me the following error
HTTP code 415: Cannot process the message because the content type 'application/soap+xml; charset=utf-8' was not the expected type 'text/xml; charset=utf-8'
This indicates that tfs has switch to Soap 1.2. To solve this I simply switch the bindings of my services from BasicHttpBinding till wsHttpBinding

Finaly working
The last thing it took to get the events working was to change the security mode to SecurityMode.None. So if you want to set up an WCF endpoint this code will do it, without any extra configuration files.

Uri baseaddress = new Uri("http://" + System.Environment.MachineName + ":8001");
srvHost = new ServiceHost(typeof(NotificationServiceImpl), baseaddress);


// Check to see if the service host already has a ServiceMetadataBehavior
ServiceMetadataBehavior smb = srvHost.Description.Behaviors.Find();
// If not, add one
if (smb == null)
smb = new ServiceMetadataBehavior();
smb.HttpGetEnabled = true;
smb.MetadataExporter.PolicyVersion = PolicyVersion.Default;
srvHost.Description.Behaviors.Add(smb);
// Add MEX endpoint
srvHost.AddServiceEndpoint(
ServiceMetadataBehavior.MexContractName,
MetadataExchangeBindings.CreateMexHttpBinding(),
"mex"
);

srvHost.AddServiceEndpoint(typeof(INotificationService), new WSHttpBinding(SecurityMode.None ), "StructureChangeNotify");
srvHost.Open();


Complete code
Below you can find the complete code, you can also download a zip file with the solution from my skydrive http://cid-5d46cae8c0008cf0.skydrive.live.com/self.aspx/.Public/EventSubscriber2010.zip

// INotifyServices.cs
namespace EventSubscriber
{
[ServiceContract(Namespace = "http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03")]
public interface INotificationService
{


[OperationContract(Action = "http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03/Notify", ReplyAction="*")]
[XmlSerializerFormat(Style = OperationFormatStyle.Document)] /* Took me hours to figure this out! */
void Notify(string eventXml, string tfsIdentityXml);


}
}


//
NotifyServices.cs
namespace EventSubscriber
{
public class NotificationServiceImpl : INotificationService
{
void INotificationService.Notify(string eventXml, string tfsIdentityXml)
{
MessageBox.Show(eventXml);
}
}
}

// Form.cs
using System;
using System.Windows.Forms;
using Microsoft.TeamFoundation.Framework.Client;
using Microsoft.TeamFoundation.Client;

using System.ServiceModel;
using System.ServiceModel.Description;





namespace EventSubscriber
{


public partial class Form1 : Form
{
protected ServiceHost srvHost;
protected int subscriptionId;



public Form1()
{
InitializeComponent();
}



private void cmdStartWCF_Click(object sender, EventArgs e)
{

Uri baseaddress = new Uri("http://" + System.Environment.MachineName + ":8001");
srvHost = new ServiceHost(typeof(NotificationServiceImpl), baseaddress);


// Check to see if the service host already has a ServiceMetadataBehavior
ServiceMetadataBehavior smb = srvHost.Description.Behaviors.Find();
// If not, add one
if (smb == null)
smb = new ServiceMetadataBehavior();
smb.HttpGetEnabled = true;
smb.MetadataExporter.PolicyVersion = PolicyVersion.Default;
srvHost.Description.Behaviors.Add(smb);
// Add MEX endpoint
srvHost.AddServiceEndpoint(
ServiceMetadataBehavior.MexContractName,
MetadataExchangeBindings.CreateMexHttpBinding(),
"mex"
);

srvHost.AddServiceEndpoint(typeof(INotificationService), new WSHttpBinding(SecurityMode.None), "WorkItemChangedNotify");



srvHost.Open();

lblRunning.Text = "Running";



}

private void cmdStopWCF_Click(object sender, EventArgs e)
{
srvHost.Close();
lblRunning.Text = "Stoped";


}



private void cmdSubscribe_Click(object sender, EventArgs e)
{


TfsTeamProjectCollection tpc = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(new Uri(txtServerUrl.Text));
tpc.EnsureAuthenticated();


IEventService eventService = tpc.GetService(typeof(IEventService)) as IEventService;
DeliveryPreference delPref = new DeliveryPreference();
delPref.Address = "http://" + System.Environment.MachineName + ":8001/WorkItemChangedNotify";
delPref.Schedule = DeliverySchedule.Immediate;
delPref.Type = DeliveryType.Soap;


subscriptionId = eventService.SubscribeEvent(System.Environment.UserDomainName + "\\" + System.Environment.UserName, "WorkItemChangedEvent", "", delPref);
lblSubId.Text = subscriptionId.ToString();


}

private void cmdUnsubscribe_Click(object sender, EventArgs e)
{

TfsTeamProjectCollection tpc = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(new Uri(txtServerUrl.Text));

IEventService eventService = tpc.GetService(typeof(IEventService)) as IEventService;
eventService.UnsubscribeEvent(subscriptionId);
lblSubId.Text = "na";
}


}