Team System

Team Foundation Server Event Service

Brian A. Randell

Code download available at:TeamSystem2008_05.exe(177 KB)

Contents

Project Alerts
Controlling E-Mail Message Formatting
Available Events
Creating Subscriptions
Creating a Custom Event Listener
Final Details

Microsoft built Team Foundation Server (TFS) as a collection of major and minor services including version control, work item tracking, and the EventService service. I classify EventService as a minor or, better yet, supporting service. EventService exposes a set of events that, when fired, can perform actions such as sending e-mail or making a SOAP-based Web service call.

In this column, I'll examine what's available out of the box via the Visual Studio® user interface, what events are exposed by EventService, how you can create and manage subscriptions, and how you can create your own Web service to receive and process events. While I will be using Visual Studio 2008 and TFS 2008, most of this column applies equally to TFS 2005.

Project Alerts

If you start up Visual Studio 2008 with Team Foundation Client (TFC) installed and connect to an existing team project, you'll be able to access the Team menu on the main menu bar. Once you do, you will see a menu item labeled Project Alerts. If you choose this menu item, Visual Studio will open the Project Alerts dialog, which allows you to create e-mail subscriptions for up to four events (see Figure 1). TFS will format the body of the message as either plain text or HTML depending upon your preferences.

Figure 1 Project Alerts for TFC

Figure 1** Project Alerts for TFC **

Visual Studio exposes four events: artifacts are checked-in, a build completes, a build status changes, and someone else changes work items assigned to you. For each event, you can enter one or more e-mail addresses separated by semicolons. You choose the format of the e-mail message (HTML or plain text) on an event-by-event basis. Note that there are more events raised by TFS than those listed in the Project Alerts dialog. (More on this a bit later.)

Controlling E-Mail Message Formatting

TFS controls the formatting of the alert e-mail messages via a set of XSL transforms. You'll find 28 files stored in the folder C:\Program Files\Microsoft Visual Studio 2008 Team Foundation Server\Web Services\Services\v1.0\Transforms. There are four categories of files: eMailTemplate, plantextXsl, XSD, and XSL. Microsoft defines the data layout for its exposed events using the XSD files. The event service uses the XSL and the plaintextXsl files to transform the XML data into the e-mail message body. Microsoft created XSL files for the four events surfaced by the Project Alerts dialog. Note that, in TFS 2008, Microsoft enhanced the Build Completion event and thus created new XSL files suffixed with the numeral 2.

Finally, the eMailTemplate files refer to a set of five additional events that are available via EventService. However, Microsoft is no longer using or supporting these files. Instead, you'll use the DataChangedEvent.xsl file. The Project Alerts dialog doesn't expose these events, so in order to subscribe to them and receive an e-mail notification, you'll need to either use a command-line tool provided by Microsoft (BisSubscribe.exe) or write some code yourself.

To control the format of the notification e-mail messages, you need to edit the appropriate transform files. For the events exposed via the Project Alerts dialog box, you'll want to edit one or more of the respective XSL files. For HTML-formatted e-mail messages, you'll find the core layout for all events stored in TeamFoundation.xsl. You should make a backup copy of any files you intend to change first, and then test your changes. Once you've made your changes, TFS will use the modified transform the next time it raises the event.

Available Events

Microsoft designed the EventService service to be flexible and extensible. There are a couple of techniques you can use to figure out the list of exposed events. I discussed the first mechanism already: just examine the XSD files stored in the Transforms folder.

Programmatically, you can access another support service, the Registration Service, to get a list of services and the events they expose. Figure 2 shows you the code needed to do that and offers an example of the output.

Figure 2 Events Defined by Registered Services

Code

If mtfs IsNot Nothing Then
  Dim rs As IRegistration = _
  DirectCast(mtfs.GetService(GetType(IRegistration)), IRegistration)

  Dim res() As RegistrationEntry = _
    rs.GetRegistrationEntries(String.Empty)

  Debug.WriteLine("Registration Entries and their Event Types.")
  For Each re As RegistrationEntry In res
    Debug.WriteLine(re.Type)
    If re.EventTypes.Length > 0 Then
      For Each et As EventType In re.EventTypes
        Debug.WriteLine("-->" & et.Name)
      Next
    Else
      Debug.WriteLine("-->No Events Found")
    End If
  Next
End If

Output

Registration Entries and their Event Types.
Build
-->Build Completion Event
-->Build Completion Event 2
-->Build Status Changed Event
vstfs
-->BranchMovedEvent
-->NodeCreatedEvent
-->NodePropertiesChangedEvent
-->NodeRenamedEvent
-->NodesDeletedEvent
-->ProjectCreatedEvent
-->ProjectDeletedEvent
Reports
-->No Events Found
Wss
-->No Events Found
WorkItemTracking
-->No Events Found
VersionControl
-->CheckinEvent
TestTools
-->No Events Found

Note that the WorkItemTracking system doesn't expose an event. Yet, if you look in the Transforms folder, you see there is, in fact, a WorkItemChangedEvent.xsd file. In addition, between Visual Studio 2005 and Visual Studio 2008, Microsoft modified how it raises certain events related to Group Security Service (GSS), Authorization, and the Common Structure Service (CSS). Figure 3 provides the current list of events you'll find exposed by EventService. You should consider those events not listed in Figure 3 to be obsolete.

Figure 3 Services and Supported Events

Service Event Listed in Registration Service?
Work Item Tracking WorkItemChangedEvent No
Version Control CheckInEvent Yes
Team Build BuildCompletionEvent2 Yes
Team Build BuildStatusChangeEvent Yes
Team Build BuildCompletionEvent Yes
Common Structure Service ProjectCreatedEvent Yes
Common Structure Service ProjectDeletedEvent Yes
GSS/Authorization/CSS DataChangedEvent No

The new DataChangedEvent, defined in DataChangedEvent.xsd, replaces the events exposed via the vstfs service listed in Figure 2. When TFS fires a DataChangedEvent, you'll find it's one of three subtypes: Structure, Security, or Identity. You'll then need to call the appropriate Web service to get the details related to those events. TFS 2008 fires the BuildCompletionEvent2 event for all new subscriptions that you create for build completion. If you've upgraded your server, TFS will continue to fire the BuildCompleteEvent for any subscriptions that existed at the time of your upgrade.

Creating Subscriptions

If you want to create a subscription for an event not available in the Visual Studio Project Alerts dialog, you have two choices. The first choice is to use the BisSubscribe.exe command-line tool installed with TFS at C:\Program Files\Microsoft Visual Studio 2008 Team Foundation Server\TF Setup. Note that this tool is also available as a part of the Visual Studio Team Server (VSTS ) SDK, which is, in turn, a part of the larger Visual Studio SDK. However, the version in the SDK is out of date, and so you should verify that the version you are using is the same as the one provided with your TFS installation.

BisSubscribe.exe supports two commands: subscribe and unsubscribe. Subscribe is implicit. It supports up to six parameters for subscribe and two for unsubscribe. Figure 4 lists the parameters as provided by the tool's help command.

Figure 4 Command-Line Arguments for BisSubscribe.exe

Parameter Command Notes
eventType Subscribe The name of the event. Case sensitive.
filter Subscribe A filter expression. Default is none.
address Subscribe The e-mail address or Web method URL for the subscriber.
server Subscribe/Unsubscribe The Team Foundation Server name.
tag Subscribe A field to use later to identify this subscription. Default is none.
deliveryType Subscribe Indicates the preferred message delivery type: EmailHtml, EmailPlaintext, or SOAP. Default is SOAP.
id Unsubscribe The integer ID for the subscription to be deleted when unsubscribing.

For example, if you want to subscribe to the check-in event for the entire server, you could issue this command (all on one line):

bissubscribe /eventType CheckinEvent 
/address brianr@tfs.local /deliveryType EmailHtml 
/server tfsrtm08

When you execute this command, BisSubscribe returns a message similar to the following:

TF50001:  Created or found an existing subscription. 
The subscription ID is 7.

Using this subscription ID, you can unsubscribe:

bissubscribe unsubscribe /id 7 /server tfsrtm08

Note that you need to manually track the ID returned from your subscribe commands if you don't plan on writing code. There's no way to get a list of subscriptions from the command line. While you can poke around inside the SQL Server® 2005 databases, that is neither recommended nor supported.

Of course, you might not want e-mail for every event. Thus, you can add a bit of filtering:

bissubscribe /eventType CheckinEvent /address brianr@tfs.local 
  /deliveryType EmailHtml /server tfsrtm08 
  /filter "'Artifacts/Artifact[@TeamProject = MSFAgile]' <> null"

What this filter will do is tell TFS that you're only interested in getting an e-mail notification whenever a check-in occurs in the MSFAgile team project.

While BisSubscribe works, it isn't the most intuitive tool. It also does not provide a way to get a list of your existing subscriptions. Therefore, your second choice—to write some of your own code—looks more promising.

I've created a simple app that you can download. It builds on code from earlier columns on work items that provide the UI and framework for connecting to a TFS 2008 installation. To get the list of subscriptions for your user account, you need to connect to EventService by using the TeamFoundationServer object's GetService method and retrieve an IEventService reference. IEventService exposes an EventSubscriptions method that accepts a user ID in the form of User Domain Name and User ID and returns an array of zero or more Subscription instances:

Dim es As IEventService = _ 
  DirectCast(mtfs.GetService( _ 
    GetType(IEventService)), IEventService)

Dim userSubs As Subscription() = _
  es.EventSubscriptions( _ 
  Environment.UserDomainName & _
  "\" & Environment.UserName)

For Each usersub As Subscription In userSubs
  ' Do something
Next

The Subscription object provides a number of properties, including the all-important ID necessary to unsubscribe a user from an event. Using the ID property, you simply execute the UnsubscribeEvent method on a valid IEventService reference and you're finished.

Creating a new subscription is just as easy, with one caveat. IEventService exposes a SubscribeEvent method. It accepts a number of parameters just like BisSubscribe.exe. The hard work is that you need to create a good user experience for defining the filter expression and you also have to write code to ensure expression correctness. You'll find a community project available on the CodePlex site (codeplex.com/tfseventsubscription) that provides sample code for defining event filters.

Creating a Custom Event Listener

While e-mail notifications are useful, you might also want to execute code in response to a particular event. The EventService service supports SOAP-based notifications, allowing you to catch and process events from just about any type of code you prefer. An easy way to get started is by looking at existing code.

For Team Foundation Server 2005, Microsoft released a sample showing how you could implement Continuous Integration using EventService and some code. There's an article at msdn2.microsoft.com/ms364045 and a code download available. This sample is no longer necessary (and it won't work correctly out of the box) with TFS 2008. However, it provides an example of how you can create your own ASMX-based Web service using just a little bit of C#. Using the Microsoft example for inspiration, I've created a simple Web service that catches the WorkItemChanged event.

The first step requires creating a Web service project. To test this code, I used the Virtual PC image provided by Microsoft—you really want to try this stuff out first before you mess with a production server.

You'll want to use the ASP.NET Web Service Application template for Visual Basic®. Set the name to MSDNMagTFSEvents and place it at C:\Program Files\Microsoft Visual Studio 2008 Team Foundation Server\Web Services\. Also, make sure to uncheck the "Create directory for solution" option.

After Visual Studio has created the project, adjust the project's properties so that the project URL uses port 8080 (do this via the Web tab in the Project designer). Then, using the IIS management tools, create an IIS application for your project under the Team Foundation Server Web. Ensure that you put your IIS application in the Microsoft® Team Foundation Server Application Pool. After you've done this, try to debug your service to make sure you got the configuration correct.

Now, rename Service1.asmx to ProcessEvents.asmx. Change the backing class name from Service1 to ProcessEvents. Finally, if you feel so inclined, update the Web service namespace to something appropriate. Replace the existing HelloWorld WebMethod with the prototype shown in Figure 5.

Figure 5 ProcessEvents WebMethod

<SoapDocumentMethod( _
  "http://schemas.microsoft.com/TeamFoundation/2005" & _
  "/06/Services/Notification/03/Notify", _
  RequestNamespace:="http://schemas.microsoft.com" & _
  "/TeamFoundation/2005/06/Services/Notification/03")> _
  <WebMethod()> _
Public Sub Notify(ByVal eventXml As String, _ 
                 ByVal tfsIdentityXml As String)

End Sub

The first parameter represents the XML document containing the event information. Think of it as being equivalent to the EventArgs parameter in regular Microsoft .NET Framework events. The second parameter identifies the TFS server raising the event.

At this point, you have all you need to get going. When TFS raises an event, EventService will call your Web service. What you do in the method is up to you and your creativity. Figure 6 provides a simple example that writes to the Windows® event log. The code first checks to verify that the event Xml parameter has data. If not, it exits. Assuming there is data to parse, the code uses LINQ to XML to see whether the collection of changed fields includes the Title. If it does, the code grabs the old and new values, as well as the work item's ID, and writes the data out.

Figure 6 Writing to the Event Log

<SoapDocumentMethod( _
  "http://schemas.microsoft.com/TeamFoundation/2005/06" & _
  "/Services/Notification/03/Notify", _
  RequestNamespace:="http://schemas.microsoft.com/TeamFoundation/" & _
  "2005/06/Services/Notification/03")> _
<WebMethod()> _
Public Sub Notify(ByVal eventXml As String, _
  ByVal tfsIdentityXml As String)

  Const logSource As String = "MSDNMagTFSEvents"
  Const logApp As String = "ProcessEvents"
  Dim el As New EventLog(logApp)
  el.Source = logSource

  Try
    el.WriteEntry("Notify event called.")

    If eventXml Is Nothing OrElse eventXml = String.Empty Then
      el.WriteEntry("eventXml data is empty.")
      Return
    Else
      Dim x As XElement = XElement.Load(New StringReader(eventXml))
      Dim referenceName = (From element _
        In x...<ChangedFields>...<ReferenceName> _
        Where element.Value = "System.Title").SingleOrDefault
      If referenceName IsNot Nothing Then
        Dim oldValue = referenceName.Parent.<OldValue>.Value
        Dim newvalue = referenceName.Parent.<NewValue>.Value
        Dim refNameId = (From element _
          In x...<CoreFields>...<ReferenceName> _
          Where element.Value = "System.Id").SingleOrDefault

        ' the Work Item's ID is always included.
        Dim id = refNameId.Parent.<OldValue>.Value
        ' Do something with the data
        Dim sw As New StringWriter()

        sw.WriteLine("Work Item {0} was changed.", id)
        sw.WriteLine("Old Title: " & oldValue)
        sw.WriteLine("New Title: " & newvalue)

        el.WriteEntry(sw.ToString())
      Else
        ' The title wasn't changed. 
        ' Do something else
      End If
    End If

  Catch ex As Exception
    el.WriteEntry(ex.Message, EventLogEntryType.Error)
  End Try
End Sub

Note that the code to create the event log's event source is defined in the sample client application and needs to run first; otherwise, calls to write to the event log will fail. In a real application, you would have your installer define the event source.

To have TFS call your Web service, you will need to create an event subscription for it. You can do that using BisSubscribe with the following command line:

bissubscribe /eventType WorkItemChangedEvent 
  /address http://localhost:8080/MSDNMagTFSEvents/ProcessEvents.asmx 
  /deliveryType Soap 
  /server tfsrtm08

To create an event subscription in code, you would need to write something like the example in Figure 7, also defined in the sample client application.

Figure 7 Subscribing to Events

If mtfs IsNot Nothing Then
  UpdateStatus(stSubscribeforWebService, True)

  Dim es As IEventService = _
   DirectCast(mtfs.GetService(GetType(IEventService)), IEventService)

  Dim dp As New DeliveryPreference()
  dp.Address = String.Format("http://{0}{1}", mtfs.Uri.Authority, _
    "/MSDNMagTFSEvents/ProcessEvents.asmx")
  dp.Schedule = DeliverySchedule.Immediate
  dp.Type = DeliveryType.Soap

  Dim filter As String = Nothing
  If Me.chkOnlySelectedProject.Checked Then
    filter = String.Format("PortfolioProject = '{0}'", _
      Me.cboTeamProjects.SelectedItem.ToString())
  End If

  Try
    Dim subId As Integer = es.SubscribeEvent( _
      mtfs.AuthenticatedUserName, "WorkItemChangedEvent", filter, dp)

    MsgBox(String.Format(otSubscriptionAdded, subId), _
           MsgBoxStyle.Information, otSuccess)

    Me.btnListMySubs_Click(Me.btnListMySubs, EventArgs.Empty)
  Catch ex As Exception
    MsgBox(ex.Message, MsgBoxStyle.Critical, ex.Source)
  End Try

  UpdateStatus(stStatus, False)
End If

Final Details

As with other event-related code, you need to connect to the EventService service using a valid TFS reference. You then create a DeliveryPreference instance and define its properties. The sample code for this column uses the TFS reference to acquire the name and port for the TFS application tier where it assumes you installed the sample Web service. If this is not the case, you'll need to change this in your deployment.

The code sets the delivery schedule to be immediate; however, you can choose either daily or weekly as alternative intervals. You'll want to ensure you use the DeliveryType.Soap option when configuring Web service subscriptions.

Next, you can define a filter string. The sample code provides an option to restrict the notifications to a particular team project, reading the names from the form's combobox of team project names. After you've defined the parameters, you call the Subscribe method. If successful, EventService returns the ID for the new subscription.

There are a few things you need to consider when building your own Web services. If you followed the instructions in this column to configure the service on the application tier, the service runs using the tfsservice account's security context. You did this so that, if necessary, the service can access additional privileged TFS services. If this is not necessary, feel free to use a different deployment model (machine or account) for your own solution.

A second issue you need to be aware of relates to creating event subscriptions. TFS only allows users who are members of the TFS Administrators or the TFS Services security groups to create subscriptions that call Web services.

The TFS EventService service provides lots of hidden power and flexibility just waiting for you to employ in your own applications. Using the code provided here, as well as additional code you can access on Internet sites such as CodePlex, you can mold TFS to fit perfectly in your organization.

Special thanks, as always, to the VSTS Team for answering questions. In particular, Bill Essary from Microsoft provided invaluable information.

Send your questions and comments to mmvsts@microsoft.com.

Brian A. Randell is a Senior Consultant with MCW Technologies LLC. He spends his time speaking, teaching, and writing about Microsoft technologies. Brian is the author of Pluralsight's Applied Team System course and is a Microsoft MVP. Contact Brian via his blog at mcwtech.com/cs/blogs/brianr.