#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








 
沒有留言:
張貼留言