Wednesday, November 4, 2015

[TFS] Use Team Foundation Server SDK to implement a WorkItem tool

 #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

即使直接更新在資料庫中的欄位,在Client端看到的Area Path和Iteration Path並不會實際改變。



3.  無法直接用TFS SDK直接修改Work Item的「Team Project」、「Area Path」、「Iteration Path」

首先,當你直接用程式碼呼叫SDK更改「Team 專案」(Team Project)的值時 , 會收到以下Exception … (連儲存的機會都沒有 lol )

"TF26194: 無法變更這個欄位 'Team 專案' 的值。"

直接修改 「區域路徑」(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。

[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。

PS. 在此我是使用 TFS2013的CMMI Process template,因此實際State值要考慮你使用的專案範本。

2.  更新State,必須符合流程標準。
請把SDK的使用,當成直接使用前端的介面操作。 所以任何違反你專案範本流程的動作,是不允許的。
舉例來說,如果專案範本定義當Work Item State為 “已提議”的情況下, 下個State只能是”作用中” 。
那麼直接在程式碼將State直接從”已提議”更新為”已關閉’, 則會引用Validation Fail。

所以我在下面程式碼的流程中, 如果原始的工作項目為”已關閉”,那我將以流程的順序,依序逐一更新工作項目的State 為 “作用中” → “已解決” → “已關閉”。

3.  不建議直接到Database更新State
更新State,會有一些相關欄位會自動更新,例如: “原因”、 “解決日期” 、“更新日期” …. Etc
所以你如果想直接繞過SDK的驗證, 調整DB的值是可以的,但是前提是你必須知道哪些欄位會連動需要更新, 否則更新後的工作項目, 當再次在介面上開啟時並儲存時, 便會開始產生一堆驗證錯誤。
所以還是建議直接用SDK調整State。

4.  客製化欄位必須手動更新
SDK的複製函式,並不會複製你的客製化欄位 (Customize Fields)。 所以請自行寫代碼更新它吧。
我這邊採用的方式,是直接到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





No comments:

Post a Comment