#C# #Team Foundation Server #Team Foundation Server SDK TFS Migration
▌背景
在TFS從 2010 升級及轉移到2013版本時,原本舊的工作管理方式是以系統類別來建立 Team Project, 但是在移轉後, 則是以部門別來建立Team Project ,並在各部門下在設定各小組的Area Path。
(圖一. 原始)
(圖二. Migration後)
換句話說, 我遇到的最大問題是必須把移轉後的舊Team Project裡面的工作項目, 再搬移到這些新的Team Project。
聽起來很簡單,可是撩下去才發現有以下問題:
1.
工作項目無法直接從Team Web Access更改Team Project
雖然可以修改Area Path (區域路徑),但是並無法設定為其他Team Project的Area Path。 例如下圖,只能從原本的Team Project: Demo選擇不同的Area Path, 無法選擇 Team Project : 資訊部 的任何Area Path。
2.
無法直接從TFS Database 更新Area Path和Iteration Path
請參考我的這一篇文章: [TFS] 手動更新Work Item在資料庫的欄位值
即使直接更新在資料庫中的欄位,在Client端看到的Area Path和Iteration Path並不會實際改變。
3.
無法直接用TFS SDK直接修改Work Item的「Team Project」、「Area Path」、「Iteration Path」
首先,當你直接用程式碼呼叫SDK更改「Team 專案」(Team Project)的值時 , 會收到以下Exception … (連儲存的機會都沒有 lol )
直接修改 「區域路徑」(Area Path)、「反覆項目路徑」(Iteration
Path)時,
SDK在儲存Work Item時會出現Validation Fail : Invalid Path
如果再細看Validation Result, 你會發現這兩個可允許的值為空白, 也就是說 … TFS SDK也不給修改這兩個屬性值啦!!
由以上實驗,我後來只能採用”複製工作項目”的方式, 將原本的工作複製到新的Team專案下。
Team Web
Access 和 Team Explorer都有提供”複製工作項目”的功能, BUT …
因為我現在要移轉的Work Items有幾千個, 如果採這種方式,我必須用滑鼠點擊好幾千次!!
因此才有了直接寫工具利用TFS SDK,來達成複製整批工作項目的想法。
▌環境
l Windows2008 R2
l Team Foundation
Server 2013 Update 5
l SQL SERVER
Management Studio 2012
|
▌實作
▋DAO
在Migration中,我需要將一些資訊紀錄在Database,作為輔助及後續的追蹤,所以以Entity Framework Code First 建立以下表格。
l WorkItemResults
紀錄將要移轉的原始工作項目。
紀錄將要移轉的原始工作項目。
[Table("WorkItemResults")]
public class WorkItemResult
{
[Key]
[Column(Order = 1)]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public Int32 Id { get; set; }
[MaxLength(100)]
public String Project { get; set; }
[MaxLength(300)]
public String AreaPath { get; set; }
public DateTime CreatedDate { get; set; }
[MaxLength(1000)]
public String Title { get; set; }
[MaxLength(50)]
public String State { get; set; }
[MaxLength(50)]
public String WiType { get; set; }
[MaxLength(200)]
public String IterationPath { get; set; }
[MaxLength(300)]
public String AssignedTo { get; set; }
}
|
l WorkItemCloneMappings
紀錄移轉過程中,複製工作項目的結果,以及對應的新舊項目ID。
紀錄移轉過程中,複製工作項目的結果,以及對應的新舊項目ID。
[Table("WorkItemCloneMappings")]
public class WorkItemCloneMapping : BaseEntity
{
[Key]
[Column(Order = 1)]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Sn { get; set; }
public int FromId { get; set; }
public int ToId { get; set; }
public bool IsSuccess { get; set; }
}
|
▋主程式 : Clone 流程
以下是我的主程式,流程已寫於註解。
這邊要注意的是,複製後的工作項目,雖然有保留原始項目的連結(包含工作項目連結、外部連結)以及附件, 和大部分的欄位值。 但是有幾點要注意:
1.
複製出來的Work Item其預設State (狀況) * 是 “已提議”; 並非舊工作項目的目前State。
因此我們也必須在複製後將其State改變成符合舊項目的State。
因此我們也必須在複製後將其State改變成符合舊項目的State。
PS. 在此我是使用
TFS2013的CMMI Process
template,因此實際State值要考慮你使用的專案範本。
2.
更新State,必須符合流程標準。
請把SDK的使用,當成直接使用前端的介面操作。 所以任何違反你專案範本流程的動作,是不允許的。
請把SDK的使用,當成直接使用前端的介面操作。 所以任何違反你專案範本流程的動作,是不允許的。
舉例來說,如果專案範本定義當Work Item State為 “已提議”的情況下, 下個State只能是”作用中” 。
那麼直接在程式碼將State直接從”已提議”更新為”已關閉’, 則會引用Validation Fail。
所以我在下面程式碼的流程中,
如果原始的工作項目為”已關閉”,那我將以流程的順序,依序逐一更新工作項目的State 為 “作用中”
→ “已解決”
→ “已關閉”。
3.
不建議直接到Database更新State
更新State,會有一些相關欄位會自動更新,例如: “原因”、 “解決日期” 、“更新日期” …. Etc
所以你如果想直接繞過SDK的驗證, 調整DB的值是可以的,但是前提是你必須知道哪些欄位會連動需要更新, 否則更新後的工作項目, 當再次在介面上開啟時並儲存時, 便會開始產生一堆驗證錯誤。
更新State,會有一些相關欄位會自動更新,例如: “原因”、 “解決日期” 、“更新日期” …. Etc
所以你如果想直接繞過SDK的驗證, 調整DB的值是可以的,但是前提是你必須知道哪些欄位會連動需要更新, 否則更新後的工作項目, 當再次在介面上開啟時並儲存時, 便會開始產生一堆驗證錯誤。
所以還是建議直接用SDK調整State。
4.
客製化欄位必須手動更新
SDK的複製函式,並不會複製你的客製化欄位 (Customize Fields)。 所以請自行寫代碼更新它吧。
我這邊採用的方式,是直接到DB下SQL更新這些客製欄位值。 這就是為什麼前面會提到要留一個新舊ID的Mapping記錄用意。
我這邊採用的方式,是直接到DB下SQL更新這些客製欄位值。 這就是為什麼前面會提到要留一個新舊ID的Mapping記錄用意。
private static Uri _tfsCollectionUri
= new Uri("http://127.0.0.1:8080/tfs/DefaultCollection");
static void Main(string[] args)
{
WorkItemCollection wiCollection = null;
//Step1. Query the original Work
Items and save them to the database.
using (var wiService = new WorkItemService(_tfsCollectionUri))
{
wiCollection =
wiService.Query(58);
}
saveWorkItemsToDatabase(wiCollection);
wiCollection = null;
//Step2. Clone the Work Items =>
The new Work Items' state will be "已提議"
var clonedWorkItemIds = cloneWorkItems(x
=> true);
//Step3. Change the specific Work
Items' state to "作用中" (Optional)
updateStateToWorking(clonedWorkItemIds);
//Step4. Change the specific Work
Items' state to "已解決" (Optional)
updateStateToDone(clonedWorkItemIds);
//Step5. Change the specific Work
Items' state to "已關閉" (Optional)
updateStateToClosed(clonedWorkItemIds);
//Final Step. Update the Work Items' vlaue of customize fields.
//For this, I write some sql to
update them in SSMS.
}
|
▋主程式 : 其他
private static void
updateStateToWorking(List<int> clonedWorkItemIds)
{
ArrayList validationResult = null;
try
{
using (var wiService = new WorkItemService(_tfsCollectionUri))
using (var wifieldService = new WorkItemFieldService(_tfsCollectionUri))
{
foreach (var id in clonedWorkItemIds)
{
var wis =
wiService.Query(id);
var keyVals = new List<KeyValuePair<String, object>>() {
new KeyValuePair<string, object>("狀況", "作用中"),
new KeyValuePair<string, object>("啟動者", wis[0].Fields["指派給"].Value)
};
wifieldService.Update(id, keyVals, out validationResult);
}
}
}
catch (Exception ex)
{
logger.Error(ex.Message);
throw;
}
}
private static List<int>
cloneWorkItems(Func<WorkItemResult,bool> filter)
{
List<int> clonedWorkItemIds = new List<int>();
IEnumerable<WorkItemResult> sourceWis = null;
try
{
using (var wiRsltService = new WorkItemResultService())
{
sourceWis =
wiRsltService.Query(filter);
}
using (var wiService = new WorkItemService(_tfsCollectionUri))
using (var wiCloneMappingService = new WorkItemCloneMappingService())
{
var wiDto = new WorkItemDto()
{
TeamProject = "資訊部",
AreaPath = @"資訊部\DevOps Team",
IterationPath = @"資訊部\2015",
};
var otherFieldsSettings = new List<KeyValuePair<string, object>>()
{
new KeyValuePair<String, object>("專業領域", "分析")
};
foreach (var wi in sourceWis)
{
ArrayList validationRslt = null;
int targetId =
wiService.Clone(
wi.Id, wiDto,
otherFieldsSettings, out validationRslt);
if
(validationRslt.Count > 0)
{
wiCloneMappingService.Add(new WorkItemCloneMapping() { FromId = wi.Id, IsSuccess = false });
String msg = String.Format("Work Itme
{0} clone attemp failed.", wi.Id);
logger.Info(msg);
}
else
{
clonedWorkItemIds.Add(targetId);
wiCloneMappingService.Add(new WorkItemCloneMapping() { FromId = wi.Id, ToId =
targetId, IsSuccess = true });
String msg = String.Format("Work Itme
{0} was cloned as Work Item {1}", wi.Id, targetId);
logger.Info(msg);
}
}
}
return clonedWorkItemIds;
}
catch (Exception ex)
{
logger.Error(ex.Message);
throw;
}
}
private static void
saveWorkItemsToDatabase(WorkItemCollection wiCollection)
{
using (var tfsDalService = new WorkItemResultService())
{
#region Clear the databse
tfsDalService.Clear();
#endregion
#region Add a work item information to
database
foreach (WorkItem wi in wiCollection)
{
tfsDalService.Add(new WorkItemResult()
{
Id = wi.Id,
Title = wi.Title,
AreaPath =
wi.AreaPath,
AssignedTo =
wi.Fields["指派給"].Value.ToString(),
IterationPath =
wi.IterationPath,
WiType =
wi.Type.Name,
Project =
wi.Project.Name,
State = wi.State,
CreatedDate =
wi.CreatedDate
});
}
#endregion
}
}
|
▌Migration 結果
上圖的查詢是我要移轉的工作項目, 下面查詢則是複製後的結果。
▌Reference
沒有留言:
張貼留言