Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / DevOps / TFS

TFS API Cloning Work Items with Children

5.00/5 (2 votes)
18 Aug 2016CPOL5 min read 16.8K  
Clone a TFS Work Item and Its Children with links

Introduction

Having to create the same stories with the same tasks over and over can be a burden. In our scenario, we create a story for Release Regression Testing and attach several dozen testing tasks, which is very time consuming. This system is setup to copy children that are work items, and attached test cases.

Warning

I cannot be responsible for your setup or your code. The system outlined below works on our current system without issue, but modifying the code or mistakes may cause issues to your TFS system. Use at your own risk as you are any time you work directly against the API. I created the tested system in a Development Environment" clone of our normal TFS system to be safe.

About the Setup

I am not going to worry about the UI and let you pick your own so we will just make the framework, then you can connect to the system however you like, but I will give some pointers. You may want to keep the wrapper in a separate project from the UI so it will be reusable. Doing this allowed me to make a Windows service that automates some tasks like emailing daily test results to the department.

Requirements

You will have to add references to the TFS DLLs. For this, I am using the DLLs from Visual Studio 12. Most of the DLLs can be found in C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\ReferenceAssemblies.

Since we are doing test first you will also need the Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll at C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\PublicAssemblies.

Getting Started

Setting Up A Data Transfer Object

First, create a class called "ClonedWorkItemResult" that will hold the data for our results. It will consist of two properties of the type WorkItem, one for the original and one for the clone. To have access to the Work Item class, you need to add the following using statement.

C#
using Microsoft.TeamFoundation.WorkItemTracking.Client;

Now add the two properties we need.

C#
public WorkItem originalWorkItem {get; set;}
public WorkItem cloneWorkItem {get; set;}

Finish this up with a constructor that inputs and sets both these values:

C#
public ClonedWorkItemResult(WorkItem originalWorkItem, WorkItem cloneWorkItem)
{
    this.origialWorkItem = originalWorkItem;
    this.cloneWorkItem = cloneWorkItem;
}

Setting Up the Methods

The work items are stored in the Work Item Store, this is in the same namespace we added earlier. The Work Item Store can be retrieved by creating a TFS Team Project Collection instance and getting the service of the type WorkItemStore. Below is the code for getting the work item store. For this tutorial, I just hard coded the values, but I use a .config file in our web site for the actual values so it can be changed externally.

C#
public static WorkItemStore GetWorkItemStoreService()
{
     NetworkCredential networkCredentials = 
     new NetworkCredential("username", "password","domain");

     var tfsUri = https://tfs.company.com/tfs";

     var myTFSTeamProjectCollection = new TfsTeamProjectCollection(
     TfsTeamProjectCollection.GetFullyQualifiedUriForName(tfsUri), 
     networkCredentials);
 
     return myTFSTeamProjectCollection.GetService<WorkItemStore>;
}
  1. Now that we have access to the Work Item Store, we can start working on the code for cloning a work item. For the main method, we want to pass in the ID of the original work item and a new title for our cloned item.
    C#
    public static List<ClonedWorkItemResult> CloneWorkItem(int workItemId, string newTitle) {}
  2. Now we need to do some work inside of that method, first we need to get an instance of the WorkItemStore. We can just call the method we made already.
    C#
    WorkItemStore wiStore = GetWorkItemStoreService();
  3. Make a list to store the ClonedWorkResults:
    C#
    List<ClonedWorkItemResult> clonedWorkItemResults = new List<ClonedWorkItemResult>();
  4. For cloning the work item, we are going to clone each child as well so we need to have a separate method that can be called recursively for each child. Since it is recursive but we need to get the results out of it, we will use a ref to the results. Here is the method call we need.
    C#
    CopyWorkItem(wiStore, workItemId, ref clonedWorkItemResults);
  5. Before we go build the CloneWorkItem method we are going to finish writing the rest of the method calls we need.

    The remaining things to do are:

    1. Set the Cloned Items new title.
    2. Remove the links in the clones as they are still linked to the originals.
    3. Save the work items so the clones generate their ID fields.
    4. Regenerate the links between the new clones.
    5. Return the results for logging and messaging.

    Below is the code we will add and that will finish out the CloneWorkItem method so we can start working on the methods to do the work outside of it that we are calling.

    C#
    //Set the title for the clone
    clonedWorkItemResults.Where
    (c => c.originalWorkItem.Id == workItemId).FirstOrDefault().cloneWorkItem.Title = newTitle;
    
    //Remove the links to the original work items for the clones
    RemoveAllWorkItemLinks(ref clonedWorkItemResults);
    
    //Save the clone work items to generate Ids
    SaveAllWorkItems(ref clonedWorkItemResults);
    
    //Rebuild the links between the clones as the originals had
    CloneWorkItemLinks(ref clonedWorkItemResults, wiStore);
    
    //Return the results for logging and messaging.
    return clonedWorkItemResults;
  6. Now to the CopyWorkItem method. You can modify this as needed to fit your needs. You may not want to copy test cases, or you may want to include attachments or whatever else, but this will get you going. I will let the comments detail what the code is doing from here on out.
    C#
    public static void CopyWorkItem(WorkItemStore wiStore, int workItemId, 
    ref List<ClonedWorkItemResult> clonedWorkItemResults)
    {
           //Get the work Item to copy
           WorkItem wi = wiStore.GetWorkItem(workItemId);
            
           //Copy the work item 
           WorkItem copy = wi.Copy();  
    
           //Iterate through all of the links we have 
           foreach (WorkItemLink wiLink in wi.WorkItemLinks) 
           { 
             //make sure the link is a Work Item link, as we dont want to worry about attachments etc.. 
             if (wiLink.BaseType == BaseLinkType.WorkItemLink) 
             { 
               //We only want to copy the children and the Tested by which is the test case, no parents. 
               if (wiLink.LinkTypeEnd.Name == "Child" || wiLink.LinkTypeEnd.Name == "Tested By") 
               { 
                 //There is a child or test case so we need to copy that child as well. 
                 //We are calling the same method we are in so it is recursive for every child. 
                 CopyWorkItem(wiStore, wiLink.TargetId, ref clonedWorkItemResults); 
               } 
             } 
           } 
           //Failsafe to make sure we do not add a work item that is already  
           if (clonedWorkItemResults.Count(c => c.originalWorkItem.Id == workItemId) == 0) 
           { 
             //Add the clone to the results list 
             clonedWorkItemResults.Add(new ClonedWorkItemResult(wi, copy)); 
           } 
         } 
  7. Now to Remove the Links to the original work items. This is pretty straight forward. We remove all links and regenerate the ones we want by comparing to the original.
    C#
    public static void RemoveAllWorkItemLinks(ref List<ClonedWorkItemResult> clonedWorkItemResults)
     {
         foreach (var clone in clonedWorkItemResults.Select(c => c.cloneWorkItem))
         {
             clone.WorkItemLinks.Clear();
         }
     }
  8. Now to Save the clones. Pretty straight forward here, just save each clone so they generate an ID we can use to regenerate the links.
    C#
    private static void SaveAllWorkItems(ref List<ClonedWorkItemResult> clonedWorkItemResults)
     {
         foreach (WorkItem wi in clonedWorkItemResults.Select(c => c.cloneWorkItem))
         {
              wi.Save();
         }
     }
  9. Now to Regenerate the Links on the clones by referencing the original links. A note here, this example does not retain all links, it only retains links that are WorkItemLinks, it will need to be modified if you want to retain other kinds of links. Also we ignore links to levels above the item we cloned, you may want to retain the original parent on the clone.
    C#
    private static void CloneWorkItemLinks
    (ref List<ClonedWorkItemResult> clonedWorkItemResults, WorkItemStore wiStore)
     {            
         //iterate through the results
         foreach (ClonedWorkItemResult wiResult in clonedWorkItemResults)
         {
             //iterate through the Work Item Links on the original work item.
             foreach (WorkItemLink wiLink in wiResult.originalWorkItem.WorkItemLinks)
         {
              //If we did not clone the Work item go to the next, 
              //i.e., skip the parent epic if we cloned a story
              if (clonedWorkItemResults.Count(c => c.originalWorkItem.Id == wiLink.TargetId) == 0)
              {
                  continue;
              }
     
             //Create a new link of the same type as the original
             wiResult.cloneWorkItem.WorkItemLinks.Add(new WorkItemLink
             (
                  wiStore.WorkItemLinkTypes.LinkTypeEnds[wiLink.LinkTypeEnd.Name],
                  wiResult.cloneWorkItem.Id,
                  clonedWorkItemResults.Where(
                      c => c.originalWorkItem.Id == wiLink.TargetId).Select(
                      c => c.cloneWorkItem.Id).FirstOrDefault())
             );
         }
    
         //Save the clones
          wiResult.cloneWorkItem.Save(SaveFlags.MergeLinks);
         }
     }

Using the Code

Now you can call your code from a Test, a UI project or whatever. Simply call the method and pass in your parameters from whatever input you setup. Here is what you may have from a WPF text box.

C#
var createdItems = CloneWorkItem(txt_WorkItemID.Text, txt_WorkItemNewName.Text);

foreach (var item in createdItems)
 {
      //If not using c#6 change $ to string.format and move the variables.
      Console.WriteLine($"Work Item '{item.cloneWorkItem.Id}: 
        {item.cloneWorkItem.Title}' Cloned from Work Item '{item.originalWorkItem.Id}: 
        {item.originalWorkItem.Title}'");
 }

For a console application, you could pass the Main method args to the clone method and write to the console.

Ending Comments

This clones the items as they are so cloning a closed story makes a closed story. An easy way of using this is have each team setup a template for the scenario they want, then clone that each time. For our agile process, we create a User Story that has a test case on the story and the following tasks as children.

  • Three Amigos/Agile Planning
  • Coding
  • Write Unit Tests
  • Code Review
  • Manual Testing
  • Coded UI Test Case
  • Write Manual Test Case (With presetup steps below)
    • ADA Compliance Generic Test Steps
    • Visual Testing Step
    • Functional Test Generic Steps

Hopefully, you enjoyed this article and it is helpful and in the end, saves you time not having to manually create the same work item setup over and over.

History

  • Original post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)