Programmatically use a server as the Build Server for multiple Project Collections

by Ewald Hofman 25. November 2010 05:41

Important: With this post you create an unsupported scenario by Microsoft. It will break your support for this server with Microsoft. So handle with care.

I am the administrator an a TFS environment with a lot of Project Collections. In the supported configuration of Microsoft 2010 you need one Build Controller per Project Collection, and it is not supported to have multiple Build Controllers installed. Jim Lamb created a post how you can modify your system to change this behaviour. But since I have so many Project Collections, I automated this with the API of TFS.

When you install a new build server via the UI, you do the following steps

  1. Register the build service (with this you hook the windows server into the build server environment)
  2. Add a new build controller
  3. Add a new build agent

So in pseudo code, the code would look like

foreach (projectCollection in GetAllProjectCollections)
{
      CreateNewWindowsService();
      RegisterService();
      AddNewController();
      AddNewAgent();
}

The following code fragements show you the most important parts of the method implementations. Attached is the full project.

CreateNewWindowsService

We create a new windows service with the SC command via the Diagnostics.Process class:

            var pi = new ProcessStartInfo("sc.exe")
                         {
                             Arguments =
                                
string
.Format(
                                    
"create \"{0}\" start= auto binpath= \"C:\\Program Files\\Microsoft Team Foundation Server 2010\\Tools\\TfsBuildServiceHost.exe
             /NamedInstance:{0}\" DisplayName= \"Visual Studio Team Foundation Build Service Host ({1})\""
,
                                     serviceHostName, tpcName)
                         };
           
Process
.Start(pi);

            pi.Arguments =
string.Format("failure {0} reset= 86400 actions= restart/60000"
, serviceHostName);
           
Process.Start(pi);

RegisterService

The trick in this method is that we set the NamedInstance static property. This property is Internal, so we need to set it through reflection. To get information on these you need nice Microsoft friends and the .Net reflectorSmile .

            // Indicate which build service host instance we are using
            typeof(BuildServiceHostUtilities).Assembly.GetType("Microsoft.TeamFoundation.Build.Config.BuildServiceHostProcess").InvokeMember("NamedInstance",
             System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.SetProperty | System.Reflection.BindingFlags.Static, null, null, new object[] { serviceName });

           
// Create the build service host
            serviceHost = buildServer.CreateBuildServiceHost(serviceName, endPoint);
            serviceHost.Save();

           
// Register the build service host
            BuildServiceHostUtilities
.Register(serviceHost, user, password);

AddNewController and AddNewAgent

Once you have the BuildServerHost, the rest is pretty straightforward. There are methods on the BuildServerHost to modify the controllers and the agents

                controller = serviceHost.CreateBuildController(controllerName);
                agent = controller.ServiceHost.CreateBuildAgent(agentName, buildDirectory, controller);
                controller.AddBuildAgent(agent);
You have now seen the highlights of the application. If you need it and want to have sample information when you work in this area, download the app
TFS2010_RegisterBuildServerToTPCs

Tags:

TFS SDK | VSTS 2010 | Team Build

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

by Ewald Hofman 2. August 2010 06:43

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

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

This article assumes that you have basic knowledge of WCF.

The steps that are involved into this exercise are:

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

Create a new Visual Studio WCF Service Application

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

Create the interface for the rollup Service

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

using System.ServiceModel;

namespace EventService
{

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

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

    }

}

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

namespace EventService
{
    public class RollupService : IRollupService
    {

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

    }
}

 

Specify the web.config

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

Here is the list of issues I had to solve:

Problem HTTP code 415: Cannot process the message because the content type 'application/soap+xml; charset=utf-8' was not the expected type 'text/xml; charset=utf-8'
Issue Because TFS 2010 is now using SOAP 1.2, you need to use wsHttpBinding.
Solution Change the basicHttpBinding to wsHttpBinding

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

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

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

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

</configuration>

Publish the web service to IIS

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

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

image 

Subscribe the WCF servcie to the TFS WorkItemChanged event

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

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

Write logic in the service to retrieve the changed work item

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

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

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

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

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

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

        public enum FieldType
        {
            IntegerField,
            StringField
        }

        public enum ValueType
        {
            NewValue,
            OldValue
        }

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

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

            var node = doc.SelectSingleNode(path);

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

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

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

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

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

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

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

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

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

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

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

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

        }

    }

Update the hours of the parent work item

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

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

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

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

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

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

                parentWorkItem.Save();
            }
        }

Test and debug the service

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

Tips for debugging:

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

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

Tags:

TFS SDK | VSTS 2010 | Work items

Time travelling with Work Item Queries In TFS 2010

by Ewald Hofman 20. April 2010 05:28

Someone asked me if I could help him because he made a mess of the work items. He published old data from Excel to TFS, so all the updates of his team mates where gone with one click.

There is a nice and very under-appreciated feature in the work item queries to get the values of the work items at a moment in history as if you created a snapshot at that time. It is pretty easy to accomplish this with the ASOF feature. The following steps show you how you can use the ASOF statement.

  1. Create a new query with the fields and filters you are interested in via the default “Add query” command
  2. Save the query (File –> Save New Query 1 [Query])
  3. You get now the ability to store the query on the file system 

    TimeTravelling002
  4. Select an appropriate location and click Save
  5. Open the file you just saved in Notepad. You will now see the Work Item Query Language (WIQL) that is used to define the query. For more information on the syntax, please refer to http://msdn.microsoft.com/en-us/library/bb130198.aspx
    This WIQL could look like:

    <?xml version="1.0" encoding="utf-8"?><WorkItemQuery Version="1"><TeamFoundationServer>http://myserver:8080/tfs/defaultcollection</TeamFoundationServer><TeamProject>Agile</TeamProject><Wiql>SELECT [System.Id], [System.Title] FROM WorkItems WHERE [System.AssignedTo] = 'Ewald Hofman' ORDER BY [System.Id]</Wiql></WorkItemQuery>
  6. You can only execute a modified WIQL with a .NET app. So lets create a new C# ConsoleApplication project
  7. Add the references to
    1. Microsoft.TeamFoundation.Client
    2. Microsoft.TeamFoundation.Common
    3. Microsoft.TeamFoundation.WorkItemTracking.Client
    4. System.Windows.Forms

      you can find the TeamFoundation assemblies in %Program Files%\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0
  8. Add the following using statements to your code

    using Microsoft.TeamFoundation.Client;
    using Microsoft.TeamFoundation.WorkItemTracking.Client;
    using System.Net;
    using System.Windows.Forms;
  9. Now add the following code to the class Program. You will have to modify the tfsServer and queryText variables to your own values. Notice at the end of the queryText the ASOF syntax. With this statement you are time travelling.
    [STAThread()]
    static void Main(string[] args)
    {
    
    
        StringBuilder clipboardText = new StringBuilder();
        bool titlesPrinted = false;
    
    
        string tfsServer = @"http://myserver:8080/tfs/defaultcollection";
        string queryText = @"SELECT [System.Id], [System.Title], [System.State] FROM WorkItems 
                                WHERE [System.AssignedTo] = 'Ewald Hofman' ORDER BY [System.Id] ASOF '4/10/2010'";
    
        // Open the connection to TFS
        using (var tfs = new TfsTeamProjectCollection(new Uri(tfsServer), CredentialCache.DefaultCredentials))
        {
    
            // Get the work item service
            var store = (WorkItemStore)tfs.GetService(typeof(WorkItemStore));
    
            // Execute the query
            var wiCollection = store.Query(queryText);
    
            // Iterate through all work items
            foreach (WorkItem wi in wiCollection)
            {
                // Add the column headers
                if (!titlesPrinted)
                {
                    titlesPrinted = true;
    
                    foreach (FieldDefinition field in wiCollection.DisplayFields)
                    {
                        clipboardText.Append(field.Name);
                        clipboardText.Append("\t");
                    }
                    clipboardText.AppendLine();
                }
    
                // Add the work item values
                foreach (FieldDefinition field in wiCollection.DisplayFields)
                {
                    clipboardText.Append(wi.Fields[field.Name].Value);
                    clipboardText.Append("\t");
                }
                clipboardText.AppendLine();
            }
        }
    
        // Put the complete text to the clipboard, so it can be copied to Excel
        if (clipboardText.Length > 0)
            SetClipboard(clipboardText.ToString());
    
    
    
    }
    
    /// <summary>
    /// Try a few times to put it on the clipboard: known issue...
    /// </summary>
    public static void SetClipboard(object data)
    {
        for (int i = 0; i < 10; i++)
        {
            try
            {
                Clipboard.SetDataObject(data);
                return;
            }
            catch { }
            System.Threading.Thread.Sleep(100);
        }
    }
  10. You can now run the code, open Excel and start paste command.

Tags:

TFS SDK | VSTS 2010 | Work items

Time travelling with Work Item Queries In TFS 2008

by Ewald Hofman 20. April 2010 04:51

Someone asked me if I could help him because he made a mess of the work items. He published old data from Excel to TFS, so all the updates of his team mates where gone with one click.

There is a nice and very under-appreciated feature in the work item queries to get the values of the work items at a moment in history as if you created a snapshot at that time. It is pretty easy to accomplish this with the ASOF feature. The following steps show you how you can use the ASOF statement.

  1. Create a new query with the fields and filters you are interested in via the default “Add query” command
  2. Save the query (File –> Save New Query 1 [Query])
  3. You get now the ability to store the query on the file system

    TimeTravelling001
  4. Select an appropriate location and click Save
  5. Open the file you just saved in Notepad. You will now see the Work Item Query Language (WIQL) that is used to define the query. For more information on the syntax, please refer to http://msdn.microsoft.com/en-us/library/bb130198.aspx
    This WIQL could look like:

    <?xml version="1.0" encoding="utf-8"?><WorkItemQuery Version="1"><TeamFoundationServer>http://myserver:8080/</TeamFoundationServer><TeamProject>TestTeam</TeamProject><Wiql>SELECT [System.Id], [System.Title] FROM WorkItems WHERE [System.AssignedTo] = 'Ewald Hofman' ORDER BY [System.Id]</Wiql></WorkItemQuery>
  6. You can only execute a modified WIQL with a .NET app. So lets create a new C# ConsoleApplication project
  7. Add the references to
    1. Microsoft.TeamFoundation.Client
    2. Microsoft.TeamFoundation.Common
    3. Microsoft.TeamFoundation.WorkItemTracking.Client
    4. System.Windows.Forms

      you can find the TeamFoundation assemblies in %Program Files%\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies
  8. Add the following using statements to your code

    using Microsoft.TeamFoundation.Client;
    using Microsoft.TeamFoundation.WorkItemTracking.Client;
    using System.Net;
    using System.Windows.Forms;
  9. Now add the following code to the class Program. You will have to modify the tfsServer and queryText variables to your own values. Notice at the end of the queryText the ASOF syntax. With this statement you are time travelling.
    [STAThread()]
    static void Main(string[] args)
    {
    
    
        StringBuilder clipboardText = new StringBuilder();
        bool titlesPrinted = false;
    
    
        string tfsServer = @"http://myserver:8080";
        string queryText = @"SELECT [System.Id], [System.Title], [System.State] FROM WorkItems 
    WHERE [System.AssignedTo] = 'Ewald Hofman' ORDER BY [System.Id] ASOF '4/10/2010'"
    ; // Open the connection to TFS using (var tfs = new TeamFoundationServer(tfsServer, CredentialCache.DefaultCredentials)) { // Get the work item service var store = (WorkItemStore)tfs.GetService(typeof(WorkItemStore)); // Execute the query var wiCollection = store.Query(queryText); // Iterate through all work items foreach (WorkItem wi in wiCollection) { // Add the column headers if (!titlesPrinted) { titlesPrinted = true; foreach (FieldDefinition field in wiCollection.DisplayFields) { clipboardText.Append(field.Name); clipboardText.Append("\t"); } clipboardText.AppendLine(); } // Add the work item values foreach (FieldDefinition field in wiCollection.DisplayFields) { clipboardText.Append(wi.Fields[field.Name].Value); clipboardText.Append("\t"); } clipboardText.AppendLine(); } } // Put the complete text to the clipboard, so it can be copied to Excel if (clipboardText.Length > 0) Clipboard.SetText(clipboardText.ToString(), TextDataFormat.Text); }
  10. You can now run the code, open Excel and start paste command.

Tags:

TFS SDK | VSTS 2008 | Work items

TFS SDK 2010 – Part 6 – Replace text in all Work Item Query Definitions

by Ewald Hofman 9. March 2010 04:20

Updates

  • 19-3-2010: Added the possibility to change the queries in a specific folder

When you work iterative, you might just want to replace your current release or iteration in the query definitions, instead of creating a bunch of new queries. This post shows you how you can perform a quick and simple replace mechanism to achieve this.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;

using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Framework.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.TestManagement.Client;
using System.Diagnostics;

namespace TFS_SDK
{
    class Program
    {
        static void Main(string[] args)
        {
            // The url to the tfs server
            Uri tfsUri = new Uri("http://localhost:8080/tfs/");
            const string projectName = "Agile";
            const string startFolder = null; //@"Team Queries\Release 1\Sprint 2";
            const string searchFor = "Iteration 1";
            const string replaceWith = "Iteration 2";

            // Load the tfs instance
            TfsTeamProjectCollection tpc = new TfsTeamProjectCollection(tfsUri, new UICredentialsProvider());

            // Log in to TFS
            tpc.EnsureAuthenticated();

            var wiStore = tpc.GetService<WorkItemStore>();
            var project = wiStore.Projects[projectName];

            foreach (QueryDefinition queryDefinition in GetAllTeamQueries(project, startFolder))
            {
                if (queryDefinition.QueryText.Contains(searchFor))
                {
                    queryDefinition.QueryText = queryDefinition.QueryText.Replace(searchFor, replaceWith);
                }
            }

            project.QueryHierarchy.Save();
        }

        private static List<QueryDefinition> GetAllTeamQueries(Project project, string startFolder)
        {
            var ret = new List<QueryDefinition>();

            var startQueryFolder = GetQueryFolder(project.QueryHierarchy,
                                                  startFolder == null ? (new string[] { }) : startFolder.Split('\\'));

            ret.AddRange(GetAllTeamQueries(startQueryFolder));

            return ret;
        }

        private static List<QueryDefinition> GetAllTeamQueries(QueryFolder queryFolder)
        {
            var ret = new List<QueryDefinition>();

            foreach (QueryItem queryItem in queryFolder)
            {
                if (queryItem is QueryFolder)
                {
                    ret.AddRange(GetAllTeamQueries(queryItem as QueryFolder));
                }
                else
                {
                    ret.Add(queryItem as QueryDefinition);
                }
            }

            return ret;
        }

        private static QueryFolder GetQueryFolder(QueryFolder queryFolder, string[] folders)
        {
            return folders.Length == 0 ? queryFolder : GetQueryFolder((QueryFolder)queryFolder[folders[0]], folders.Skip(1).ToArray());
        }
    }
}

Tags:

VSTS 2010 | TFS SDK

TFS SDK 2010 – Part 5 – Create a new Test Case work item

by Ewald Hofman 11. December 2009 06:26

In Part 1 I have described how to access the Application Instance of the TFS server. This post describes how you can create a new test case based on the TestManagement capabilities. In later posts I will show what you can do more with the test management.

In order to get access to the work items, add a reference to:

  • Microsoft.TeamFoundation.TestManagement.Client
  • WindowBase (which you can find on the .Net tab in the Add Reference Dialog)

You can find the dll’s in C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0

Then you have to add the following using statements:

using Microsoft.TeamFoundation.TestManagement.Client;

You can now create the work items with the following code:

// Get the work item store
ITestManagementService tms = tfai.GetTeamFoundationServer(tpc.Id).GetService<ITestManagementService>();

// Get the team project
var project = tms.GetTeamProject("Agile");

var testCase = project.TestCases.Create();
testCase.Title = "Browse my blog";

var navigateToSiteStep = testCase.CreateTestStep();
navigateToSiteStep.Title = "Navigate to \"http://www.ewaldhofman.nl\"";
testCase.Actions.Add(navigateToSiteStep);

var clickOnFirstPostStep = testCase.CreateTestStep();
clickOnFirstPostStep.Title = "Click on the first post in the summary";
clickOnFirstPostStep.ExpectedResult = "The details of the post are visible";
testCase.Actions.Add(clickOnFirstPostStep);

testCase.Save();

The result is a new test case with two steps in it:

image

Tags:

TFS SDK | VSTS 2010

TFS SDK 2010 – Part 2 - Get a work item

by Ewald Hofman 11. December 2009 04:23

In Part 1 I have described how to access the Application Instance of the TFS server. This post describes how you can create access to your work items:

In order to get access to the work items, add a reference to:

  • Microsoft.TeamFoundation.WorkItemTracking.Client.dll

You can find the dll’s in C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0

Then you have to add the following using statements:

using Microsoft.TeamFoundation.WorkItemTracking.Client;

You can now enumerate the values of a work item with the following code:

// Get the work item store
WorkItemStore wiStore = tfai.GetTeamFoundationServer(tpc.Id).GetService<WorkItemStore>();

// Get the work item with the Id 1
WorkItem wi = wiStore.GetWorkItem(1);

// Write the name and value for all fields in the work item
foreach (Field fd in wi.Fields)
{
    Console.WriteLine("{0}: {1}", fd.Name, fd.Value);
}

All code examples are based on Beta 2 and are subject to change.

Tags:

TFS SDK | VSTS 2010

TFS SDK 2010 – Part 4 - Create a new User Story, with the implementation (Tasks) and test scenarios (Test Case)

by Ewald Hofman 10. December 2009 08:31

In Part 1 I have described how to access the Application Instance of the TFS server. This post describes how you can create new work items, including a new linked work item. The new link makes use of the new link type in TFS 2010, so it shows up on the correct tab page. In this example a new User Story is created with one Task as its implementation and one Test Case which tests the User Story:

In order to get access to the work items, add a reference to:

  • Microsoft.TeamFoundation.WorkItemTracking.Client.dll

You can find the dll’s in C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0

Then you have to add the following using statements:

using Microsoft.TeamFoundation.WorkItemTracking.Client;

You can now create the work items with the following code:

// Get the work item store
WorkItemStore wiStore = tfai.GetTeamFoundationServer(tpc.Id).GetService<WorkItemStore>();

// Get the team project
var project = wiStore.Projects["Agile"];

// Create a new User Story
var wiUserStory = new WorkItem(project.WorkItemTypes["User Story"])
{
    Title = string.Format("New User Story, created at {0:g}", DateTime.Now),
    Description = "This is an example how you create a new work item with the SDK"
};
wiUserStory.Save();

// Create a new Task
var wiTask = new WorkItem(project.WorkItemTypes["Task"])
{
    Title = "Create a very secure design"
};
wiTask.Save();

// Add a parent-child link between User Story and Task
var hierarchicalLink = wiStore.WorkItemLinkTypes["System.LinkTypes.Hierarchy"];
wiUserStory.WorkItemLinks.Add(new WorkItemLink(hierarchicalLink.ForwardEnd, wiTask.Id));
wiUserStory.Save();

// Create a new Test Case
var wiTestCase = new WorkItem(project.WorkItemTypes["Test Case"])
{
    Title = "Test on security",
};
wiTestCase.Fields["Steps"].Value = "<steps id=\"0\" last=\"2\"><step id=\"1\" type=\"ActionStep\"><parameterizedString>" + 
                                   "<text>Go to the correct url</text></parameterizedString><parameterizedString /><description />" + 
                                   "</step><step id=\"2\" type=\"ActionStep\"><parameterizedString><text>Hack the site</text>" + 
                                   "</parameterizedString><parameterizedString /><description /></step></steps>";
wiTestCase.Save();

// Add a tested by link between User Story and Test Case
var testedByLink = wiStore.WorkItemLinkTypes["Microsoft.VSTS.Common.TestedBy"];
wiUserStory.WorkItemLinks.Add(new WorkItemLink(testedByLink.ForwardEnd, wiTestCase.Id));
wiUserStory.Save();

Tags:

TFS SDK | VSTS 2010

TFS SDK 2010 – Part 3 - Enumerate the build controllers and agents

by Ewald Hofman 6. December 2009 05:48

In Part 1 I have described how to access the Application Instance of the TFS server. This post describes how you can create access to your build controller and agents:

In order to get access to the work items, add a reference to:

  • Microsoft.TeamFoundation.WorkItemTracking.Build.dll

You can find the dll’s in C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0

Then you have to add the following using statements:

using Microsoft.TeamFoundation.Build.Client;

You can now enumerate the values of a work item with the following code:

// Get the build server
IBuildServer buildServer = tfai.GetTeamFoundationServer(tpc.Id).GetService<IBuildServer>();

// Enumerate the controllers
foreach (IBuildController controller in buildServer.QueryBuildControllers(true))
{
    Console.WriteLine("{0}", controller.Name);

    // Enumerate the agents in the controller
    foreach (IBuildAgent agent in controller.Agents)
    {
        Console.WriteLine("\t{0}", agent.Name);
    }
}

All code examples are based on Beta 2 and are subject to change.

Tags:

VSTS 2010 | TFS SDK

TFS SDK 2010 – Part 1 – Get the Team Project Collection

by Ewald Hofman 3. December 2009 11:28

This is the first post in a series that describes how you can work with the TFS Object Model, API or SDK. In this first part we will open a connection to the team project collection that opens the ability to access all artifacts of TFS.

Create a new project and add the following references to your project:

  • Microsoft.TeamFoundation.Client.dll
  • Microsoft.TeamFoundation.Common.dll

You can find the dll’s in C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0

Then you have to add the following using statements:

using Microsoft.TeamFoundation.Client; 
using Microsoft.TeamFoundation.Framework.Client;

You can now open the connection with the following code:

// The url to the tfs server 
Uri tfsUri = new Uri("http://localhost:8080/tfs/");

// Load the tfs instance 
TeamFoundationApplicationInstance tfai = new TeamFoundationApplicationInstance(tfsUri, new UICredentialsProvider()); 

// Log in to TFS 
tfai.EnsureAuthenticated(); 

// Get the default project collection 
TeamProjectCollection tpc = tfai.GetService<ITeamProjectCollectionService>().GetDefaultCollection();

The following posts will describe how to access the rest of the artifacts (work items, version control and team build) in the TFS SDK.

All code examples are based on Beta 2 and are subject to change.

Tags:

VSTS 2010 | TFS SDK

Powered by BlogEngine.NET 1.6.1.0
Theme by Mads Kristensen


ClusterMap

Statistics

Statistics created at 09 Sep 2009

121 posts
493 comments
329 raters
1959242 visit (1042 per day)
15 users online

Recent comments

Comment RSS