In diesem Post soll es darum gehen, wie man mit Hilfe von Service Hooks Events in TFS 2015/ VSTS verarbeiten und einen REST-Service aufrufen kann, der dann entsprechende Aktionen über die TFS REST API auslöst. In dem hier gezeigten Beispiel sollen beim Verschieben eines Product Backlog Items auf dem Board automatisiert entsprechende Tasks erzeugt werden. Dies kann z.B. genutzt werden, um für jedes PBI wiederkehrende Tasks zu generieren. Der grobe Ablauf ist wie folgt:
image
Durch das Verschieben eines Product Backlog Items triggert VSTS den konfigurierten ServiceHook. Dieser sendet das WorkItemChanged Event an den Api Controller. Dieser handelt die Informationen ab und der Rest Client schickt die Anfrage für das Erstellen des WorkItems an die VSTS Rest Api.

Konkret soll für jedes PBI, das in die Spalte Committed verschoben wird, automatisch ein Task „Make it done“ angelegt. Wir nutzen hierzu eine Azure Api App, wobei hier auch ein beliebiger anderer Service, z.B. lokaler REST Service, möglich wäre.

Die Solution zu diesem Blogeintrag findet man hier:

https://github.com/artiso-solutions/TfsServices

  1. Api App auf Azure

    image

    1. Anlegen der Api App in Azure

      Zunächst erstellt man eine Api App im Azure Portal.
      clip_image005clip_image007

      Anschließend kann für die Api App Name, Resourcengruppe und Region ausgewählt werden.

      clip_image008

      Nach der Fertigstellung wird man zur Portalseite der Api App weitergeleitet, auf der man seine Einstellungen vornehmen kann. Hier kann z.B. die Skalierung eingestellt werden, um die Kosten zu regulieren.

    2. Konfiguration der Api App

      Die Api App hat standardmäßig das Pricing Tier „Free“.
      AppServicePlan

      Im Reiter/Tab „Application Setings kann man Remote Debugging für die jeweilige Visual Studio Version aktivieren.

      clip_image012[9]

      Nach dem Speichern ist die Konfiguration in Azure vorerst abgeschlossen und man kann mit der Entwicklung der App beginnen.

  2. Erstellen der App

    Nachdem man im ersten Kapitel auf Azure die Api App angelegt hat wird im zweiten Kapitel des Blogs auf die Erstellung der Azure Api App eingegangen. Diese Api App soll jetzt die Events erhalten, die bei VSTS über die Sevice Hooks getriggert werden und danach über die TFS REST API Tasks anlegen soll.

    image

    1. Anlegen eines neuen Azure Api App Projekts

      Als Voraussetzung braucht man das Azure SDK für .NET, welches man hier bekommt (https://azure.microsoft.com/de-de/downloads/), falls es noch nicht installiert ist.

      Nach der Installation wird in Visual Studio ein neues Web Application Projekt angelegt.

      clip_image016

      Bei der Auswahl des Templates wählt man die Api App aus. Diese hat keine Authentifizierung. Diesen Umstand können wir später beheben, indem man eine eigene Authentifizierung erstellt. Die checkbox „Host in the Cloud“ kann man ignorieren, da die Api App ja schon auf Azure regristriert ist.

      clip_image018[8]

    2. Implementierung der API App

      Nach dem Wizard wird eine Solution angelegt, die ein Controllers-Verzeichnis enthält. Darin ist eine Klasse enthalten, die von ApiController erbt. Diese benutzt man um Http-Requests abzuarbeiten. Dazu ändern wir diesen Controller und fügen unsere eigene Methode ein. Diese nimmt den POST-Request vom ServiceHook (siehe Kapitel 3) entgegen und generiert eine Antwort. Die weitere Logik kann man hier dann einbetten.

      public async Task<HttpResponseMessage> WorkItemChanged()
      {
        var content = this.Request.Content.ReadAsStringAsync().Result;
        return this.Request.CreateResponse(HttpStatusCode.Accepted, content);
      }
      

      Um sich z.B. den Content anzusehen, liefert z.B. (http://jsonviewer.stack.hu/) eine schöne Darstellung. Der erhaltene JSON-String kann mit dem Nuget Package NewtonSoftJson deserialisiert werden.

    3. Eigene Authentifizierung erstellen

      Um eine gewisse Sicherheit zu gewährleisten, kann man sich selbst Credentials erstellen, welche später bei der Konfiguration des Service Hooks, für eine Basic-Authentifizierung, verwendet werden können. Folgende Abfrage stellt sicher, dass nur authentifizierte Anfragen bearbeitet werden:

      if (this.Request.Headers.Authorization.Scheme == "Basic" &&
        this.Request.Headers.Authorization.Parameter == Convert.ToBase64String(Encoding.ASCII.GetBytes($"{Username}:{Password}")))
      
    4. Verbindungsaufbau über Security Token

      Jetzt kann eine Verbindung zu VSTS aufgebaut werden, wozu ein Token benötigt wird, welches man sich auf dem VSTS Portal erstellen lässt. (https://{yourAccount}.visualstudio.com/_details/security/tokens)

      clip_image019

      Bei der Erstellung kann die Lebensdauer und der Scope eingestellt werden. Die Lebensdauer bedeutet natürlich, daß man nachdem die Zeit abgelaufen ist, das Token erneuern muss und entsprechend in den Code einpflegen muss. Beim Scope wird eingestellt,welche Rechte der jeweilige Aufruf hat. In diesem Fall bekommt das Token Lese- und Schreibzugriff auf Work Items.

      clip_image021

      Nun kann man im Controller eine Verbindung zu VSTS aufbauen.

      var uri = $"https://{this.account}.visualstudio.com/{this.collection}/{teamProject}/_apis/wit/workitems/${workItemType}?api-version=1.0";
      using (HttpClient client = new HttpClient())
      {
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json-patch+json"));
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{this.user}:{this.password}")));
      }
      

      Hierbei ist der Username leer und das Passwort das vorher erzeugte Token.

      Die Uri kann natürlich angepasst werden. Mehr Informationen dazu findet man auf:

      https://www.visualstudio.com/en-us/integrate/api/overview

    5. Auslesen der wichtigen Informationen aus dem eingegangenen Service Hook Event

      Die einzelnen Parameter können aus dem eingegangenen Event geholt werden. Die Id des WorkItems liegt auf dem folgenden Feld:

      var parentWorkItemId = deserializedWorkItem.Resource.WorkItemId;

      Aus der folgenden Methode bekommt man alle Informationen, die auf dem WorkItem gespeichert sind:

      public async Task ReadFullyExpandedWorkItem(int workItemId)
      {
        FullyExpandedWorkItem workItem;
        var uri = $"https://{this.account}.visualstudio.com/{this.collection}/_apis/wit/workitems/{workItemId}?$expand=all&api-version=1.0";
        using (var client = new HttpClient())
        {
          client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
          client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
          Convert.ToBase64String(Encoding.ASCII.GetBytes($"{this.user}:{this.password}")));
          using (HttpResponseMessage response = client.GetAsync(uri).Result)
          {
            response.EnsureSuccessStatusCode();
            string responseBody = await response.Content.ReadAsStringAsync();
            workItem = JsonConvert.DeserializeObject (responseBody);
          }
          return workItem;
        }
      }
      
    6. Erstellen des neuen WorkItems

      Um jetzt ein WorkItem anzulegen, muss man analog zu der in der TFS REST API beschriebenen PATCH Operation (https://www.visualstudio.com/en-us/integrate/api/wit/work-items#CreateaworkitemWithaworkitemlink) vorgehen. Man kann diese JSON-PatchOperation gut mit dem Nuget-Package NewtonSoftJson nachbilden:

      private JsonPatchDocument CreateJsonPatchDocument(string newWorkItemTitle, int workItemId, string iteration, string description)
      {
        var json = new JsonPatchDocument
        {
          new JsonPatchOperation
          {
            Operation = Operation.Add,
            Path = "/fields/System.Title",
            Value = newWorkItemTitle
          }
        };
        if (iteration != null)
        {
          json.Add(new JsonPatchOperation
          {
            Operation = Operation.Add,
            Path = "/fields/System.IterationPath",
            Value = iteration
          });
        }
        if (description != null)
        {
          json.Add(new JsonPatchOperation
          {
            Operation = Operation.Add,
            Path = "/fields/System.Description",
            Value = description
          });
        }
      
        /// Hier kommt der unten genannte Teil mit der Verlinkung hinein
        return json;
      }
      

      Wichtig ist hier ein Teil, der die Verlinkung zu dem ParentWorkItem gewährleistet, damit der Task der erstellt wird auch unter dem richtigen Product Backlog Item erscheint:

       if (workItemId >= 0)
        {
          json.Add(new JsonPatchOperation
          {
            Operation = Operation.Add,
            Path = "/relations/-",
            Value = new WorkItemLink
            {
              Rel = "System.LinkTypes.Hierarchy-Reverse",
              Url = $"https://{this.account}.visualstudio.com/{this.collection}/_apis/wit/workItems/{workItemId}",
              Attributes = new Attributes()
              {
                Comment = "Created by TFSServicesForExpertBlog"
              }
            }
          });
        }
    7. Senden des Json-Patchdocuments an die TFS Rest API

      Die nächste Methode beschreibt, wie man die erzeugte JSON-Patchoperation dann an die TFS REST API schickt.

      public async Task CreateNewLinkedWorkItem(string teamProject, string newWorkItemTitle, string workItemType, int parentWorkItemId, string iteration, string description)
      {
        WorkItem createdWorkItem;
        var json = this.CreateJsonPatchDocument(newWorkItemTitle, workItemId, iteration, description);
        var jsonString = JsonConvert.SerializeObject(json);
        var uri = $"https://{this.account}.visualstudio.com/{this.collection}/{teamProject}/_apis/wit/workitems/${workItemType}?api-version=1.0";
        var content = new StringContent(jsonString, Encoding.UTF8, "application/json-patch+json");
        using (HttpClient client = new HttpClient())
        {
          client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json-patch+json"));
          client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{this.user}:{this.password}")));
          using (var response = client.PatchAsync(uri, content).Result)
          {
            response.EnsureSuccessStatusCode();
            string responseBody = await response.Content.ReadAsStringAsync();
            createdWorkItem = JsonConvert.DeserializeObject(responseBody);
          }
        }
        return createdWorkItem;
      }
    8. Publishing der Azure Api App aus Visual Studio

      Um die Azure Api App zu publishen klickt man mit der rechten Maustaste im Solution Explorer auf das Projekt und wählt „Publish“ aus.clip_image023[8]

      Über den Button Microsoft Azure App Service kann man sich bei Azure mit seinem Microsoft Account anmelden, und die vorher angelegte Api App wird angezeigt.

      clip_image025[8]

      Dieser wird ausgewählt und der Wizard holt alle wichtigen Daten für das Publishen.

      clip_image027[8]

      Nun können noch die Konfiguration und die Settings eingestellt werden.

      clip_image029[8]

      Nach einem Klick auf „Next“ kann mit dem neu erstellten „Web Publish“-Profil die Api App auf Azure hochgeladen werden.

  3. Registrieren der Api App im VSTS Service Hook

    Nach dem Erstellen und Publishen der Api App soll jetzt im dritten Teil die Verbindung zum VSTS Service Hook erstellt werden.image

    1. Erstellen des Service Hooks

      Jetzt kann man zum VSTS Dashboard des TeamProjects wechseln.(https://{account}.visualstudio.com/{Collection}/{TeamProject}/_dashboards)Über das Zahnrad in der rechten oberen Ecke, wird zu Administrationsseite navigiert, und dort zum Tab für die Service Hooks.

      clip_image032

      clip_image035

      clip_image034

      Hier wird ein neuer Service Hook vom Typ „Web Hooks“ erstellt:

      clip_image037

    2. Konfiguration des Service Hooks

      Es kann ausgewählt werden welches Event die Api App triggern soll, sowie welche Bedingungen zusätzlich erfüllt sein sollen.Im Beispiel wird für den Trigger „Work item updated“ und für Work Item Type „Product Backlog Item“ gewählt um auf jegliche Änderung eines PBIs zu reagieren.
      clip_image038

      Auf der letzten Konfigurationsseite kann nun die Api App URL mit dem Zusatz „/api/WorkItemChanged“ eingegeben werden. Damit hier die Authentifizierung nicht in Klartext verschickt wird, muss der URL https vorangestellt werden. Zur Authentifizierung geben wir nun den Username und das Passwort, das wir im zweiten Kapitel erstellt haben an.

      clip_image039

      Nach einem optionalen „Test“, ob die Api App angesprochen werden kann, wird die Konfiguration mit „Finish“ beendet.

      Im Ganzen haben wir jetzt also folgende Prozesskette:

      image

      Durch das Verschieben eines Product Backlog Items triggert VSTS den konfigurierten ServiceHook. Dieser sendet das WorkItemChanged Event an den Api Controller. Dieser handelt die Informationen ab und der Rest Client schickt die Anfrage für das Erstellen des Workitems an die VSTS Rest Api.

      Für den abschließenden Test wird auf dem Kanban Board ein Product Backlog Item in die Spalte „Committed“ verschoben. Daraufhin wird ein WorkItemChangedEvent ausgelöst, welches über den Service Hook eine Mitteilung an die Api App schickt. Die Api App legt dann über die REST API von VSTS den Task „Make it Done“ an. So bekommt man den Task, den man im Sprint immer braucht, automatisch angelegt.

      clip_image042[8]

      clip_image044

  4. Troubleshooting

    1. Remote Debugging aus Visual Studio

      Falls man Probleme hat kann man die Api App aus Visual Studio heraus debuggen.https://blogs.msdn.microsoft.com/webdev/2013/11/04/remote-debugging-a-window-azure-web-site-with-visual-studio-2013/