2015年11月4日 星期三

[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 ProjectArea Path
例如下圖,只能從原本的Team Project: Demo選擇不同的Area Path 無法選擇 Team Project : 資訊部 的任何Area Path




2.  無法直接從TFS Database 更新Area PathIteration Path

即使直接更新在資料庫中的欄位,在Client端看到的Area PathIteration 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. 在此我是使用 TFS2013CMMI 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) 所以請自行寫代碼更新它吧。
我這邊採用的方式,是直接到DBSQL更新這些客製欄位值 這就是為什麼前面會提到要留一個新舊IDMapping記錄用意。




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





沒有留言:

張貼留言