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 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 2 – Set the CreatedDate to historic date (Same as the ChangedDate)
Image 3 – Simulate of effort burn down on a task via the TFS API
Image 4 – The history of changes on the Task. So, essentially this task has burned 1 hour per day
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 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 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.”
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!