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 **
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( _
"https://schemas.microsoft.com/TeamFoundation/2005" & _
"/06/Services/Notification/03/Notify", _
RequestNamespace:="https://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( _
"https://schemas.microsoft.com/TeamFoundation/2005/06" & _
"/Services/Notification/03/Notify", _
RequestNamespace:="https://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 https://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("https://{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.