Extending Team Foundation Server - Administration - Professional Team Foundation Server 2013 (2013)

Professional Team Foundation Server 2013 (2013)

Part V

Administration

Chapter 29
Extending Team Foundation Server

What's in this chapter?

· Getting started with the client object model

· Exploring the server object model

· Building server plug-ins

· Exploring other extension options

Wrox.com Code Downloads for This Chapter

The wrox.com code downloads for this chapter are found at http://www.wrox.com/go/proftfs2013 on the Download Code tab. The code is in the Chapter 29 download and individually named according to the code filenames noted throughout this chapter.

From the very start, Team Foundation Server was built to be extended. Microsoft acknowledged that it would never be able to build all the different features and functionality that customers would want. The philosophy was that Microsoft's own features should be built upon the same API that customers and partners can use to build additional features.

This proved to be a very wise design choice and has led to a thriving ecosystem of products and extensions. Following are some examples of this ecosystem:

· Microsoft partners have built products that provide rich integration with products such as Outlook and Word.

· Competing and complementary Application Lifecycle Management (ALM) products have been built to integrate with Team Foundation Server.

· Consultants have leveraged the APIs to fill gaps to meet their client's very specific requirements.

· The community has built and shared useful tools and utilities for performing common tasks.

· The Visual Studio ALM Ranger community builds tools and solutions that address common adoption blockers and pain points.

· Microsoft itself builds the Team Foundation Server Power Tools to address gaps within the product outside the normal release cycle.

Perhaps the two most successful examples of the extensibility model (and people leveraging it) were the products formerly known as TeamPlain and Teamprise:

· TeamPlain—was a product built on the client object model to provide web browser-based access to Team Foundation Server. When it was first released, it was a “must-have” extension for organizations adopting Team Foundation Server. It allowed non-technical users to access the server and participate in the software development life cycle.

· Teamprise—was a fully featured, cross-platform client implemented in native Java. It allowed Mac, Linux, and Java users to access Team Foundation Server from their native environments.

Ultimately, both of these products and their development teams were acquired by Microsoft, and the products now ship as a standard part of Team Foundation Server. TeamPlain is now incorporated as Web Access, and Teamprise is available as Team Explorer Everywhere. However, the fact that these once-partner products were integrated so tightly into the shipping Team Foundation Server release also gives you a clue to how the team develops the product. The same APIs, events, and protocols available for different parts of the product to talk to each other are made public and are available as extension points for you to integrate with Team Foundation Server.

Extensibility Points

When people talk about Team Foundation Server extensibility, they are likely referring to building something that leverages the client object model or the TFS SDK for Java. The client object model assemblies are installed with Visual Studio Team Explorer. It is the main .NET API used by products, tools, and utilities that interact with Team Foundation Server. The TFS SDK for Java is a very similar client API implemented entirely in Java and used by the Team Explorer Everywhere clients to talk to Team Foundation Server. The TFS SDK for Java is also available as a standalone download and can be redistributed with your applications.

All of the client interaction with the server is performed through web services. Although it is possible to invoke the web services directly, they are not documented, and their use is discouraged. Microsoft reserves the right to change the web service interfaces in any future release, and it maintains only backward compatibility via the client object model.

On the application tier, the web services then interact with services provided by the server object model. The server object model then accesses the internal SQL tables and stored procedures. Figure 29.1 shows how these different components interact.

image

Figure 29.1 Team Foundation Server extensibility architecture

Additionally, the server provides other extensibility points, such as the following:

· Simple Object Access Protocol (SOAP) event subscription notifications

· In-process server events

· Request filters

· Server jobs

The functionality provided within Team Explorer and Excel clients also can be extended. As you can see, just about everything Microsoft ships as part of Team Foundation Server can be extended and built upon to suit your own needs and requirements.

Note

Not all of the extensibility points are available to you if your Team Foundation Server instance is hosted on Visual Studio Online. The Client Object Model, SOAP Event Subscriptions, and Visual Studio extensibility points are all available for both on-premises and Visual Studio Online instances. However, due to the installation requirements, the Server Object Model is available to you only if you have an on-premises installation.

.NET Client Object Model

The client object model is the most commonly used way to programmatically interact with Team Foundation Server. It is the same API that Team Explorer and all .NET-based client-side applications use to access the server.

Note

This chapter only briefly covers the client object model. For more detailed instructions and plenty of examples on how to use the .NET client object model, you should refer to the Team Foundation Server 2013 SDK at http://aka.ms/TFS2013SDK.

Connecting to the Server

Depending on what you want to do, to get connected to the server, you must use one of the following classes defined in the Microsoft.TeamFoundation.Client.dll assembly:

· TfsConnection

· TfsConfigurationServer

· TfsTeamProjectCollection

The following code will connect to the server and retrieve the latest changeset (code file: ClientObjectModelSample.cs).

using System;

using Microsoft.TeamFoundation.Client;

using Microsoft.TeamFoundation.VersionControl.Client;

namespace ClientObjectModelSample

{

class Program

{

static void Main(string[] args)

{

TfsTeamProjectCollection tfs =

new TfsTeamProjectCollection(

new Uri("http://localhost:8080/tfs/DefaultCollection"),

new TfsClientCredentials());

VersionControlServer vcs = tfs.GetService<VersionControlServer>();

int latestChangesetId = vcs.GetLatestChangesetId();

Console.WriteLine("Latest Changeset = {0}", latestChangesetId);

}

}

}

For this example to work, you will need to add a reference to both Microsoft.TeamFoundation.Client.dll and Microsoft.TeamFoundation.VersionControl.Client.dll in the references section of your project.

In this example, you create a connection to the collection using the collection URL. Then, you get the VersionControlServer service using GetService<T>. You then use it to retrieve the ID of the most recent changeset on the collection. It's that simple!

Team Foundation Server Impersonation

Team Foundation Server Impersonation allows a privileged user to execute commands as if the execution had been done by another user. In short, by using the constructor overloads available on the TeamProjectCollection and TfsConfigurationServerclasses, you can pass in the identity of a user to impersonate.

For more information, see “Introducing TFS Impersonation” at http://tinyurl.com/TFSImpersonation. Note that while the article is for Team Foundation Server 2010, this part of the API has not changed.

For another example, see “Using TFS Impersonation with the Version Control Client APIs” at http://tinyurl.com/TFSImpersonationVC.

Team Project Selection Dialog Box

Although you can enumerate the collections and team projects using the client object model, you can also leverage the TeamProjectPicker dialog box. This is the same dialog box that Visual Studio uses and is prepopulated with servers that the user has previously connected to.

The following snippet shows how to create a TeamProjectPicker that allows the user to select a server, collection, and multiple projects (code file: ProjectPicker.cs):

using (TeamProjectPicker tpp = new

TeamProjectPicker(TeamProjectPickerMode.MultiProject,

false))

{

DialogResult result = tpp.ShowDialog();

if (result == DialogResult.OK)

{

// tpp.SelectedTeamProjectCollection.Uri

foreach(ProjectInfo projectInfo in tpp.SelectedProjects)

{

// projectInfo.Name

}

}

}

Note

For more information, see “Using the TeamProjectPicker API” at http://tinyurl.com/TeamProjectPicker.

Handling Multiple API Versions

If you are building an extension using the client object model, you may want to ensure that it works against different server versions. There is no single, definitive way to do this. You can infer the server version by looking at the features and services provided by the server.

The following is an example that does this against the Version Control Service using a PowerShell script that retrieves the VersionControlServer.WebServiceLevel. Then the VersionControlServer.SupportedFeatures property that can be used to infer what the server version is as follows (code file: MultipleApiVersions.ps1):

#

# TFS2013 VersionControl has a WebServiceLevel that gives

# an idea of the version of the server you are talking to

#

# Tfs2012_1 Team Foundation Server 2012 Beta

# Tfs2012_2 Team Foundation Server 2012 RC

# Tfs2012_3 Team Foundation Server 2012 RTM

# Tfs2012_QU1 Team Foundation Server 2012 Update 1

# Tfs2012_QU1_1 Team Foundation Server 2012 Update 1 with Hotfix

# Tfs2013 Team Foundation Server 2013

#

# Prior to TFS 2012, VersionControlServer.SupportedFeatures

# is an indicator of what server version you are talking to

#

# 7 Team Foundation Server 2008 RTM

# 31 Team Foundation Server 2008 SP1

# 895 Team Foundation Server 2010 RTM

# 1919 Team Foundation Server 2010 SP1

#

# Halt on errors

$ErrorActionPreference = "Stop"

$Uri = $args[0]

if ([String]::IsNullOrEmpty($Uri))

{

$Uri = "http://localhost:8080/tfs/DefaultCollection"

}

Add-Type -LiteralPath "C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\

IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.Client.dll"

Add-Type -

LiteralPath "C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\

ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.Common.dll"

Add-Type -LiteralPath

"C:\Program Files (x86)\Microsoft Visual Studio

12.0\Common7\IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.VersionControl.

Client.dll"

Add-Type -LiteralPath "C:\Program Files (x86)\Microsoft Visual Studio

12.0\Common7\IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.VersionControl.

Common.dll"

$Tpc = New-Object Microsoft.TeamFoundation.Client.TfsTeamProjectCollection -

ArgumentList $Uri

$vcs = $Tpc.GetService(

[Microsoft.TeamFoundation.VersionControl.Client.VersionControlServer])

if ($vcs.WebServiceLevel -eq $WebServiceLevel.PreTfs2010)

{

switch ($vcs.SupportedFeatures)

{

7 {"Tfs2008_RTM"}

31 {"Tfs2008_SP1"}

default {"Tfs2005"}

}

}

elseif ($vcs.WebServiceLevel -eq $WebServiceLevel.Tfs2010)

{

switch ($vcs.SupportedFeatures)

{

895 {"Tfs2010_RTM"}

1919 {"Tfs2010_SP1"}

default {"Tfs2010"}

}

}

else

{

$vcs.WebServiceLevel

}

Note

This approach is necessary only to determine programmatically what features a server you are talking to has from the client side. If you are an administrator looking to see exactly what service level your Team Foundation Server instance is running at, then you can easily see this from the Team Foundation Server Administration Console from a Team Foundation Server Application Tier machine.

Distributing the Client Object Model

Once you have built an application, you will probably want to make it available for others to use. Your application will have a dependency on the client object model assemblies that you are not allowed to redistribute with your application.

The general recommendation is that any client that requires the object model should have Visual Studio Team Explorer installed already; however, there is a standalone installer for the TFS Object Model available at http://aka.ms/TFS2013OM.

SOAP Event Subscriptions

All versions of Team Foundation Server include SOAP event subscriptions. You can subscribe to work item changes, check-ins, and other events. In the subscription definition, you specify the event type to subscribe to and the SOAP endpoint that should be called. When an event is triggered, Team Foundation Server calls the Notify web method on your endpoint, and that code is executed.

A great example for the use of SOAP subscriptions came from the Team Foundation Server 2005 release. The product lacked continuous integration build functionality. The community responded by building a service that subscribed to the CheckinEvent. Then, when someone checked in a file to a particular path, the service would start a build on that path.

While there were significant improvements to the way that e-mail alerts were managed in Team Foundation Server 2012, SOAP event subscriptions have remained largely unchanged since the 2010 release. A few new events were added in the 2012 release, and a small number were added in the 2013 release as well. For SOAP event subscribers coming from a version of Team Foundation Server before 2012, two important things should be considered:

· There can be a delay of up to two minutes for event delivery. SOAP event subscriptions are delivered using a job agent job. The default schedule of this job is to run every two minutes.

· The protocol version is now SOAP 1.2. This means the content-type of the request is now text/xml instead of application/soap+xml as in versions of Team Foundation Server before 2010. If you're using Windows Communication Foundation (WCF), you must change your bindings from BasicHttpBinding to WSHttpBinding.

There are two limitations of SOAP event subscriptions that you will want to consider before using them:

· SOAP event subscriptions allow you to react only after the fact. You cannot prevent an event from further execution. Team Foundation Server requests the endpoint asynchronously after the event has happened. This means that they are not very well-suited to “enforcement” activities. They can only send an alert or run further code to change the item back to its previous values.

· Delivery of SOAP event subscriptions is not guaranteed. If your endpoint is unavailable, or if it has a problem processing the event, that event can be missed. If missed events are a problem for you, you will need to periodically run a task that reconciles missed events.

WARNING

For more information, see “Does TFS guarantee event subscription delivery?” at http://tinyurl.com/TFSGuaranteedEvents.

Available Event Types

To retrieve a list of all the SOAP events available for subscription, you can use the IRegistration service with the following PowerShell script (code file: AvailableEventTypes.ps1):

# Halt on errors

$ErrorActionPreference = "Stop"

$Uri = $args[0]

if ([String]::IsNullOrEmpty($Uri))

{

$Uri = "http://localhost:8080/tfs/DefaultCollection"

}

Add-Type -LiteralPath "C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\

IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.Client.dll"

$Tpc = New-Object Microsoft.TeamFoundation.Client.TfsTeamProjectCollection

-ArgumentList $Uri

$reg = $Tpc.GetService([Microsoft.TeamFoundation.Server.IRegistration])

$reg.GetRegistrationEntries($null) | fl Type, EventType

This script calls the GetRegistrationEntries method and outputs the results. Table 29.1 lists the event types available in Team Foundation Server 2013.

Note

GitPushEvent is a new event in Team Foundation Server 2013. Its purpose is fairly self-explanatory, but it serves to show that distributed source control is fully integrated into the product.

Table 29.1 Available SOAP Event Types

Component

Event Type

Build

BuildCompletionEvent
BuildCompletionEvent2
BuildCompletedEvent
BuildStatusChangeEvent
BuildDefinitionChangedEvent
BuildDefinitionUpgradeCompletionEvent
BuildResourceChangedEvent

Version Control

CheckinEvent
ShelvesetEvent

vstfs

BranchMovedEvent
DataChangedEvent
NodeCreatedEvent
NodePropertiesChangedEvent
NodeRenamedEvent
NodesDeletedEvent
ProjectCreatedEvent
ProjectDeletedEvent

Git

GitPushEvent

Work Item Tracking

WorkItemChangedEvent
WITAdapterSchemaConflictEvent

Test Management

TestRunCompletedEvent
TestRunStartedEvent

Discussion

CodeReviewChangeEvent

Building an Endpoint

The easiest way to host an endpoint is to build a Windows Service and use WCF with WSHttpBinding bindings. Alternatively, you can deploy it as a website in IIS.

Note

For more information on creating a SOAP event subscription handler, see Ewald Hofman's blog post, “How to use WCF to subscribe to the TFS Event Service,” at http://tinyurl.com/TFSEventSubWCF. This post is targeted at Team Foundation Server 2010 so you will need to change file paths where appropriate.

Adding the Subscription

Once you have a subscription handler, you must add the subscription to Team Foundation Server. The usual way of doing this in a scriptable form is by running the BisSubscribe.exe tool from C:\Program Files\Microsoft Team Foundation Server 12.0\Tools\ on any of your application tier servers. You can also copy this program to a local machine if you don't want to log on to the server for adding new subscriptions.

In Team Foundation Server 2013, you can also create new Alerts from the same Alerts web pages that you use to create e-mail alerts. Simply change the format of the message to be a SOAP message and specify the web service endpoint in the Send To field, as shown in Figure 29.2.

WARNING

Project Collection Administrator permissions are required to add SOAP subscriptions.

image

Figure 29.2 Creating a SOAP event subscription in the Alerts editor

Listing All Event Subscriptions

If you are a project collection administrator, you may want to see what subscriptions are configured for your collection for all users. This is not possible through BisSubscribe.exe or Alerts Explorer.

You can do this by using the following PowerShell script that calls the GetAllEventSubscriptions method on the IEventService service (code file: ListEventSubscriptions.ps1):

# Get all event subscriptions

#

# Halt on errors

$ErrorActionPreference = "Stop"

$Uri = $args[0]

if ([String]::IsNullOrEmpty($Uri))

{

$Uri = "http://localhost:8080/tfs/DefaultCollection"

}

Add-Type -LiteralPath "C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\

IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.Client.dll"

$Tpc = New-Object Microsoft.TeamFoundation.Client.TfsTeamProjectCollection

-ArgumentList $Uri

$event = $Tpc.GetService([Microsoft.TeamFoundation.Framework.Client.IEventService])

$event.GetAllEventSubscriptions() | fl ID,EventType,ConditionString

If there are subscriptions that you'd like to remove, you can use BisSubscribe.exe /unsubscribe, or use the following PowerShell line after running the previous script:

# Unsubscribe an event

$event.UnsubscribeEvent(<id here>)

Note

SOAP subscriptions can be a convenient way to respond to events with Team Foundation Server, but SOAP is a fairly legacy technology. Additionally, notifications are not guaranteed. If the event is important, you should consider using a server plug-in.

Server Object Model

With all the architectural changes in Team Foundation Server to support multiple collections, the server object model is now more accessible.

Note

For more information on the server object model, see “Extending Team Foundation” on MSDN at http://aka.ms/TFS2013ServerOM.

Server Extensibility Interfaces

Although many of the interfaces in the server object model are marked public, some are more suitable for customization and extending than others. Table 29.2 describes the interfaces and their suitability according to the following guide:

· Suitable—This is intended for third-party extensions. This is used by Team Foundation Server internally for extensibility.

· Limited—Some extension is expected by tool providers, though in a limited fashion.

· Not Suitable—This is dangerous or not really intended for third-party extensions.

WARNING

Extending many of these interfaces can have negative effects on the performance and stability of your servers. The server will also disable any plug-ins that throw exceptions. If you don't want your plug-in disabled, you can catch System.Exceptionand log it so that you can diagnose failure conditions.

Table 29.2 Server Extensibility Interfaces and Their Suitability

Interface

Description

Suitability

ISubscriber

The in-process event system is described later in this chapter.

Suitable

ITeamFoundationRequestFilter

This is intended to allow the extension to inspect and manage all incoming requests. It is described later in this chapter.

Suitable

ISecurityChangedEventHandler

A notification useful for reacting to security changes. The most common use would be to write an auditor of security changes to tie in to your needs. However, you would normally use an ISubscriber, because all security changes are published through events.

Limited

ITeamFoundationHostStateValidator

Used to prevent a collection from being started. If a customer wants to implement custom online/offline semantics, this would allow it.

Limited

ICatalogResourceTypeRuleSet

If you are going to extend the catalog with custom objects, and you want to implement some rules to enforce that your catalog objects conform to a specific set of rules, this is the interface you would implement. This is not a real high-profile interface, and there should not be many extensions in this area.

Limited

IStepPerformer

This is the servicing engine's API. Servicing is a method for performing a set of actions within the job agent scripted through an XML document. This is very similar to a workflow. You can add behaviors to a servicing job by writing IStepPerformers.

Limited

IServicingStepGroupExecutionHandler

These are hooks in the servicing engine that allow you to alter the set of steps executed, or change the state of the execution engine before and after a step group.

Limited

ISecurityNamespaceExtension

This allows you to override behaviors within an existing security namespace. This one is particularly dangerous. If you wanted to implement custom security rules for a given dataset, this will allow you to do it.

Not Suitable

ILinkingConsumer, ILinkingProvider

The linking interfaces are used to extend the artifact system. This system is not currently used extensively throughout the server, and isn't a great place for third parties to add extensions.

Not Suitable

ISubscriptionPersistence

This is used when eventing subscriptions are read and written. This allows subscriptions to be stored in a canonical form, and then they are expanded at evaluation time. There is very little value here for third parties.

Not Suitable

ITeamFoundationSystemHostStateValidator

This interface is internal and not intended for public use. See ITeamFoundationHostStateValidator.

Not Suitable

IIdentityProvider

This interface is to support other Identity types. Team Foundation Server has two identity types built in, which are used in on-premises installations: WindowsIdentity and TeamFoundationIdentity. This interface is what allows the overall support of other Identity types such as ASP.NET Membership, Live ID, custom, and so on. However, some services (such as Work Item Tracking) support only the built-in types. This may be available in a future release for extensions.

Not Suitable

Server Plug-Ins

Team Foundation Server includes the concept of server plug-ins. These plug-ins are relatively straightforward to write and very easy to deploy. Because they are deployed on the server, they don't require any client-side changes to be effective. These attributes make them a great candidate for extending and controlling Team Foundation Server.

Two interfaces are suitable for extending:

· ISubscriber—Used to define real-time, in-process event subscriptions.

· ITeamFoundationRequestFilter—Implementations can inspect all requests.

To use these interfaces, you must add a reference to the Microsoft.TeamFoundation.Framework.Server.dll assembly from the \Application Tier\Web Services\bin directory of your application tier. You will also need a reference to the assemblies that contain the events that you want to subscribe to.

WARNING

If you have multiple application tiers, your server plug-ins and job extensions must be deployed to all of them. If the plug-in is not available on a particular application tier, requests to that application tier will not trigger the plug-in.

ISubscriber: In-Process Eventing

This is the most common place for third parties to use extensions. Team Foundation Server 2013 fires events to all ISubscribers that sign up for events. Almost all major events on the server publish events.

There are two different types of events that you can subscribe to:

· DecisionPoint—You can prevent something from happening.

· Notification—You receive an event as soon as something happens.

In most (but not all) cases, a DecisionPoint and a Notification event will be fired, as shown in Table 29.3. This means that you must check the NotificationType in your event handler; otherwise, your code may run twice when you were expecting it to run only once.

Note

For the ISubscriber Interface definition, see http://aka.ms/ISubscriber.

Table 29.3 Available Notification Events

Component

Server Object Model Assembly

Event

Notification Type

Version Control

Microsoft.TeamFoundation.VersionControl.Server.dll

CheckinNotification

Decision, Notification

PendChangesNotification

Decision, Notification

UndoPendingChangesNotification

Decision, Notification

ShelvesetNotification

Decision, Notification

WorkspaceNotification

Decision, Notification

LabelNotification

Notification

CodeChurnCompletedNotification

Notification

Git

Microsoft.TeamFoundation.Git.Server.dll

PushNotification

Notification, Decision

RefUpdateNotification

Notification, Decision

Build

Microsoft.TeamFoundation.Build.Server.dll

BuildCompletionNotificationEvent

Notification

BuildQualityChangedNotificationEvent

Notification

Work Item Tracking

Microsoft.TeamFoundation.WorkItemTracking.Server.DataAccessLayer.dll

WorkItemChangedEvent

Notification

WorkItemMetadataChangedNotification

Notification (minimal)

WorkItemsDestroyedNotification

Notification (minimal)

Test Management

Microosft.TeamFoundation.TestManagement.Server.dll

TestSuiteChangedNotification

Notification

TestRunChangedNotification

Notification

TestPlanChangedNotification

Notification

TestCaseResultChangedNotification

Notification

TestPointChangedNotification

Notification

TestRunCoverageUpdatedNotification

Notification

BuildCoverageUpdatedNotification

Notification

TestConfigurationChangedNotification

Notification

Framework

Microsoft.TeamFoundation.Server.dll

StructureChangedNotification

Notification

AuthorizationChangedNotification

Notification

Framework

Microsoft.TeamFoundation.Framework.Server.dll

IdentityChangedNotification

Notification

SecurityChangedNotification

Decision, Notification

SendEmailNotification

Decision

HostReadyEvent

Notification

Chat

Microsoft.TeamFoundation.Chat.Server

MessageSentEvent

Notification

MessageDeletedEvent

Notification

MessageUpdatedEvent

Notification

GetMessagesEvent

Notification

RoomCreatedEvent

Notification

RoomDeletedEvent

Notification

RoomUpdatedEvent

Notification

MemberEvent

Notification

MemberAddedEvent

Notification

MemberRemovedEvent

Notification

MemberEnteredEvent

Notification

MemberLeftEvent

Notification

ClientEvent

Notification

ClientCreatedEvent

Notification

ClientDeletedEvent

Notification

Decision Points

DecisionPoint events are triggered before the action is committed and can be used to prevent the action from occurring. Most actions do not allow you to change the values, just accept or deny the request.

WARNING

These are handled on the request processing thread and, therefore, they should be lightweight and execute quickly. Otherwise, they will impact any caller that triggers the event.

For a DecisionPoint notification, your ProcessEvent call will be called before the change occurs. If you deny the request, you can set a message that will be shown to the user. The change will be aborted, and the processing will not continue.

The following code sample shows a simple ISubscriber plug-in that subscribes to the CheckinNotification event and DecisionPoint notification type (code file: DecisionPointSubscriber.cs):

using System;

using Microsoft.TeamFoundation.Common;

using Microsoft.TeamFoundation.Framework.Server;

using Microsoft.TeamFoundation.VersionControl.Server;

namespace DecisionPointSubscriber

{

/// <summary>

/// This plugin will reject any checkins that have comments

/// containing the word 'foobar'

/// </summary>

public class DecisionPointSubscriber : ISubscriber

{

public string Name

{

get { return "Sample DecisionPoint Subscriber"; }

}

public SubscriberPriority Priority

{

get { return SubscriberPriority.Low; }

}

public Type[] SubscribedTypes()

{

return new Type[] {

typeof(CheckinNotification)

};

}

public EventNotificationStatus ProcessEvent(TeamFoundationRequestContext

requestContext, NotificationType notificationType,

object notificationEventArgs, out int statusCode,

out string statusMessage, out ExceptionPropertyCollection

properties)

{

statusCode = 0;

properties = null;

statusMessage = string.Empty;

if (notificationType == NotificationType.DecisionPoint)

{

try

{

if (notificationEventArgs is CheckinNotification)

{

CheckinNotification notification =

notificationEventArgs as CheckinNotification;

// Logic goes here

if (notification.Comment.Contains("foobar"))

{

statusMessage = "Sorry, your checkin was rejected.

The word 'foobar' cannot be used in

checkin comments";

return EventNotificationStatus.ActionDenied;

}

}

}

catch (Exception exception)

{

// Our plugin cannot throw any exception or it will

// get disabled by TFS. Log it and eat it.

TeamFoundationApplicationCore.LogException("DecisionPoint

plugin encountered the following error

while processing events", exception);

}

}

return EventNotificationStatus.ActionPermitted;

}

}

}

Inside the ProcessEvent method, the plug-in checks whether the check-in comment contains the string foobar. If it does, a custom error message is set, and the return value is ActionDenied.

The other possible return values are as follows:

· ActionDenied—Action denied; do not notify other subscribers.

· ActionPermitted—Action permitted; continue with subscriber notification.

· ActionApproved—Similar to ActionPermitted, but do not notify other subscribers.

Notifications

For a Notification event, no return values are expected, and the publication serves as a notification of the occurrence of an event. Notification events are performed asynchronously and, therefore, do not have an impact on the caller. You should still take care to not consume too many resources here.

The following code sample shows a simple ISubscriber plug-in that subscribes to the LabelNotification event:

using System;

using System.Text;

using Microsoft.TeamFoundation.Common;

using Microsoft.TeamFoundation.Framework.Server;

using Microsoft.TeamFoundation.VersionControl.Server;

namespace NotificationSubscriber

{

/// <summary>

/// This request filter will log an event to the Application

/// event log whenever TFS labels are changed

/// </summary>

public class NotificationSubscriber : ISubscriber

{

public string Name

{

get { return "Sample Notification Subscriber"; }

}

public SubscriberPriority Priority

{

get { return SubscriberPriority.Low; }

}

public Type[] SubscribedTypes()

{

return new Type[] {

typeof(LabelNotification)

};

}

public EventNotificationStatus ProcessEvent(TeamFoundationRequestContext

requestContext, NotificationType notificationType,

object notificationEventArgs, out int statusCode,

out string statusMessage, out ExceptionPropertyCollection properties)

{

statusCode = 0;

properties = null;

statusMessage = string.Empty;

if (notificationType == NotificationType.Notification)

{

try

{

if (notificationEventArgs is LabelNotification)

{

LabelNotification notification = notificationEventArgs

as LabelNotification;

StringBuilder sb = new StringBuilder();

sb.AppendLine(string.Format("Labels changed by {0}",

notification.UserName));

foreach (LabelResult label in notification.AffectedLabels)

{

sb.AppendLine(string.Format("{0}: {1}@{2}",

label.Status, label.Label, label.Scope));

}

TeamFoundationApplicationCore.Log(sb.ToString(), 0,

System.Diagnostics.EventLogEntryType.Information);

}

}

catch (Exception exception)

{

// Our plugin cannot throw any exception or it will get

// disabled by TFS. Log it and eat it.

TeamFoundationApplicationCore.LogException("Notification plugin

encountered the following error while

processing events", exception);

}

}

return EventNotificationStatus.ActionPermitted;

}

}

}

Inside the ProcessEvent method, the plug-in extracts the user who changed the label, along with the label details. It then uses TeamFoundationApplicationCore.Log to send the details to the Application event log.

Log Name: Application

Source: TFS Services

Date: 1/1/2011 12:00:00 AM

Event ID: 0

Task Category: None

Level: Information

Keywords: Classic

User: N/A

Computer: WIN-GS9GMUJITS8

Description:

Labels changed by WIN-GS9GMUJITS8\Administrator

Created: MyLabel@$/TestAgile

Application Domain: /LM/W3SVC/8080/ROOT/tfs-2-129366296712694768

Rather than writing to the event log, this plug-in could be easily modified to send an e-mail notification or call another web service with the label change details.

ITeamFoundationRequestFilter: Inspecting Requests

This interface is intended to allow the plug-in to inspect and manage all incoming requests. Team Foundation Server 2013 includes a number of built-in request filters. These filters perform tasks such as logging, collecting performance metrics, controlling the number of requests that can run concurrently, checking for disabled features, and checking the requests that are being made by compatible clients.

Note

For the ITeamFoundationRequestFilter Interface definition, see http://aka.ms/ITeamFoundationRequestFilter.

Because request filters don't rely on any particular events being implemented in the server, they are a way to inspect any and all server requests. Table 29.4 describes each of the methods in ITeamFoundationRequestFilter and when they are called in the request pipeline.

Table 29.4 ITeamFoundationRequestFilter Interface Methods

Method

Description

BeginRequest

BeginRequest is called after the server has determined which site or host the request is targeting and verified that it is processing requests. A call to BeginRequest is not guaranteed for all requests. An ITeamFoundationRequestFilter can throw a RequestFilterException in BeginRequest to cause the request to be completed early and an error message to be returned to the caller.

RequestReady

RequestReady is called once the request has completed authentication and is about to begin execution. At this point, the requestContext.UserContext property will contain the authenticated user information. An ITeamFoundationRequestFilter can throw a RequestFilterException in RequestReady to cause the request to be completed early and an error message to be returned to the caller.

EnterMethod

EnterMethod is called once the method being executed on this request is declared. At the time EnterMethod is called, the basic method information will be available. This includes method name, type, and the list of input parameters. This information will be available in requestContext.Method. An ITeamFoundationRequestFilter can throw a RequestFilterException in EnterMethod to cause the request to be completed early and an error message to be returned to the caller.

LeaveMethod

LeaveMethod is called once the method is complete. Once EnterMethod is called, LeaveMethod should always be called as well. Exceptions are ignored because the request is now complete.

EndRequest

EndRequest is called once the request is complete. All requests with a BeginRequest will have a matching EndRequest call. Exceptions are ignored because the request is now complete.

The following code sample shows a simple ITeamFoundationRequestFilter plug-in that only implements EnterMethod (code file: RequestFilter.cs):

using System;

using System.Diagnostics;

using System.Text;

using System.Text.RegularExpressions;

using Microsoft.TeamFoundation.Framework.Common;

using Microsoft.TeamFoundation.Framework.Server;

using Microsoft.TeamFoundation.Server.Core;

namespace RequestFilter

{

/// <summary>

/// This request filter will log an event to the Application event log

/// whenever TFS group memberships are changed

/// </summary>

public class RequestFilter : ITeamFoundationRequestFilter

{

public void EnterMethod(TeamFoundationRequestContext requestContext)

{

switch (requestContext.Method.Name)

{

case "AddMemberToApplicationGroup":

case "RemoveMemberFromApplicationGroup":

try

{

StringBuilder sb = new StringBuilder();

sb.AppendLine(string.Format("TFS group memberships have

been changed by {0}",

requestContext.AuthenticatedUserName));

sb.AppendLine(string.Format("{0}",

requestContext.Method.Name));

Regex regex = new Regex(@"^IdentityDescriptor

\(IdentityType: (?<IdentityType>[\w\.]+);

Identifier: (?<Identifier>[S\d\-]+)\)?",

RegexOptions.Compiled);

foreach (string parameterKey in

requestContext.Method.Parameters.AllKeys)

{

string parameterValue =

requestContext.Method.Parameters

[parameterKey];

if (regex.IsMatch(parameterValue))

{

// If the parameter is an identity descriptor,

// resolve the SID to a display name using IMS

string identityType =

regex.Match(parameterValue).Groups

["IdentityType"].Value;

string identifier =

regex.Match(parameterValue).Groups

["Identifier"].Value;

IdentityDescriptor identityDescriptor = new

IdentityDescriptor(identityType,

identifier);

TeamFoundationIdentityService ims =

requestContext.GetService

<TeamFoundationIdentityService>();

TeamFoundationIdentity identity =

ims.ReadIdentity(requestContext,

identityDescriptor, MembershipQuery.None,

ReadIdentityOptions.None);

sb.AppendLine(string.Format("{0}: {1}",

parameterKey, identity.DisplayName));

}

else

{

// Log other parameters, if any

sb.AppendLine(string.Format("{0}: {1}",

parameterKey, parameterValue));

}

}

TeamFoundationApplicationCore.Log(sb.ToString(), 0,

EventLogEntryType.Information);

}

catch (Exception exception)

{

// Our plugin cannot throw any exception or it will get

// disabled by TFS. Log it and eat it.

TeamFoundationApplicationCore.LogException("DecisionPoint

plugin encountered the following error while

processing events", exception);

}

break;

}

}

public void BeginRequest(TeamFoundationRequestContext requestContext)

{

}

public void EndRequest(TeamFoundationRequestContext requestContext)

{

}

public void LeaveMethod(TeamFoundationRequestContext requestContext)

{

}

public void RequestReady(TeamFoundationRequestContext requestContext)

{

}

}

}

In this example, if the method name matches AddMemberToApplicationGroup or RemoveMemberFromApplicationGroup, the plug-in logs a message. It uses the IdentityManagementService from the server object model to resolve the SID from the method parameters to a display name. It then logs a message to the Application event log, indicating that the application group memberships have been modified.

Job Extensions

All jobs in Team Foundation Server are plug-ins that implement the ITeamFoundationJobExtension interface. This is an example of the product using its own extensibility interfaces.

Note

For the ITeamFoundationJobExtension interface definition, see http://aka.ms/ITeamFoundationJobExtension.

In addition to how they are invoked, the main difference between ISubscriber plug-ins and job extensions is how they're deployed:

· Jobs are deployed by copying the assembly into the \Application Tier\TFSJobAgent\PlugIns directory, rather than the Web Services\bin\PlugIns directory.

· If a job plug-in is already loaded, and the job agent is currently running, you must stop the job agent before you can replace the plug-in assembly. This releases the file handle.

· Along with having the assembly deployed, a job must be either scheduled to run or manually queued using the job service API.

The following code sample shows a simple ITeamFoundationJobExtension plug-in that implements the Run method (code file: JobExtension.cs):

using System;

using System.Diagnostics;

using Microsoft.TeamFoundation.Framework.Server;

using Microsoft.TeamFoundation.VersionControl.Server;

namespace JobExtension

{

/// <summary>

/// This job will log an event to the Application event log with the

/// current number of workspaces that have not been accessed in 30 days

/// </summary>

public class JobExtension : ITeamFoundationJobExtension

{

public TeamFoundationJobExecutionResult Run(

TeamFoundationRequestContext requestContext,

TeamFoundationJobDefinition jobDefinition, DateTime queueTime,

out string resultMessage)

{

resultMessage = null;

int warningDays = 30;

int oldWorkspaces = 0;

try

{

// Get all workspaces in the collection

foreach (Workspace workspace in

requestContext.GetService

<TeamFoundationVersionControlService>

().QueryWorkspaces(requestContext, null, null, 0))

{

if (workspace.LastAccessDate <= DateTime.Now.Subtract(new

TimeSpan(warningDays, 0, 0, 0)))

{

oldWorkspaces++;

}

}

TeamFoundationApplicationCore.Log(string.Format("There are {0}

workspaces that have not been accessed in the last {1}

days", oldWorkspaces, warningDays), 0,

EventLogEntryType.Information);

return TeamFoundationJobExecutionResult.Succeeded;

}

catch (RequestCanceledException)

{

resultMessage = null;

return TeamFoundationJobExecutionResult.Stopped;

}

catch (Exception exception)

{

resultMessage = exception.ToString();

return TeamFoundationJobExecutionResult.Failed;

}

}

}

}

When this job is executed, it uses TeamFoundationVersionControlService from the server object model to enumerate all workspaces in the collection. It then checks whether the workspace has been accessed in the past 30 days. If it has not been accessed, a counter is incremented.

Finally, the job logs a message to the Application event log, indicating how many workspaces are stale and have not been accessed in the past month.

Job Deployment

The most difficult part of custom job extensions is deployment. It's easy enough to define a single job for a single collection. However, if you want to have your job defined and scheduled for all new collections, there's not an easy way to do that.

Note

The built-in jobs are scheduled using servicing steps that are part of the built-in project collection creation scripts. Unfortunately, these servicing steps are replaced with each patch of Team Foundation Server. So, if you customized them, they would get overwritten the next time you upgraded. A creative solution would be to build an ISubscriber Notification plug-in that subscribes to the HostReadyEvent. Each time the server starts, it could check if the job is scheduled for all project collections, and schedule it if it isn't.

The following PowerShell script demonstrates how to use ITeamFoundationJobService from the client object model to define and schedule a job for a single collection (code file: Install-JobExtension.ps1):

# Install-JobExtension.ps1

# Usage: Install-JobExtension <TfsCollectionUri>

# Example: Install-JobExtension http://yourserver:8080/tfs/yourcollection

#

# Before running this, you will need to copy the assembly containing the

# class that implements ITeamFoundationJobExtension to the following directory

# on all Application Tier servers:

# C:\Program Files\Microsoft Team Foundation Server 12.0\Application

# Tier\TFSJobAgent\plugins

#

# This will use the TFS client object model to register and schedule the job

# By default the job will be scheduled to run at 5 AM daily.

# Halt on errors

$ErrorActionPreference = "Stop"

$Uri = $args[0]

if ([String]::IsNullOrEmpty($Uri))

{

$Uri = "http://localhost:8080/tfs"

}

# Define your own well-known GUIDs for your job

$JobDefinitionGuid = New-Object System.Guid -ArgumentList

"E2B88C7A-7745-4E49-9442-5A6851190242"

Add-Type -LiteralPath "C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\

IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.Common.dll"

Add-Type -LiteralPath "C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\

IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.Client.dll"

# Get the job service for the collection.

$Tpc = New-Object Microsoft.TeamFoundation.Client.TfsTeamProjectCollection

-ArgumentList $Uri

$JobService = $Tpc.GetService([Microsoft.TeamFoundation.

Framework.Client.ITeamFoundationJobService])

# Define the job agent job

$JobDefinition = New-Object Microsoft.TeamFoundation.

Framework.Client.TeamFoundationJobDefinition ′

-ArgumentList $JobDefinitionGuid, ′

"Sample Job Extension", ′

"JobExtension.JobExtension", ′

$null

# Schedule the job to run at 5 AM every day, starting tomorrow.

$TomorrowFiveAM = [DateTime]::Today.AddDays(1).AddHours(5).ToUniversalTime()

$JobSchedule = New-Object Microsoft.TeamFoundation.

Framework.Client.TeamFoundationJobSchedule ′

-ArgumentList $TomorrowFiveAM, 86400

$JobDefinition.Schedule.Add($JobSchedule)

# Save the job definition to the collection's job service.

$JobService.UpdateJob($JobDefinition)

This script connects to the specified collection or the default collection if none is specified. It then creates a TeamFoundationJobDefinition object for the job extension and then creates a TeamFoundationJobSchedule for the job that schedules it to be run every 24 hours (86400 seconds) starting at 5 a.m. tomorrow, UTC time.

Once the job and its schedule are defined, it is saved to the server using ITeamFoundation JobService.UpdateJob().

To invoke the job manually, and to see if your job extension works, you can run the following PowerShell command:

# Run the job now manually

$jobService.QueueJobNow($JobDefinition, $false)

Visual Studio Extensibility

You can extend Visual Studio by using macros, add-ins, VSPackages, and Managed Extensibility Framework (MEF) extensions, and you can deploy them in a number of ways, including VSIX and custom extension galleries.

By leveraging Visual Studio extensibility, it's also possible to automate and extend some of the Team Foundation Server windows, dialog boxes, and events. Figure 29.3 shows a sample add-in that interacts with Source Control Explorer. Be warned, however, that not all of the dialog boxes are extensible without getting deep into reflection, and if you do so, it is very likely that an update to Visual Studio may break your integration.

Note

For an example of extending Source Control Explorer, see http://tinyurl.com/TFSExtendingSCE. For an example of extending the work item tracking context menu, see http://tinyurl.com/TFSExtendingWITMenu. For general information on Visual Studio extensibility, see “Customizing, Automating, and Extending the Development Environment” at http://aka.ms/VSExtensions.

image

Figure 29.3 A sample Visual Studio add-in that extends the Source Control Explorer

One area of the Team Foundation Server UI that was designed for extensibility inside Visual Studio is the Team Explorer view and all the controls that it hosts. For example, it is possible to extend the pending changes page by adding new sections to it. Table 29.5describes the possible Team Explorer extensibility points.

Table 29.5 Team Explorer Extensibility

Extensibility Type

Description

Team Explorer Page

Team Explorer consists of multiple pages (for example, the Pending Changes view is a page). You can create your own pages or add sections to existing ones.

Team Explorer Section

A page is made up of sections.

Team Explorer Navigation Item

Team Explorer allows shortcuts to pages to appear in the drop-down menu at the top of Team Explorer. These are defined at Navigation Items.

Team Explorer Navigation Link

Sections can contain links that navigate to other pages in Team Explorer.

The Team Explorer control is a WPF based-control that makes use of extensibility points defined in the Microsoft.TeamFoundation.VersionControl.Extensibility namespace. The ALM Rangers released a great article on extending the Team Explorer view in Visual Studio 2012, which you can read at http://aka.ms/ExtendTE. The walkthrough is very comprehensive, but there have been some changes in the Team Foundation Server 2013 release. Tarun Arora, a Microsoft MVP, has an excellent blog post explaining how to upgrade a 2012 extension to work with Team Foundation Server 2013 at http://aka.ms/UpgradingVISXExtensions.

Other Resources

Team Foundation Server is very extensible, and, accordingly, a number of solutions and resources are available to assist you. These resources can have useful samples for getting started. The partners can also be engaged to build custom solutions for your needs.

Table 29.6 shows a breakdown of some available resources.

Table 29.6 Available Resources

Resource

Description

Notes

Visual Studio ALM Rangers

This delivers out-of-band solutions for missing features or for guidance. Periodically, they take nominations for new projects, vote for them, develop them, and then release them on CodePlex.

For more information, refer to the Visual Studio ALM Ranger page on MSDN at http://aka.ms/AlmRangers.

Visual Studio Industry Partners (VSIP)

This provides technical resources, business insight, and extensive co-marketing to partners who sell products that integrate with and extend Visual Studio.

To learn about existing VSIP partners, see http://aka.ms/vsip.

Microsoft Partner Program: ALM Competency

The Application Lifecycle Management (ALM) Competency in the Microsoft Partner Program enables partners to demonstrate their expertise in providing training and consultation for, or deploying, Microsoft Visual Studio tools.

To find partners who have achieved the ALM Competency, see Microsoft Pinpoint at http://aka.ms/AlmPartners.

CodePlex

This is Microsoft's Open Source project hosting website. You can download and use many projects on CodePlex that extend Team Foundation Server. You can also use CodePlex to share your own extensions with the world.

For Open Source projects that extend Team Foundation Server, seehttp://www.codeplex.com/site/search?query=tfs.

Visual Studio Gallery

This provides quick access to tools, controls, and templates to help you get the most out of Visual Studio.

For a list of Visual Studio tools and extensions, seehttp://tinyurl.com/VSGalleryTFS.

MSDN Code Gallery

This is a site where you may download and share applications, code snippets, and other resources with the developer community.

For a list of code snippets, see http://tinyurl.com/MSDNGalleryTFS.

Summary

This chapter began with an overview of the high-level architecture of the extensibility available within Team Foundation Server. You learned how to get started with the client object model as well as some useful tips for working with it. SOAP event subscriptions were then discussed, along with the available event types that can be subscribed to.

The server object model was examined, and examples of server plug-ins were provided. This included plug-ins that send real-time notifications and plug-ins that can change the flow of a command. Finally, other resources were discussed that can help you leverage the extensibility available in Team Foundation Server.