How to use WCF to subscribe to the TFS 2010 Event Service [rolling up hours]

by Ewald Hofman 2. August 2010 06:43

There is a lot of information on the web around the TFS Event Service, but that is mainly around ASMX and TFS 2008. I am using Visual Studio 2010, TFS 2010 and WCF. To be able to subscribe to the created event, the easiest is to have a local instance of TFS 2010 running on your machine.

The purpose of my WCF service will be to rollup the hours of the children into the hour fields (estimate, remaining work and completed work) of the (grand)parents. The result will be a framework on which you can build further as it is not smart enough yet to handle all the other link types (like bugs that are assigned to test cases etc.)

This article assumes that you have basic knowledge of WCF.

The steps that are involved into this exercise are:

  1. Create a new Visual Studio WCF Service Application
  2. Create the interface for the rollup Service
  3. Specify the web.config
  4. Subscribe the WCF servcie to the TFS WorkItemChanged event
  5. Write logic in the service to retrieve the changed work item
  6. Update the hours of the parent work item
  7. Deploy the web service to IIS
  8. Test and debug the service

Create a new Visual Studio WCF Service Application

Open Visual Studio 2010 and create a new project of the type WCF Service Application, which is called EventService. Cleanup the proposed situation by removing all methods in the IService1 and the Service1 files and the CompositeType. Then change the name of the files to RollupService and IRollupService (and check whether the name of the class and interface have changed too). We have now an empty WCF Service Application container in which we can add the logic.

Create the interface for the rollup Service

To be able to consume the event, we need to decorate the interface with the ServiceContract attribute and we need to add a new operation Notify that has two string arguments. In short your IRollupService will look like this:

using System.ServiceModel;

namespace EventService
{

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

        [OperationContract(Action = http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03/Notify)]
        [XmlSerializerFormat(Style = OperationFormatStyle.Document)]
        void Notify(string eventXml, string tfsIdentityXml);

    }

}

Next implement the Notify method in the RollupService class as well.

namespace EventService
{
    public class RollupService : IRollupService
    {

        void IRollupService.Notify(string eventXml, string tfsIdentityXml)
        {
            // Add logic
        }

    }
}

 

Specify the web.config

In WCF the web.config is very important and it took me hours to find the correct contents of the config to finally hook up to the TFS 2010 system.

Here is the list of issues I had to solve:

Problem 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'
Issue Because TFS 2010 is now using SOAP 1.2, you need to use wsHttpBinding.
Solution Change the basicHttpBinding to wsHttpBinding

Thanks to Mattias Sköldin his post  http://mskold.blogspot.com/2010/02/upgrading-tfs-event-subscriptions-to.html
   
Problem System.Web.Services.Protocols.SoapException: The message could not be processed. This is most likely because the action 'http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03/Notify' is incorrect or because the message contains an invalid or expired security context token or because there is a mismatch between bindings. The security context token would be invalid if the service aborted the channel due to inactivity. To prevent the service from aborting idle sessions prematurely increase the Receive timeout on the service endpoint's binding.
Issue You have to change the security of the binding to None
Solution Add a new bindig configuration (in the example see the EventServiceBinding) where you set the Security to None

Finally I was able to execute the notification with the following web.config

<?xml version="1.0"?>
<configuration>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="EventServiceBinding">
          <security mode="None" />
        </binding>
      </wsHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="EventServiceBehavior">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <services>
      <service behaviorConfiguration="EventServiceBehavior" name="EventService.RollupService">
        <endpoint address="" binding="wsHttpBinding" bindingConfiguration="EventServiceBinding"
          contract="EventService.IRollupService" />
      </service>
    </services>
  </system.serviceModel>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
  </system.webServer>

</configuration>

Publish the web service to IIS

In Visual Studio right click on the project in the solution explorer, and find the Publish command. When you start the command, you get a dialog to enter the information where you want to publish this WCF Service to.

To exclude any issues with the interference with other web application, I always tend to use a new web site for this (in this example I used port 1001). I use the following information.

image 

Subscribe the WCF servcie to the TFS WorkItemChanged event

In the folder "C:\Program Files\Microsoft Team Foundation Server 2010\Tools” you can find the application bisubscribe with which you can subscribe your events. To register the service to be executed on every change of a work item execute the following command.

"C:\Program Files\Microsoft Team Foundation Server 2010\Tools\bissubscribe" /eventType WorkItemChangedEvent /address http://localhost:1001/RollupService.svc /collection http://localhost:8080/tfs/DefaultCollection

Write logic in the service to retrieve the changed work item

When you build the project and attach to the correct w3wp process, you can debug what is actually send to your service. it turns out that TFS sends this information:

eventXml
<?xml version="1.0" encoding="utf-16"?>
<WorkItemChangedEvent xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <PortfolioProject>MyTeamProject</PortfolioProject>
  <ProjectNodeId>f0fedf0c-3c7f-47e1-b144-ba85fc9765ba</ProjectNodeId>
  <AreaPath>\MyTeamProject</AreaPath>
  <Title>MyTeamProject Work Item Changed: Task 109 - MyNewTitle</Title>
  <WorkItemTitle>k</WorkItemTitle>
  <Subscriber>[DefaultCollection]\Project Collection Service Accounts</Subscriber>
  <ChangerSid>S-1-5-21-1482476501-2139871995-682003330-85089</ChangerSid>
  <DisplayUrl>http://localhost:8080/tfs/web/wi.aspx?pcguid=64925f8d-7df7-4369-a922-6e44d69d781f&amp;id=109</DisplayUrl>
  <TimeZone>W. Europe Daylight Time</TimeZone>
  <TimeZoneOffset>+02:00:00</TimeZoneOffset>
  <ChangeType>Change</ChangeType>
  <CoreFields>
    <IntegerFields>
      <Field>
        <Name>ID</Name>
        <ReferenceName>System.Id</ReferenceName>
        <OldValue>109</OldValue>
        <NewValue>109</NewValue>
      </Field>
      <Field>
        <Name>Rev</Name>
        <ReferenceName>System.Rev</ReferenceName>
        <OldValue>12</OldValue>
        <NewValue>13</NewValue>
      </Field>
      <Field>
        <Name>Area ID</Name>
        <ReferenceName>System.AreaId</ReferenceName>
        <OldValue>203</OldValue>
        <NewValue>203</NewValue>
      </Field>
    </IntegerFields>
    <StringFields>
      <Field>
        <Name>Work Item Type</Name>
        <ReferenceName>System.WorkItemType</ReferenceName>
        <OldValue>Task</OldValue>
        <NewValue>Task</NewValue>
      </Field>
      <Field>
        <Name>Title</Name>
        <ReferenceName>System.Title</ReferenceName>
        <OldValue>MyOldTitle</OldValue>
        <NewValue>MyNewTitle</NewValue>
      </Field>
      <Field>
        <Name>Area Path</Name>
        <ReferenceName>System.AreaPath</ReferenceName>
        <OldValue>\MyTeamProject</OldValue>
        <NewValue>\MyTeamProject</NewValue>
      </Field>
      <Field>
        <Name>State</Name>
        <ReferenceName>System.State</ReferenceName>
        <OldValue>Proposed</OldValue>
        <NewValue>Proposed</NewValue>
      </Field>
      <Field>
        <Name>Reason</Name>
        <ReferenceName>System.Reason</ReferenceName>
        <OldValue>New</OldValue>
        <NewValue>New</NewValue>
      </Field>
      <Field>
        <Name>Assigned To</Name>
        <ReferenceName>System.AssignedTo</ReferenceName>
        <OldValue>Ewald Hofman</OldValue>
        <NewValue>Ewald Hofman</NewValue>
      </Field>
      <Field>
        <Name>Changed By</Name>
        <ReferenceName>System.ChangedBy</ReferenceName>
        <OldValue>Ewald Hofman</OldValue>
        <NewValue>Ewald Hofman</NewValue>
      </Field>
      <Field>
        <Name>Created By</Name>
        <ReferenceName>System.CreatedBy</ReferenceName>
        <OldValue>Ewald Hofman</OldValue>
        <NewValue>Ewald Hofman</NewValue>
      </Field>
      <Field>
        <Name>Changed Date</Name>
        <ReferenceName>System.ChangedDate</ReferenceName>
        <OldValue>30-7-2010 13:39:12</OldValue>
        <NewValue>30-7-2010 13:56:49</NewValue>
      </Field>
      <Field>
        <Name>Created Date</Name>
        <ReferenceName>System.CreatedDate</ReferenceName>
        <OldValue>30-7-2010 11:42:24</OldValue>
        <NewValue>30-7-2010 11:42:24</NewValue>
      </Field>
      <Field>
        <Name>Authorized As</Name>
        <ReferenceName>System.AuthorizedAs</ReferenceName>
        <OldValue>Ewald Hofman</OldValue>
        <NewValue>Ewald Hofman</NewValue>
      </Field>
      <Field>
        <Name>Iteration Path</Name>
        <ReferenceName>System.IterationPath</ReferenceName>
        <OldValue>\MyTeamProject</OldValue>
        <NewValue>\MyTeamProject</NewValue>
      </Field>
    </StringFields>
  </CoreFields>
  <ChangedFields>
    <IntegerFields />
    <StringFields>
      <Field>
        <Name>Title</Name>
        <ReferenceName>System.Title</ReferenceName>
        <OldValue>MyOldTitle</OldValue>
        <NewValue>MyNewTitle</NewValue>
      </Field>
      <Field>
        <Name>Original Estimate</Name>
        <ReferenceName>Microsoft.VSTS.Scheduling.OriginalEstimate</ReferenceName>
        <OldValue>3</OldValue>
        <NewValue>6</NewValue>
      </Field>
      <Field>
        <Name>Remaining Work</Name>
        <ReferenceName>Microsoft.VSTS.Scheduling.RemainingWork</ReferenceName>
        <OldValue>3</OldValue>
        <NewValue>1</NewValue>
      </Field>
      <Field>
        <Name>Completed Work</Name>
        <ReferenceName>Microsoft.VSTS.Scheduling.CompletedWork</ReferenceName>
        <OldValue>0</OldValue>
        <NewValue>3</NewValue>
      </Field>
    </StringFields>
  </ChangedFields>
</WorkItemChangedEvent>
tfsIdentityXml
<TeamFoundationServer url="http://localhost:8080/tfs/DefaultCollection/Services/v3.0/LocationService.asmx" />

Based on the information that is in those arguments it is possible to see the changes of the effort fields (estimate, remaining and completed) from the current work item. You can use a simple xQuery to get the required information. To be able to get the work item, we need:

  1. the url to the TFS Server, which we can find in the tfsIdentityXml
  2. the parent work item, which we can find via the links of the changed work item. The eventXml has the id of the changed work item
  3. the current and previous values of the effort fields, which are in the eventXml

To be able to read from the eventXml, I have created a seperate class to have a seperation of concerns called EventXmlHelper. There is one method in there that reads the EventXml and has some arguments to define what you want to read

    using System;
    using System.Xml;
    public class EventXmlHelper
    {
        public enum FieldSection
        {
            CoreFields,
            ChangedFields
        }

        public enum FieldType
        {
            IntegerField,
            StringField
        }

        public enum ValueType
        {
            NewValue,
            OldValue
        }

        public static T GetWorkItemValue<T>(string eventXml, FieldSection section, FieldType type, ValueType valueType, string refName)
        {
            var path = string.Format("/WorkItemChangedEvent/{0}/{1}s/Field[ReferenceName='{3}']/{2}", section, type, valueType, refName);

            var doc = new XmlDocument();
            doc.LoadXml(eventXml);

            var node = doc.SelectSingleNode(path);

            object text;
            if (node == null)
            {
                if (typeof(T) == typeof(int))
                {
                    text = 0;
                }
                else if (typeof(T) == typeof(string))
                    {
                        text =  "";
                    }
                else
                {
                    throw new NotImplementedException();
                }
            }
            else
            {
                text = node.InnerText;
            }

            return (T)Convert.ChangeType(text, typeof(T));
        }
    }

To be able to communicate with TFS, I also created a helper class. This class opens the connection to TFS and is able to open the parent work item.

    using System;
    using System.Linq;
    using System.Xml;
    using Microsoft.TeamFoundation.Client;
    using Microsoft.TeamFoundation.WorkItemTracking.Client;
    public class TfsHelper
    {
        public TfsTeamProjectCollection TfsInstance { get; set; }

        public TfsHelper(string tfsIdentityXml)
        {
            //Get the url from the tfsIdentity xml.
            var doc = new XmlDocument();
            doc.LoadXml(tfsIdentityXml);

            if (doc.FirstChild == null) throw new Exception("url not found");
            var url = doc.FirstChild.Attributes["url"].Value;

            //the url is in the form of http://localhost:8080/tfs/DefaultCollection/Services/v3.0/LocationService.asmx
            //strip the /Services/v3.0/LocationService.asmx part
            url = url.Substring(0, url.Length - ("/Services/v3.0/LocationService.asmx").Length);

            // Instantiate a reference to the TFS Project Collection
            TfsInstance = new TfsTeamProjectCollection(new Uri(url));
        }

        /// <summary>
        /// Returns the work item with the specified id
        /// </summary>
        public WorkItem OpenWorkItem(int id)
        {
            var store = (WorkItemStore)TfsInstance.GetService(typeof(WorkItemStore));
            return store.GetWorkItem(id);
        }

        /// <summary>
        /// Returns the parent work item of the work item with the specified id. When it has no parent, null is returned.
        /// </summary>
        public WorkItem OpenParentWorkItem(int id)
        {
            // Get the work item with the specified id
            var workItem = OpenWorkItem(id);

            // Get the link to the parent work item through the work item links
            var q = from l in workItem.WorkItemLinks.OfType<WorkItemLink>()
                    where l.LinkTypeEnd.LinkType.LinkTopology == WorkItemLinkType.Topology.Tree
                    && !l.LinkTypeEnd.IsForwardLink
                    select l.TargetId;

            // If there is a link with a parent work item
            if (q.Count() > 0)
            {
                // Return that one
                return OpenWorkItem(q.ElementAt(0));
            }
            else
            {
                return null;
            }

        }

    }

Update the hours of the parent work item

With these helper files, rolling up the hours is a piece of cake. However you still have to harden your code to deal with errors.

        void IRollupService.Notify(string eventXml, string tfsIdentityXml)
        {
            const string refNameEstimate = "Microsoft.VSTS.Scheduling.OriginalEstimate";
            const string refNameRemaining = "Microsoft.VSTS.Scheduling.RemainingWork";
            const string refNameCompleted = "Microsoft.VSTS.Scheduling.CompletedWork";

            // Extract the required information out of the eventXml
            var workItemId = EventXmlHelper.GetWorkItemValue<int>(eventXml, EventXmlHelper.FieldSection.CoreFields,
                                                                  EventXmlHelper.FieldType.IntegerField,
                                                                  EventXmlHelper.ValueType.NewValue, "System.Id");
            var oldOriginalEstimate = EventXmlHelper.GetWorkItemValue<int>(eventXml,
                                                                           EventXmlHelper.FieldSection.ChangedFields,
                                                                           EventXmlHelper.FieldType.StringField,
                                                                           EventXmlHelper.ValueType.OldValue,
                                                                           refNameEstimate);
            var newOriginalEstimate = EventXmlHelper.GetWorkItemValue<int>(eventXml,
                                                                           EventXmlHelper.FieldSection.ChangedFields,
                                                                           EventXmlHelper.FieldType.StringField,
                                                                           EventXmlHelper.ValueType.NewValue,
                                                                           refNameEstimate);
            var oldRemainingWork = EventXmlHelper.GetWorkItemValue<int>(eventXml,
                                                                        EventXmlHelper.FieldSection.ChangedFields,
                                                                        EventXmlHelper.FieldType.StringField,
                                                                        EventXmlHelper.ValueType.OldValue,
                                                                        refNameRemaining);
            var newRemainingWork = EventXmlHelper.GetWorkItemValue<int>(eventXml,
                                                                        EventXmlHelper.FieldSection.ChangedFields,
                                                                        EventXmlHelper.FieldType.StringField,
                                                                        EventXmlHelper.ValueType.NewValue,
                                                                        refNameRemaining);
            var oldCompletedWork = EventXmlHelper.GetWorkItemValue<int>(eventXml,
                                                                        EventXmlHelper.FieldSection.ChangedFields,
                                                                        EventXmlHelper.FieldType.StringField,
                                                                        EventXmlHelper.ValueType.OldValue,
                                                                        refNameCompleted);
            var newCompletedWork = EventXmlHelper.GetWorkItemValue<int>(eventXml,
                                                                        EventXmlHelper.FieldSection.ChangedFields,
                                                                        EventXmlHelper.FieldType.StringField,
                                                                        EventXmlHelper.ValueType.NewValue,
                                                                        refNameCompleted);

            // Create a new TFS helper
            var tfsHelper = new TfsHelper(tfsIdentityXml);

            // Get the parent work item
            var parentWorkItem = tfsHelper.OpenParentWorkItem(workItemId);

            // If there is one
            if (parentWorkItem != null)
            {
                // Update the effort fields with the difference between the old and the new value
                parentWorkItem.Fields[refNameEstimate].Value =
                    Convert.ToInt32(parentWorkItem.Fields[refNameEstimate].Value) + newOriginalEstimate -
                    oldOriginalEstimate;
                parentWorkItem.Fields[refNameRemaining].Value =
                    Convert.ToInt32(parentWorkItem.Fields[refNameRemaining].Value) + newRemainingWork - oldRemainingWork;
                parentWorkItem.Fields[refNameCompleted].Value =
                    Convert.ToInt32(parentWorkItem.Fields[refNameCompleted].Value) + newCompletedWork - oldCompletedWork;

                parentWorkItem.Save();
            }
        }

Test and debug the service

To test your service, you can create two task work items which have a parent/child relation. Now attach Visual Studio to the w3wp process that hosts the web service (Tools –> Attach to Process) and set a breakpoint in your code. Then modify the effort fields in the child work item and wait. It can take 1 to 2 minutes before the event is processed and your breakpoint is hit. See debugging tip #2 how to change this.

Tips for debugging:

#1: When the service is not working as you would expect, you can follow the post of Grant Holiday how to see the errors the event system is having http://blogs.msdn.com/b/granth/archive/2009/10/28/tfs2010-diagnosing-email-and-soap-subscription-failures.aspx

#2: The Job Agent Service, which is the agent that calls our service, is configured by default to run every 2 minutes. You can change the setting by following the instructions at http://blogs.msdn.com/b/chrisid/archive/2010/03/05/faster-delivery-of-notifications.aspx

Tags:

TFS SDK | VSTS 2010 | Work items

Comments

1/19/2011 4:04:34 AM #

Shay

Ewald hi,

A very nice sample.
It does not, however, deal with the scenario of adding a new child link to a WI which already exists,
or removing a child from it's parent.
Am I missing something?
Do you know of a more complete implementation for effort field rollup? Even a commercial one?

Thank You,
Shay Cicelsky
shay.cicelsky@elbitsystems.com

Shay |

1/19/2011 3:29:42 PM #

Ewald Hofman

I am not aware of a complete implementation that is existent today.

There is however the Project Server Integration CTP (blogs.msdn.com/.../...erver-integration-ctp.aspx). This integration will have an implemented rollup.

Ewald Hofman Netherlands |

11/9/2012 12:30:05 PM #

pingback

Pingback from blogs.blackmarble.co.uk

Change in the System.AssignedTo in TFS SOAP alerts with TFS 2012

blogs.blackmarble.co.uk |

4/9/2013 3:58:51 AM #

trackback

TFS Event Service - WorkItemChangedEvent

TFS Event Service - WorkItemChangedEvent

Name of the blog |

6/24/2013 6:37:41 PM #

trackback

Taking the SuggestedValues rule one step further

Taking the SuggestedValues rule one step further

ObjectSharp Blog |

2/2/2014 10:56:58 AM #

pingback

Pingback from eonlinegratis.com

How To Access / Query Team Foundation Server 2012 With Odata? | Click & Find Answer !

eonlinegratis.com |

Comments are closed

Powered by BlogEngine.NET 1.6.1.0
Theme by Mads Kristensen


ClusterMap

Statistics

Statistics created at 09 Sep 2009

121 posts
493 comments
328 raters
1951911 visit (1042 per day)
24 users online

Recent comments

Comment RSS