TFS API Change WorkItem CreatedDate And ChangedDate To Historic Dates

Posted by Tarun Arora on Geeks with Blogs See other posts from Geeks with Blogs or by Tarun Arora
Published on Sat, 27 Oct 2012 11:53:00 GMT Indexed on 2012/10/27 17:01 UTC
Read the original article Hit count: 356

Filed under:

There may be times when you need to modify the value of the fields “System.CreatedDate” and “System.ChangedDate” on a work item. Richard Hundhausen has a great blog with ample of reason why or why not you should need to set the values of these fields to historic dates.

In this blog post I’ll show you,

  • Create a PBI WorkItem linked to a Task work item by pre-setting the value of the field ‘System.ChangedDate’ to a historic date
  • Change the value of the field ‘System.Created’ to a historic date
  • Simulate the historic burn down of a task type work item in a sprint
  • Explain the impact of updating values of the fields CreatedDate and ChangedDate on the Sprint burn down chart

Rules of Play

     1. You need to be a member of the Project Collection Service Accounts

        image

     2. You need to use ‘WorkItemStoreFlags.BypassRules’ when you instantiate the WorkItemStore service

// Instanciate Work Item Store with the ByPassRules flag
_wis = new WorkItemStore(_tfs, WorkItemStoreFlags.BypassRules);

     3. You cannot set the ChangedDate

        - Less than the changed date of previous revision

        - Greater than current date

Walkthrough

The walkthrough contains 5 parts

  • 00 – Required References
  • 01 – Connect to TFS Programmatically
  • 02 – Create a Work Item Programmatically
  • 03 – Set the values of fields ‘System.ChangedDate’ and ‘System.CreatedDate’ to historic dates
  • 04 – Results of our experiment

Lets get started…………………………………………………

00 – Required References

Microsoft.TeamFoundation.dll

Microsoft.TeamFoundation.Client.dll

Microsoft.TeamFoundation.Common.dll

Microsoft.TeamFoundation.WorkItemTracking.Client.dll

01 – Connect to TFS Programmatically

I have a in depth blog post on how to connect to TFS programmatically in case you are interested. However, the code snippet below will enable you to connect to TFS using the Team Project Picker.

// Services I need access to globally
private static TfsTeamProjectCollection _tfs;
private static ProjectInfo _selectedTeamProject;
private static WorkItemStore _wis;

// Connect to TFS Using Team Project Picker
public static bool ConnectToTfs()
{
            var isSelected = false;

            // The  user is allowed to select only one project
            var tfsPp = new TeamProjectPicker(TeamProjectPickerMode.SingleProject, false);

            tfsPp.ShowDialog();

            // The TFS project collection
            _tfs = tfsPp.SelectedTeamProjectCollection;

            if (tfsPp.SelectedProjects.Any())
            {
               //  The selected Team Project
                _selectedTeamProject = tfsPp.SelectedProjects[0];
                isSelected = true;
            }

            return isSelected;
}

02 – Create a Work Item Programmatically

In the below code snippet I have create a Product Backlog Item and a Task type work item and then link them together as parent and child.

Note – You will have to set the ChangedDate to a historic date when you created the work item. Remember, If you try and set the ChangedDate to a value earlier than last assigned you will receive the following exception…

TF26212: Team Foundation Server could not save your changes. There may be problems with the work item type definition. Try again or contact your Team Foundation Server administrator.

If you notice below I have added a few seconds each time I have modified the ‘ChangedDate’ just to avoid running into the exception listed above.

// Create Linked Work Items and return Ids
private static List<int> CreateWorkItemsProgrammatically()
{
    // Instantiate Work Item Store with the ByPassRules flag
    _wis = new WorkItemStore(_tfs, WorkItemStoreFlags.BypassRules);

    // List of work items to return
    var listOfWorkItems = new List<int>();

    // Create a new Product Backlog Item
    var p = new WorkItem(_wis.Projects[_selectedTeamProject.Name].WorkItemTypes["Product Backlog Item"]);
    p.Title = "This is a new PBI";
    p.Description = "Description";
    p.IterationPath = string.Format("{0}\\Release 1\\Sprint 1", _selectedTeamProject.Name);
    p.AreaPath = _selectedTeamProject.Name;
    p["Effort"] = 10;

    // Just double checking that ByPassRules is set to true
    if (_wis.BypassRules)
    {
        p.Fields["System.ChangedDate"].Value = Convert.ToDateTime("2012-01-01");
    }

    if (p.Validate().Count == 0)
    {
        p.Save();

        listOfWorkItems.Add(p.Id);
    }
    else
    {
        Console.WriteLine(">> Following exception(s) encountered during work item save: ");
        foreach (var e in p.Validate())
        {
            Console.WriteLine(" - '{0}' ", e);
        }
    }

    var t = new WorkItem(_wis.Projects[_selectedTeamProject.Name].WorkItemTypes["Task"]);
    t.Title = "This is a task";
    t.Description = "Task Description";
    t.IterationPath = string.Format("{0}\\Release 1\\Sprint 1", _selectedTeamProject.Name);
    t.AreaPath = _selectedTeamProject.Name;
    t["Remaining Work"] = 10;

    if (_wis.BypassRules)
    {
        t.Fields["System.ChangedDate"].Value = Convert.ToDateTime("2012-01-01");
    }

    if (t.Validate().Count == 0)
    {
        t.Save();

        listOfWorkItems.Add(t.Id);
    }
    else
    {
        Console.WriteLine(">> Following exception(s) encountered during work item save: ");
        foreach (var e in t.Validate())
        {
            Console.WriteLine(" - '{0}' ", e);
        }
    }

    var linkTypEnd = _wis.WorkItemLinkTypes.LinkTypeEnds["Child"];
            
    p.Links.Add(new WorkItemLink(linkTypEnd, t.Id)
                    {ChangedDate = Convert.ToDateTime("2012-01-01").AddSeconds(20)});
            
    if (_wis.BypassRules)
    {
        p.Fields["System.ChangedDate"].Value = Convert.ToDateTime("2012-01-01").AddSeconds(20);
    }

    if (p.Validate().Count == 0)
    {
        p.Save();
    }
    else
    {
        Console.WriteLine(">> Following exception(s) encountered during work item save: ");
        foreach (var e in p.Validate())
        {
            Console.WriteLine(" - '{0}' ", e);
        }
    }

    return listOfWorkItems;
}

03 – Set the value of “Created Date” and Change the value of “Changed Date” to Historic Dates

The CreatedDate can only be changed after a work item has been created. If you try and set the CreatedDate to a historic date at the time of creation of a work item, it will not work.

// Lets do a work item effort burn down simulation by updating the ChangedDate & CreatedDate to historic Values
private static void WorkItemChangeSimulation(IEnumerable<int> listOfWorkItems)
{
    foreach (var id in listOfWorkItems)
    {
        var wi = _wis.GetWorkItem(id);

        switch (wi.Type.Name)
        {
            case "ProductBacklogItem":

                if (wi.State.ToLower() == "new")
                    wi.State = "Approved";

                // Advance the changed date by few seconds
                wi.Fields["System.ChangedDate"].Value =
                    Convert.ToDateTime(wi.Fields["System.ChangedDate"].Value).AddSeconds(10);
                        
                // Set the CreatedDate to Changed Date
                wi.Fields["System.CreatedDate"].Value =
                    Convert.ToDateTime(wi.Fields["System.ChangedDate"].Value).AddSeconds(10);

                wi.Save();

                break;
            case "Task":

                // Advance the changed date by few seconds
                wi.Fields["System.ChangedDate"].Value =
                    Convert.ToDateTime(wi.Fields["System.ChangedDate"].Value).AddSeconds(10);

                // Set the CreatedDate to Changed date
                wi.Fields["System.CreatedDate"].Value =
                    Convert.ToDateTime(wi.Fields["System.ChangedDate"].Value).AddSeconds(10);

                wi.Save();

                break;
        }
    }

    // A mock sprint start date 
    var sprintStart = DateTime.Today.AddDays(-5);
    // A mock sprint end date
    var sprintEnd = DateTime.Today.AddDays(5);

   // What is the total Sprint duration
    var totalSprintDuration = (sprintEnd - sprintStart).Days;
    // How much of the sprint have we already covered
    var noOfDaysIntoSprint = (DateTime.Today - sprintStart).Days;
            
    // Get the effort assigned to our tasks 
    var totalEffortRemaining = QueryTaskTotalEfforRemaining(listOfWorkItems);
            
    // Defining how much effort to burn every day
    decimal dailyBurnRate = totalEffortRemaining / totalSprintDuration < 1
                                ? 1
                                : totalEffortRemaining / totalSprintDuration;

    // we have just created one task
    var totalNoOfTasks = 1; 
            
    var simulation = sprintStart;
    var currentDate = DateTime.Today.Date;

    // Carry on till effort has been burned down from sprint start to today
    while (simulation.Date != currentDate.Date)
    {
        var dailyBurnRate1 = dailyBurnRate;

        // A fixed amount needs to be burned down each day
        while (dailyBurnRate1 > 0)
        {
            // burn down bit by bit from all unfinished task type work items
            foreach (var id in listOfWorkItems)
            {
                var wi = _wis.GetWorkItem(id);

                var isDirty = false;

                // Set the status to in progress
                if (wi.State.ToLower() == "to do")
                {
                    wi.State = "In Progress";
                    isDirty = true;
                }

                // Ensure that there is enough effort remaining in tasks to burn down the daily burn rate
                if (QueryTaskTotalEfforRemaining(listOfWorkItems) > dailyBurnRate1)
                {
                    // If there is less than 1 unit of effort left in the task, burn it all
                    if (Convert.ToDecimal(wi["Remaining Work"]) <= 1)
                    {
                        wi["Remaining Work"] = 0;
                        dailyBurnRate1 = dailyBurnRate1 - Convert.ToDecimal(wi["Remaining Work"]);
                        isDirty = true;
                    }
                    else
                    {
                        // How much to burn from each task?
                        var toBurn = (dailyBurnRate / totalNoOfTasks) < 1
                                            ? 1
                                            : (dailyBurnRate / totalNoOfTasks);

                        // Check that the task has enough effort to allow burnForTask effort
                        if (Convert.ToDecimal(wi["Remaining Work"]) >= toBurn)
                        {
                            wi["Remaining Work"] = Convert.ToDecimal(wi["Remaining Work"]) - toBurn;
                            dailyBurnRate1 = dailyBurnRate1 - toBurn;
                            isDirty = true;
                        }
                        else
                        {
                            wi["Remaining Work"] = 0;
                            dailyBurnRate1 = dailyBurnRate1 - Convert.ToDecimal(wi["Remaining Work"]);
                            isDirty = true;
                        }
                    }
                }
                else
                {
                    dailyBurnRate1 = 0;
                }

                if (isDirty)
                {
                    if (Convert.ToDateTime(wi.Fields["System.ChangedDate"].Value).Date == simulation.Date)
                    {
                        wi.Fields["System.ChangedDate"].Value =
                            Convert.ToDateTime(wi.Fields["System.ChangedDate"].Value).AddSeconds(20);
                    }
                    else
                    {
                        wi.Fields["System.ChangedDate"].Value = simulation.AddSeconds(20);
                    }

                    wi.Save();
                }
            }                    
        }

        // Increase date by 1 to perform daily burn down by day
        simulation = Convert.ToDateTime(simulation).AddDays(1);
    }
}
// Get the Total effort remaining in the current sprint
private static decimal QueryTaskTotalEfforRemaining(List<int> listOfWorkItems)
{
    var unfinishedWorkInCurrentSprint =
                    _wis.GetQueryDefinition(
                        new Guid(QueryAndGuid.FirstOrDefault(c => c.Key == "Unfinished Work").Value));

    var parameters = new Dictionary<string, object> { { "project", _selectedTeamProject.Name } };
    var q = new Query(_wis, unfinishedWorkInCurrentSprint.QueryText, parameters);
    var results = q.RunLinkQuery();

    var wis = new List<WorkItem>();

    foreach (var result in results)
    {
        var _wi = _wis.GetWorkItem(result.TargetId);
        if (_wi.Type.Name == "Task" && listOfWorkItems.Contains(_wi.Id))
            wis.Add(_wi);
    }
    return wis.Sum(r => Convert.ToDecimal(r["Remaining Work"]));
}

 

04 – The Results

If you are still reading, the results are beautiful!

Image 1 – Create work item with Changed Date pre-set to historic date

image

Image 2 – Set the CreatedDate to historic date (Same as the ChangedDate)

image

Image 3 – Simulate of effort burn down on a task via the TFS API

image

 

Image 4 – The history of changes on the Task. So, essentially this task has burned 1 hour per day

image

Sprint Burn Down Chart – What’s not possible?

The Sprint burn down chart is calculated from the System.AuthorizedDate and not the System.ChangedDate/System.CreatedDate. So, though you can change the System.ChangedDate and System.CreatedDate to historic dates you will not be able to synthesize the sprint burn down chart.

image

Image 1 – By changing the Created Date and Changed Date to ‘18/Oct/2012’ you would have expected the burn down to have been impacted, but it won’t be, because the sprint burn down chart uses the value of field ‘System.AuthorizedDate’ to calculate the unfinished work points. The AsOf queries that are used to calculate the unfinished work points use the value of the field ‘System.AuthorizedDate’.

image

Image 2 – Using the above code I burned down 1 hour effort per day over 5 days from the task work item, I would have expected the sprint burn down to show a constant burn down, instead the burn down shows the effort exhausted on the 24th itself. Simply because the burn down is calculated using the ‘System.AuthorizedDate’.

Now you would ask… “Can I change the value of the field System.AuthorizedDate to a historic date”

Unfortunately that’s not possible! You will run into the exception ValidationException –  “TF26194: The value for field ‘Authorized Date’ cannot be changed.”

SNAGHTML48b5abc6

Conclusion

- You need to be a member of the Project Collection Service account group in order to set the fields ‘System.ChangedDate’ and ‘System.CreatedDate’ to historic dates

- You need to instantiate the WorkItemStore using the flag ByPassValidation

- The System.ChangedDate needs to be set to a historic date at the time of work item creation. You cannot reset the ChangedDate to a date earlier than the existing ChangedDate and you cannot reset the ChangedDate to a date greater than the current date time.

- The System.CreatedDate can only be reset after a work item has been created. You cannot set the CreatedDate at the time of work item creation. The CreatedDate cannot be greater than the current date. You can however reset the CreatedDate to a date earlier than the existing value.

- You will not be able to synthesize the Sprint burn down chart by changing the value of System.ChangedDate and System.CreatedDate to historic dates, since the burn down chart uses AsOf queries to calculate the unfinished work points which internally uses the System.AuthorizedDate and NOT the System.ChangedDate & System.CreatedDate

- System.AuthorizedDate cannot be set to a historic date using the TFS API

Read other posts on using the TFS API here

Enjoy!

© Geeks with Blogs or respective owner