Overwriting TFS Web Services

In this blog I will share a technique I used to intercept TFS Web Services calls.

This technique is a very invasive one and requires you to overwrite default TFS Web Services behavior. I only recommend taking such an approach when other means of TFS extensibility fail to provide the same functionality (this is not a supported TFS extensibility point).

For instance, intercepting and aborting a Work Item change operation could be implemented using this approach (consider TFS Subscribers functionality before taking this approach, check Martin’s post about subscribers).

So let’s get started.

The technique consists in versioning TFS Web Services .asmx service classes. If you look into TFS’s ASMX services you will notice that versioning is supported by creating a class hierarchy between different product versions.

For instance, let’s take the Work Item management service .asmx. Check the following .asmx file located at:

%Program Files%\Microsoft Team Foundation Server 2010\Application Tier\Web Services\_tfs_resources\WorkItemTracking\v3.0\ClientService.asmx

The .asmx references the class Microsoft.TeamFoundation.WorkItemTracking.Server.ClientService3:

<%-- Copyright (c) Microsoft Corporation.  All rights reserved. --%> 
<%@ webservice language="C#" 
Class="Microsoft.TeamFoundation.WorkItemTracking.Server.ClientService3" %>

The inheritance hierarchy for this service class follows:

image

Note the naming convention used for service versioning (ClientService3, ClientService2, ClientService).

We will need to overwrite the latest service version provided by the product (in this case ClientService3 for TFS 2010).

The following example intercepts and analyzes WorkItem fields. Suppose we need to validate state changes with more advanced logic other than the provided validations/constraints of the process template.

Important: Backup the original .asmx file and create one of your own.

  1. Create a Visual Studio Web App Project and include a new ASMX Web Service in the project
  2. Add the following references to the project (check the folder %Program Files%\Microsoft Team Foundation Server 2010\Application Tier\Web Services\bin\):
    • Microsoft.TeamFoundation.Framework.Server.dll
    • Microsoft.TeamFoundation.Server.dll Microsoft.TeamFoundation.Server.dll
    • Microsoft.TeamFoundation.WorkItemTracking.Client.QueryLanguage.dll
    • Microsoft.TeamFoundation.WorkItemTracking.Server.DataAccessLayer.dll
    • Microsoft.TeamFoundation.WorkItemTracking.Server.DataServices.dll
  3. Replace the default service implementation with the something similar to the following code:
Code Snippet
/// <summary>
/// Inherit from ClientService3 to overwrite default Implementation
/// </summary>
[WebService(Namespace = "http://schemas.microsoft.com/TeamFoundation/2005/06/WorkItemTracking/ClientServices/03", Description = "Custom Team Foundation WorkItemTracking ClientService Web Service")]
public class CustomTfsClientService : ClientService3
{
    [WebMethod, SoapHeader("requestHeader", Direction = SoapHeaderDirection.In)]
    public override bool BulkUpdate(
        XmlElement package,
        out XmlElement result,
        MetadataTableHaveEntry[] metadataHave,
        out string dbStamp,
        out Payload metadata)
    {
        var xe = XElement.Parse(package.OuterXml);

        // We only intercept WorkItems Updates (we can easily extend this sample to capture any operation).
        var wit = xe.Element("UpdateWorkItem");
        if (wit != null)
        {
            if (wit.Attribute("WorkItemID") != null)
            {
                int witId = (int)wit.Attribute("WorkItemID");
                // With this Id. I can query TFS for more detailed information, using TFS Client API (assuming the WIT already exists).
                var stateChanged =
                    wit.Element("Columns").Elements("Column").FirstOrDefault(c => (string)c.Attribute("Column") == "System.State");

                if (stateChanged != null)
                {
                    var newStateName = stateChanged.Element("Value").Value;
                    if (newStateName == "Resolved")
                    {
                        throw new Exception("Cannot change state to Resolved!");
                    }
                }
            }
        }

        // Finally, we call base method implementation
        return base.BulkUpdate(package, out result, metadataHave, out dbStamp, out metadata);
    }
}

4. Build your solution and overwrite the original .asmx with the new implementation referencing our new service version (don’t forget to backup it up first).

5. Copy your project’s .dll into the following path:

%Program Files%\Microsoft Team Foundation Server 2010\Application Tier\Web Services\bin

6. Try saving a WorkItem into the Resolved state.

Enjoy!

No Comments