Jubiläums-Dossier 2017 60 Seiten gebündeltes Wissen zu Mobile-/App-Trends, -Development, -Business & -Design bastacon www.basta.net Inhalt .NET Framework & C# Tolle Typen – Wie statisch muss Typisierung sein? Kolumne: Olis bunte Welt der IT 3 von Oliver Sturm Agile & DevOps ViewModels test­getrieben entwickeln Test-driven Development (TDD) in MVVM-Apps 7 von Thomas Claudius Huber VSTS/TFS – ganz nach meinem Geschmack Verschiedene Erweiterungs- und Anpassungsmöglichkeiten 16 von Marc Müller Web Development Echtzeitchat mit Node.js und Socket.IO Nutzung von echtem Server-Push mit dem WebSocket-Protokoll 22 von Manuel Rauber Mit Azure zur IoT-Infrastruktur Vom Gerät zum Live-Dashboardl 35 von Thomas Claudius Huber Data Access & Storage Entity Framework Core 1.0 Was bringt der Neustart? 42 von Manfred Steyer HTML5 & JavaScript Die Alternative für JavaScript-Hasser TypeScript = JavaScript + x 48 von Dr. Holger Schwichtenberg Asynchrones TypeScript TypeScript lernt async/await 58 von Rainer Stropek User Interface Das GeBOT der Stunde? Conversational UIs: Ein erster Blick auf das Microsoft Bot Framework von Roman Schacherl und Daniel Sklenitzka 64 DOSSIER .NET Framework & C# Kolumne: Olis bunte Welt der IT Tolle Typen – Wie statisch muss Typisierung sein? Wenn es quakt wie eine Ente und schwimmt wie eine Ente, dann ist es eine Ente. Sie haben diesen Spruch schon einmal gehört – er wird von Verfechtern der dynamischen Programmierung seit Jahren immer wieder vorgetragen. Die Ente ist sinnbildlich ein Objekt, ein Element, mit dem anderer Code arbeitet, und an das dieser Code Erwartungen hat: quaken soll sie können, und schwimmen, und wenn das gegeben ist, dann ist alles in bester Ordnung. von Oliver Sturm Statische Typisierung – eine Evolution Im Mainstream der Softwareentwicklung, besonders im großen Bereich der Businesssoftware, ist man in Hinsicht auf Entenprobleme gern anderer Meinung. Statisch typisierte Sprachen haben in diesem Bereich lange dominiert, und diese Sprachen mögen eine Ente nur, wenn sie klar definiert ist. Gefieder- und Schnabelfarbe, Gewicht und Schwanzlänge müssen sorgsam in einem Objektmodell abgelegt und notwendige Schnittstellen wie ISchwimmfähigesTier definiert und implementiert werden. Irgendwann darf dann auch gequakt und geschwommen werden, aber nur unter penibler Kontrolle der Compiler- und Laufzeitsysteme. Viele Programmierer sind der Auffassung, dass diese statische Typisierung sich aus einem evolutionären Vorgang entwickelt hat und letztlich grundlegend „besser“ ist als eine offenere Anschauung der Entenproblematik. In einer bestimmten Gruppe von Sprachen ist diese Ansicht durchaus nachvollziehbar. In Sprachen wie C und später Pascal gab es zunächst nur eine trügerische Art von Typsicherheit: Es gab zwar Typen, aber es war sehr einfach, dem Compiler seinen Irrtum in Hinsicht auf eine bestimmte Typisierung mit einem simplen Cast verständlich zu machen. Alles basierte auf Zeigern, und letztlich zählte das Verständnis des Programmierers mehr als das des Compilers. www.basta.net 3 DOSSIER Datentypen wie die Unions in C und die varianten Records in Pascal machten sich dies schamlos zunutze, und große Speichereffizienz und Performance konnte auf dieser Basis erzielt werden – wenn auch mit beträchtlichem Risiko. In strikt objektorientierten Sprachen, zunächst in erster Linie Java, wurde Typen ein ganz neuer Stellenwert eingeräumt. Der Cast wurde vom Befehl zur Anfrage degradiert, und ohne Zustimmung von Compiler und Runtime konnte der Programmierer nicht mehr beliebig mit Typen umgehen. Drei Schritte wurden hier gemacht, um Typsicherheit zu erzwingen. Erstens musste der Programmierer nun sorgsam spezifische Typen definieren, und seinen Code so strukturieren, dass Typkonversionen außerhalb des Systems nicht mehr notwendig waren. Zweitens, und darauf aufbauend, konnten Compiler und Runtime die korrekte Verwendung dieser Typen erzwingen und so die gewünschte Sicherheit herbeiführen. Drittens war es notwendig, das Verständnis des Programmierers immer wieder durch die klare Nennung der Typbezeichnungen im Code zu versichern – eigentlich ein erheblicher Aufwand, aber im Vergleich zu komplexen Zeigern in C oder C++ sahen die Typnamen auf den ersten Blick so übersichtlich aus, dass man damit zunächst ziemlich glücklich war. In der .NET-Welt wurde die Idee der strikten Typisierung ebenfalls umgesetzt und tief in den Sprachen der Plattform verankert. C# unterstützt aus Kompatibilitätsgründen mit der Windows-Plattform auch Strukturen, die ein bestimmtes statisches Speichermapping anwenden. Mit dem var-Schlüsselwort in C# versuchte man, den Aufwand etwas zu mindern, der dem Programmierer durch die wiederholte Verwendung der Typnamen entsteht. Typherleitung ist in C# allerdings sehr rudimentär, und jeder verwendete Typ muss zumindest einmal beim Namen genannt werden. Mit einem anderen Schlüsselwort, dynamic, sollte eine Anbindung an dynamische Welten vereinfacht werden. Das machte C# zu einer relativ flexiblen statisch typisierten Sprache, schaffte aber auch Verwirrung bei Anwendern, die von der Microsoft-Welt klare Strategien erwarteten und gewohnt waren. .NET Framework & C# Basis von implementiertem Verhalten: Der Compiler leitet etwa automatisch her, dass Typen, mit denen Addition betrieben wird, zu einer gemeinsamen Typklasse gehören, die genau diese Fähigkeit verallgemeinert. Auf der .NET-Plattform bietet F# sehr gute Unterstützung für Typherleitung, die sich allerdings nicht in jedem Detail in den objektorientierten Bereich erstreckt und, mangels der entsprechenden Basis in der CLR, auch keine Typklassen kennt. Somit bleibt generell die Tatsache bestehen, dass statische Sprachen Mehraufwand für den Programmierer erzeugen, da kontinuierlich sorgfältige Pflege der Typen und aller Anwendungspunkte betrieben werden muss. Natürlich soll damit nicht gesagt sein, dass dem kein positiver Wert gegenübersteht, aber die Arbeit zur Pflege von strikt typisiertem Code kann oft beträchtlich sein. Dynamischer entwickeln Tatsache ist, dass die Welt der Programmierung heutzutage dynamischer ist denn je. Wenn vor einigen Jahren die Frage gestellt wurde, was für unmittelbare Vorteile die Verwendung dynamischer Sprachen vorzuweisen habe, dann wurden als Antwort gewöhnlich bestimmte Anwendungsfälle beschrieben. Ich selbst habe immer gern ein Szenario einer Tabellenkalkulationssoftware beschrieben, die in Python implementiert wurde, und ein unglaublich einfach verwendbares API hatte, basierend auf der Tatsache, dass mittels dynamischer Mechanismen extrem einfache Syntax für Zelladressierung möglich war. MySheet.K5 liest sich eben besser als MySheet.Cells[„K5“], um ein simples Beispiel zu nennen. Auch im Bereich von Object-relational Mapping gab es beeindruckende Beispiele von ähnlicher Natur. Heute hingegen ist Dynamik ein elementarer Bestandteil von Applikationsarchitekturen und Entwicklungsmethoden, und ihr muss deshalb eine wesentlich größere Bedeutung beigemessen werden. Typen ohne Aufwand Besuchen Sie auch folgende Session: Wie wäre es, wenn der Compiler mit Typen arbeiten und deren korrekte Verwendung garantieren könnte, ohne dass dem Programmierer dadurch syntaktischer Aufwand entsteht? Mithilfe extrem leistungsfähiger Typherleitung ist dies tatsächlich möglich, wie etwa das Typsystem der Sprache Haskell beweist. Wertzuweisungen müssen in Haskell nur in Konfliktfällen mit Typannotationen versehen werden, sodass der Code im Allgemeinen ganz ohne die Erwähnung expliziter Typen auskommt – der Compiler leitet die Typen her und prüft sie, aber der Programmierer wird damit nicht belastet. Was Haskell allerdings zusätzlich besonders interessant macht, ist die Fähigkeit zur automatischen Verallgemeinerung. Dies funktioniert sogar auf der Line-of-Business-Apps mit der Universal Windows Platform entwickeln www.basta.net Thomas Claudius Huber Zum Entwickeln von nativen Windows-Anwendungen stellt die Universal Windows Platform (UWP) nach der WPF die neuste Technologie dar. Doch inwiefern lässt sich die UWP zum Entwickeln von Business-Apps einsetzen? Wie sieht es mit den typischen Anforderungen aus – Datenbindung und MVVM-Unterstützung, Validierung etc.? In dieser Session erfahren Sie im LiveCoding, was die UWP heute zum Entwickeln einer klassischen Enterprise-App bietet und wohin die Reise geht. 4 DOSSIER Dynamische Sprachen im .NET-Umfeld In verteilten Systemen kommt es immer wieder vor, dass APIs externalisiert werden, die in der Vergangenheit elementarer Bestandteil des eigenen Systems waren. Das passiert zum Beispiel, wenn zur Datenablage oder zu anderen Zwecken Cloud-Dienste verwendet werden oder ähnliche Integrationen mit Diensten von Drittanbietern stattfinden. REST-Zugriff und Datenübertragung im JSON-Format führen in solchen Fällen leicht zu sehr dynamischen APIs, die der Programmierer nicht selbst beeinflussen kann, mit denen der Programmcode aber möglichst flexibel arbeiten muss, um dauerhaft stabil zu bleiben. Die Problematik ist bei der Nutzung von offensichtlich externen Diensten nicht neu. Allerdings bringt die Idee der Microservices dieselben Umstände direkt ins eigene Haus. Modularisierung wird bei Anwendung dieses Schemas zur Disziplin, und eine große Anzahl eigenständiger Dienste wird unabhängig von mehreren Teams gepflegt und sogar betrieben, um ein Anwendungssystem aufzubauen. Diese Dienste müssen sich natürlich austauschen, was gewöhnlich mit ähnlichen Mitteln geschieht wie bei der Einbindung externer Dienste. Daraus entsteht natürlich eine beeindruckende Konsistenz, aber auch die Notwendigkeit, dort dynamisch zu denken, wo es um die Schnittstellen zwischen einzelnen Komponenten geht. Auch Systeme zur bidirektionalen Kommunikation oder zum Message-basierten Austausch von Informationen sind gewöhnlich in diesem Sinne dynamisch. Letztlich halten dynamische Komponenten sogar im allgemeinen .NET-Umfeld Einzug, wie etwa in ASP.NET MVC, wo anonyme Objekte zur Konfiguration und zur Übertragung von Daten zwischen Views und Controllern seitens Microsoft favorisiert werden. Die Historie dynamischer Sprachen ist ebenso lang und interessant wie die der statisch typisierten. Lisp, eine der ältesten Programmiersprachen, ist dynamisch und bildet die Basis vieler anderer Sprachen. Clojure ist eine Lisp-basierte Sprache, die im Java-Umfeld sehr beliebt ist und sich auch auf .NET verbreitet. In Objective-C gibt es Message Passing in der Sprache selbst, wodurch Dynamik erreicht werden kann. Erlang ist dynamisch und wird immer wieder aufgrund seiner beeindruckenden Fähigkeiten in der Parallelisierung als Beispiel herangezogen, und das Schema der Aktoren, das in Erlang von großer Bedeutung ist, kann mittlerweile auf allen wichtigen Plattformen angewandt werden. In Python wurden und werden große Anwendungssysteme entwickelt, und dann gibt es natürlich JavaScript, dessen Bedeutung heute kaum überschätzt werden kann. Pflegeleichte Typsicherheit Der Vorteil dynamischer Sprachen in der beschriebenen dynamischen Umgebung besteht hauptsächlich darin, dass der Aufwand zur Pflege bestimmter Elemente einer Codebasis wesentlich niedriger ist als bei der Anwen- www.basta.net .NET Framework & C# dung statisch typisierter Sprachen. Wenn ein externes API geändert wird, kann der eigene Code womöglich ohne Änderungen weiter arbeiten, und wenn Änderungen notwendig werden, müssen sie an weniger Codestellen durchgeführt werden. Der Programmierer statisch typisierter Sprachen verlässt sich gern darauf, dass Compiler und Runtime einen Teil der Prüfungen durchführen, die sicherstellen, dass Programmcode in Hinsicht auf Datentypen korrekt ist. Daraus entsteht die größte Skepsis in Bezug auf dynamische Sprachen: Wie kann ein stabiles Ergebnis erzielt werden, wenn keine Typsicherheit besteht? Auf diese Frage gibt es drei wesentliche Antworten. Zunächst gibt es bei dynamischen APIs gewisse Patterns, die direkt der langfristigen Stabilität von Anwendungssystemen dienen. APIs können auf diese Weise versioniert werden, sodass Clients nicht sofort mit jeder Änderung kompatibel sein müssen. Natürlich wollen solche parallelen Versionen eines API gepflegt werden, und Regeln etabliert, um gleichzeitig den Anwendern des API Sicherheit zu geben und den Pflegeaufwand überschaubar zu halten. Zweitens verwenden auch dynamische Programmierer gern Softwarewerkzeuge, etwa zur statischen Codeanalyse, deren Mechanismen oft ähnliche Resultate erzielen können wie ein Compiler einer statisch typisierten Sprache. In diesen Bereich fällt auch Microsofts TypeScript, das mithilfe einer eigenen Syntax und eines Compilers eine gewisse Typsicherheit für JavaScript herbeiführt. Die dritte Antwort, und wohl die wichtigste, besteht aus der Erzeugung von automatisierten Tests. Aus Statistiken geht hervor, dass die Anzahl von Tests in Projekten auf Basis dynamischer Sprachen oft wesentlich größer ist als bei statisch typisierten. Im dynamischen Besuchen Sie auch folgende Session: Enterprise-Apps mit Xamarin und den Azure App Services entwickeln Jörg Neumann Die Anforderungen an eine Business-App steigen stetig. Sie soll auf verschiedenen Plattformen laufen, auch von unterwegs Zugriff auf Unternehmensdaten bieten und natürlich offlinefähig sein. Für solche Aufgaben bieten die Azure App Services elegante Lösungen. Sie ermöglichen eine einfache Bereitstellung von Backend-Services, die Anbindung an unterschiedliche Datenquellen und eine Integration ins Unternehmensnetz. Zudem werden verschiedene Varianten der Authentifizierung und der Versand von Push-Benachrichtungen geboten. Jörg Neumann zeigt Ihnen, wie Sie mit Xamarin und den Azure App Services Enterprise-taugliche Apps entwickeln und betreiben können. 5 DOSSIER Umfeld wird die Erzeugung von Tests gewissenhafter betrieben, und die Abdeckung bestimmter Szenarien ist größer, um Fehler aufgrund der falschen Verwendung von Typen auszuschließen. Gerade dieser letzte Teil wird von manchen Programmierern leider als Problem angesehen. Obwohl die Erzeugung einer möglichst vollständigen Testbasis seit Langem eine anerkannte Best Practice ist, hat diese sich in vielen Projekten und Entwicklerteams bisher nicht durchsetzen können. Oft wird als Grund der Aufwand genannt, der als zu hoch empfunden wird. Realistisch und korrekt ist dies meiner Erfahrung nach nicht. Selbst in bestehenden Projekten, die statische Typisierung verwenden, lohnt sich die Einführung einer guten Testbasis gewöhnlich schnell, da die Stabilität des Codes steigt, Fehlerbehebung sich vereinfacht und Regression ausgeschlossen werden kann. In modernen Anwendungssystemen, die mit der zuvor beschriebenen Dynamik der umgebenden Welt umgehen müssen, steht dem Aufwand der Testerzeugung außerdem der Pflegeaufwand statischer Typsysteme gegenüber, der unter Umständen sehr erheblich sein kann. Fazit Statisch und dynamisch typisierte Sprachen haben Vor- und Nachteile, wie auch jede einzelne Sprache im Vergleich mit jeder anderen. In der heutigen Zeit können Sie deutlich Zeit gewinnen und konzeptionell leistungsfähigere Anwendungen bauen, wenn Sie sich die Fähigkeiten dynamischer Sprachen zunutze machen www.basta.net .NET Framework & C# und die Ideen der Dynamik akzeptieren. Sie haben die Wahl, welche Methoden zur Absicherung Sie einsetzen möchten, aber letztlich hilft vor allem eine Maßgabe, die in der Softwareentwicklung eine der ältesten ist: testen, testen und nochmal testen! Besuchen Sie auch folgende Session: TypeScript für .NET-Entwickler Christian Wenz Mit TypeScript macht sich Microsoft daran, das – für viele Entwickler aus dem eigenen Kosmos – ungewohnte JavaScript zugänglicher zu machen, indem beispielsweise statisches Typing und bestimmte OOP-Features hinzugefügt werden. Nach einigen Anlaufschwierigkeiten hat TypeScript inzwischen auch außerhalb der MicrosoftWelt Traktion erhalten. Viel besser noch: Angular setzt auf TypeScript! Es ist also höchste Zeit, sich mit der Sprache zu beschäftigen. Diese Session stellt die Features von TypeScript vor und geht dabei auch auf die Toolunterstützung seitens Visual Studio und Co. ein. Damit sind Sie auch für die Entwicklung von Anwendungen auf Basis von Angular bestens gewappnet. 6 Agile & DevOps ©S&S Media, ©iStockphoto.com/danleap DOSSIER Test-driven Development (TDD) in MVVM-Apps ViewModels test­ getrieben entwickeln Einer der zentralen Vorteile des Model-View-ViewModel-Patterns (MVVM) ist es, dass sich ViewModels automatisiert testen lassen. Das ViewModel ist unabhängig vom UI und enthält nur die UI-Logik; eine Tatsache, die es zu einem idealen Kandidaten für Unit-Tests macht. Doch um ein testbares ViewModel zu erhalten, muss ein Entwickler ein paar Punkte beachten, insbesondere beim Laden von Daten oder beim Anzeigen von Dialogen. Worauf es ankommt und wie ViewModels testgetrieben entwickelt werden, zeigt dieser Artikel. Dabei werden das Unit-Testing-Framework xUnit, die Mocking Library Moq und das Dependency-Injection-Framework Autofac eingesetzt. von Thomas Claudius Huber „Ich habe genug zu tun, da hab‘ ich doch nicht noch Zeit, um Unit-Tests zu schreiben“. Diese Aussage hört man nur allzu oft von Entwicklern, insbesondere von jenen, die noch nie in einem testgetriebenen Projekt mitgearbeitet haben. Von Michael Feathers, Autor des erfolgreichen Buchs „Working with Legacy Code“, stammt folgendes Zitat: „Code ohne Tests ist schlechter Code“. Der Grund www.basta.net Hinweis In diesem Artikel wird auf xUnit gesetzt, aber wenn Sie MSTest oder NUnit nutzen, ist das auch in Ordnung. Wichtig ist allein die Tatsache, dass Sie überhaupt Unit-Tests schreiben, um wartbaren Code zu erhalten. Welches Unit-Testing-Framework Sie dafür einsetzen, ist zweitrangig. 7 DOSSIER Agile & DevOps Unit-Test entweder rot oder grün sein muss, also keine manuellen Schritte enthalten darf. Muss der Entwickler beispielsweise nach dem Test manuell prüfen, ob eine bestimmte Datei erstellt wurde, dann ist es kein UnitTest mehr, da er sich nicht selbst validiert. So viel zu den Regeln. Sind die Unit-Tests für den eigenen produktiven Code geschrieben, ergeben sich einige Vorteile: Abb. 1: Test-driven Development ist folgender: Wenn es Tests gibt, kann ein Entwickler das Verhalten des Codes sehr schnell und verifizierbar ändern, da er nach dem Ändern des Codes die Unit-Tests laufen lässt, um zu sehen, ob er nicht aus Versehen bestehende Logik zerstört hat. Ohne Tests kann der Entwickler nach dem Ändern des Codes niemals wissen, ob der Code jetzt besser oder schlechter ist. In diesem Artikel wird nur „guter Code“ geschrieben. Nach einem Blick auf Unit-Tests, Test-driven Development [1] und das xUnit-Framework zeigt der Artikel, wie testbare ViewModels geschrieben werden und wie sie sich testen lassen. Unit-Tests Ein Unit-Test ist ein automatisierter Test, der bekanntlich ein Stück produktiven Code testet. Ein Unit-Test erfüllt dabei verschiedene Eigenschaften, die unter dem Akronym F.I.R.S.T zusammengefasst sind [2]. UnitTests sind schnell (Fast), unabhängig voneinander (Isolates), wiederholbar in einer beliebigen Umgebung (Repeatable), selbstvalidierend (Self-validating) und werden zeitlich mit oder sogar vor dem produktiven Code geschrieben (Timely). Damit diese Eigenschaften eines Unit-Tests erfüllt sind, muss der produktive Code ein entsprechendes Design erfüllen. Beispielsweise muss es möglich sein, den Unit-Test in einer beliebigen Umgebung zu wiederholen (Repeatable): im Zug, zuhause, im Büro oder sogar auf dem Mond ohne Internet. Das bedeutet, dass beispielsweise ein Datenbankzugriff nicht im produktiven Code enthalten sein darf; er muss mit einem Interface abstrahiert werden. Dann wird im produktiven Code auf dieses Interface programmiert und eben nicht auf eine konkrete Implementierung, die auf die Datenbank zugreift. Im Unit-Test kommt dann eine Testimplementierung des Interface zum Einsatz, ein so genanntes Mock-Objekt. Dieses Mock-Objekt hat die Aufgabe, den Datenbankzugriff für den Test zu simulieren. Näheres dazu später beim Datenzugriff aus einem ViewModel. Das S im F.I.R.S.T-Akronym sagt, dass ein Unit-Test auch selbstvalidierend sein muss. Das bedeutet, dass ein www.basta.net • Es lassen sich Änderungen durchführen, ohne dass existierende Logik zerstört wird. • Gute Tests ergeben auch eine gute Dokumentation. • Das Schreiben von Unit-Tests kann das Design des produktiven Codes positiv beeinflussen. • Das Schreiben von Unit-Tests erfordert, dass man bereits vor der Implementierung stärker über das Problem nachdenkt. Doch läuft die Anwendung fehlerfrei und stabil, wenn alle Unit-Tests grün sind? Natürlich nicht, es braucht auch noch Integrationstests. Unit-Test vs. Integrationstest Unit-Tests allein sind kein Heilbringer. Viele grüne Unit-Tests bedeuten noch nicht, dass eine Anwendung danach keine Fehler mehr hat. Neben Unit-Tests, die nur ein Stück Code isoliert betrachten, gibt es viele andere Testarten. Eine sehr wichtige stellen die Integrationstests dar. Beim Integrationstest gibt es keine Mock-Objekte wie beim Unit-Test. Es wird beispielsweise auf eine Datenbank zugegriffen, es wird der richtige Web Service aufgerufen etc. Das heißt, beim Integrationstest geht es darum, wie die einzelnen getesteten Einheiten (Units) dann tatsächlich zusammenspielen. Zum Schreiben von Integrationstests lässt sich auch ein Unit-Testing-Framework wie xUnit einsetzen. Dabei werden im Test dann eben keine Mock-Objekte, sondern die richtigen Objekte verwendet – beispielsweise für den Datenbankzugriff. Besuchen Sie auch folgende Session: Git-Grundlagen für Entwickler Thomas Claudius Huber Git hat sich in den letzten Jahren zum Standard für die Sourcecodeverwaltung und -versionierung entwickelt. Doch immer noch fehlen Entwicklern die Grundlagen, um Git voll und ganz zu verstehen. Damit machen wir hier Schluss. In dieser Session lernen Sie die Basics von Git, um zu „fetchen“, „pullen“, „pushen“, „committen“ und natürlich auch, um mit Tags und Branches umzugehen. 8 DOSSIER Test-driven Development Test-driven Development (TDD) ist eine Vorgehensweise zum Schreiben von produktivem Code, die sich Unit-Testing zu Nutze macht. Dabei ist ein zentrales Prinzip, dass der Unit-Test vor und eben nicht mit oder nach dem produktiven Code geschrieben wird. Beim Einsatz von TDD befindet sich der Entwickler in einem ständigen Kreislauf der drei Phasen Red/Green/Refactor (Abb. 1), die in dieser Reihenfolge immer wieder durchlaufen werden: 1. Red: Es wird ein Unit-Test geschrieben, der fehlschlägt, da der produktive Code noch nicht implementiert wurde. 2. Green: Der produktive Code wird erstellt/angepasst. Und zwar lediglich so, damit der Unit-Test grün wird. 3. Refactor: Der produktive Code wird überarbeitet und strukturiert. Alle Unit-Tests müssen danach weiterhin grün sein. Ist der Entwickler zufrieden mit der Struktur, geht es weiter mit Schritt 1, indem der nächste Unit-Test geschrieben wird. Test-driven Development hat neben den Vorteilen des klassischen Unit-Testings weitere Vorteile: • Da der Test vor dem produktiven Code geschrieben wird, hat der produktive Code auf jeden Fall ein testbares Design. • Das Schreiben des Tests vor dem produktiven Code erfordert eine genaue Analyse der Anforderungen. • Die eigene Logik kann bereits fertiggestellt werden, auch wenn beispielsweise der dazu benötigte Datenbankserver noch nicht läuft. Insbesondere der letzte Punkt ist spannend: In größeren Teams können Entwickler ihren Teil bereits fertigstellen, auch wenn andere Teile der Software noch fehlen. Beispielsweise kann ein fehlender Datenbankzugriff in einem Unit-Test mit einem Mock-Objekt simuliert werden. Somit kann die Logik isoliert fertiggestellt werden, auch wenn der Datenbankzugriff noch nicht vorhanden ist. Es braucht dazu lediglich das entsprechende Interface, das später implementiert wird. Spikes in TDD Immer mit einem roten Unit-Test zu starten, ist in manchen Fällen etwas schwierig. Insbesondere, wenn sich ein Entwickler nicht sicher ist, ob der eingeschlagene Weg für Listing 1 ein größeres Feature public class MainViewModelTests auch funktionieren { wird – dann ist es bei [Fact] TDD natürlich unsinpublic void ShouldLoadFriends() nig, zuerst mit einem { Test zu starten. In ei} nem solchen Fall wird } ein so genannter Spike www.basta.net Agile & DevOps erstellt. Ein Spike ist ein Durchstich und ein Experiment; er stellt sicher, dass der eingeschlagene Weg funktioniert. Der in einem Spike geschriebene Code sollte nach Sicherstellung der Funktion laut Theorie wieder verworfen werden. Anschließend sollte es wieder TTD üblich nach Red/Green/Refactor weitergehen. In der Praxis sieht es meist so aus, dass erfahrene Entwickler Spikes schreiben, die danach auch zu 100 Prozent testbar sind, womit sie manchmal den klassischen TDD-Zyklus umgehen. Das Testing-Framework xUnit Es gibt viele interessante Unit-Testing-Frameworks. Mit Visual Studio wird das von Microsoft bereitgestellte MSTest installiert. MSTest ist mittlerweile – insbesondere im .NET-Core-Zeitalter – etwas in die Jahre gekommen. Microsoft arbeitet aktuell an einer Version 2 von MSTest, die allerdings noch nicht fertiggestellt ist. Neben MSTest ist das aus der Java-Welt von JUnit auf .NET portierte NUnit sehr beliebt. NUnit wurde mittlerweile komplett neu geschrieben, um die Vorteile von .NET auszunutzen. Einer der Entwickler von NUnit hat sich dazu entschlossen, xUnit zu erstellen, da ihm sowohl NUnit als auch MSTest zu schwergewichtig waren. xUnit ist heute ebenfalls ein sehr beliebtes Framework und derzeit auch die erste Wahl des Autors. Einige Gründe dafür sind: Besuchen Sie auch folgende Session: VS Team Services: Die fertige Teamumgebung von Microsoft in der Cloud Neno Loje Visual Studio Team Services ist Microsofts offene Plattform für Ihre gesamte Softwareentwicklung. Und die ist so vielseitig wie die Aufgabenstellungen, mit denen es Entwicklungsteams heutzutage zu tu bekommen. So werden Builds unter Linux und iOS oder Projekte in Java oder für mobile Plattformen wie Android und iOS ebenso unterstützt wie das moderne .NET Core. Die fertige Teamumgebung als Cloud-Lösung wird u. a. in Westeuropa gehostet und ist für kleine Teams (bis fünf Benutzer), für Stakeholder sowie MSDN-Abonnenten kostenlos. Der administrative Aufwand ist gering und gibt Ihrem Team Zeit und Raum, sich auf die eigentliche Arbeit zu fokussieren. In diesem Vortrag begeben wir uns gemeinsam auf einen Rundgang durch VS Team Services, klären, welches die Unterschiede zum klassischen Team Foundation Server (TFS) sind, welche Kosten entstehen und wie eine Migration vom TFS zu VSTS funktioniert. Wenn Sie den Aufwand für Ihre Entwicklungsumgebung minimieren wollen, um sich auf die essenziellen Aufgaben konzentrieren zu können, sind Sie in diesem Vortrag genau richtig. 9 DOSSIER Agile & DevOps Abb. 2: Die NuGet-Packages „xUnit“ und „xUnit.runner.visualstudio“ für ein Testprojekt mit xUnit • xUnit ist sehr schnell. • xUnit benötigt im Gegensatz zu MSTest kein spezifisches Testprojekt, stattdessen reicht eine einfache Klassenbibliothek aus. • xUnit und die für die Testausführung benötigten Klassen lassen sich einfach via NuGet installieren. • xUnit ist sehr leichtgewichtig: Die Testklassen selbst benötigen keine Attribute, sondern nur die Testmethoden. • Testmethoden lassen sich einfach parametrisieren. Ein Testprojekt mit xUnit anlegen Um mit xUnit ein Testprojekt anzulegen, wird eine Klassenbibliothek erstellt und die beiden NuGet-Packages „xUnit“ und „xUnit.runner.visualstudio“ hinzugefügt (Abb. 2). Wurden die NuGet-Packages hinzugefügt, wird noch eine Referenz auf das Projekt hinzugefügt, das die zu testenden Klassen enthält. In diesem Artikel Listing 2 public class MainViewModel:ViewModelBase { public MainViewModel() { Friends = new ObservableCollection<Friend>(); } public void Load() { var dataService = new FriendDataService(); var friends = dataService.GetFriends(); foreach (var friend in friends) { Friends.Add(friend); } } public ObservableCollection<Friend> Friends { get; private set; } } www.basta.net Abb. 3: Die Testmethode wird im Test-Explorer angezeigt wird eine Referenz auf ein kleines WPF-Projekt namens FriendStorage.UI hinzugefügt, das eine Main­ ViewModel-Klasse enthält. Zum Testprojekt wird eine neue Klasse mit dem Namen MainViewModelTests hinzugefügt, in der die Testmethoden für diese MainViewModel-Klasse untergebracht werden. Der erste Fall, der getestet werden soll, ist das Laden von Friend-Objekten. Somit wird eine ShouldLoad­ Friends-Methode hinzugefügt. Damit diese als Testmethode erkannt wird, wird sie mit dem Fact-Attribut aus dem Namespace XUnit markiert (Listing 1). Auch wenn die Methode noch leer ist, lässt sie sich bereits ausführen. Dazu wird in Visual Studio über das Hauptmenü Test | Windows | Test Explorer der Test-Explorer aufgerufen. Nach einem Klick auf Run all wird die Testmethode ausgeführt und als grün im Test-Explorer angezeigt (Abb. 3). Bei einem Blick auf den Test-Explorer fällt auf, dass der Testmethode der voll qualifizierte Klassenname vorangestellt wird. Das lässt sich anpassen, indem zum Testprojekt eine Konfigurationsdatei mit dem Namen xUnit.runner.json hinzugefügt wird. Diese Datei muss mit ins Ausgabeverzeichnis kopiert werden, um den Test-Runner von xUnit anzupassen. Mit dem folgenden Code werden im Test-Explorer die Testmethoden ohne den voll qualifizierten Klassennamen angezeigt: { "methodDisplay": "method" } Testbare ViewModels schreiben Ein eifriger Entwickler hat in der MainViewModel-Klasse bereits etwas Code in der Load-Methode hinzugefügt, um Friend-Objekte mithilfe einer FriendDataServiceKlasse zu laden und in einer ObservableCollection abzuspeichern (Listing 2). Genau diese Load-Methode der MainViewModelKlasse soll jetzt in der Testmethode ShouldLoadFriends getestet werden. Allerdings ist in Listing 2 ein Problem zu erkennen. Die Load-Methode instanziert einen FriendDataService. Dieser FriendDataService könnte 10 DOSSIER auf eine Datenbank, auf einen Web Service oder auf etwas anderes zugreifen. Das bedeutet, dass mit diesem FriendDataService ein Unit-Test nicht mehr in einer beliebigen Umgebung wiederholbar und es somit eben kein Unit-Test wäre. Und damit stellt sich die Frage, wie sich testbare ViewModels entwickeln lassen. Die Antwort ist recht banal: ViewModels werden testbar, indem Abhängigkeiten abstrahiert und ausgelagert werden. Diese Antwort gilt nicht nur für ViewModels, sondern auch für jede andere Klasse, die testbar sein soll. Im Fall des MainViewModels aus Listing 2 stellt der FriendDataService eine Abhängigkeit dar, die das Testen der Load-Methode nicht erlaubt. Woher soll ein Entwickler im Unit-Test wissen, wie viele FriendObjekte dieser FriendDataService zurückgibt? Wie soll das Ganze in einer beliebigen Umgebung auch ohne Datenzugriff getestet werden können? Es gibt nur eine Lösung: Der Datenzugriff muss aus der ViewModelKlasse abstrahiert werden. Den Datenzugriff abstrahieren Um den Datenzugriff aus der MainViewModel-Klasse zu abstrahieren, wird ein IFriendDataProvider-Interface eingeführt, das eine Methode LoadFriends wie folgt dargestellt definiert: public interface IFriendDataProvider { IEnumerable<Friend> LoadFriends(); } Agile & DevOps Das MainViewModel selbst wird angepasst. Im Konstruktor wird ein IFriendDataProvider-Objekt entgegengenommen und in der Instanzvariablen _dataProvider gespeichert. Anstelle der FriendDataAccess-Klasse wird in der Load-Methode des MainViewModels das IFriend­ Data­Provider-Objekt verwendet (Listing 3). Mit dem Verwenden des IFriendDataProvider-Inter­ face ist die Abhängigkeit zur FriendDataAccess-Klasse aus dem MainViewModel verschwunden. Die Load-Methode lässt sich jetzt einfach testen, indem der IFriend­ DataProvider gemockt wird. Den Datenzugriff von Hand „mocken“ Um die Load-Methode des MainViewModels zu testen, wird im Testprojekt eine neue Klasse namens FriendDa­ taProviderMock hinzugefügt, die das Interface IFriend­ DataProvider implementiert (Listing 4). Mit der FriendDataProviderMock-Klasse lässt sich in der ShouldLoadFriends-Methode eine neue Main­ ViewModel-Instanz erstellen (Listing 5). Anschließend wird auf dem MainViewModel die Load-Methode aufgerufen und mit der Assert-Klasse angenommen, dass die Friends Property des MainViewModels genau zwei Friend-Instanzen enthält. Das sind die zwei Instanzen, die vom FriendDataProviderMock-Objekt aus Listing 4 zurückgegeben werden. Wird der Test ausgeführt, ist er grün. Die händische Implementierung von IFriendDataProvider für den Test kann natürlich etwas aufwändig werden, wenn es vie- Listing 4 Listing 3 public class MainViewModel:ViewModelBase { private IFriendDataProvider _dataProvider; public MainViewModel(IFriendDataProvider dataProvider) { _dataProvider = dataProvider; Friends = new ObservableCollection<Friend>(); } public void Load() { var friends = _dataProvider.LoadFriends(); foreach (var friend in friends) { Friends.Add(friend); } public class FriendDataProviderMock : IFriendDataProvider { public IEnumerable<Friend> LoadFriends() { yield return new Friend { Id = 1, FirstName = "Thomas" }; yield return new Friend { Id = 2, FirstName = "Julia" }; } } Listing 5 public class MainViewModelTests { [Fact] public void ShouldLoadFriends() { var viewModel = new MainViewModel(new FriendDataProviderMock()); viewModel.Load(); } Assert.Equal(2, viewModel.Friends.Count); public ObservableCollection<Friend> Friends { get; private set; } } www.basta.net } } 11 DOSSIER le Interfaces und viele Tests gibt. In einem solchen Fall lohnt sich der Einsatz einer Mocking Library wie beispielsweise Moq. Moq einsetzen In Listing 4 wurde händisch eine FriendDataProvider­ Mock-Klasse für den Test erstellt. Diese Klasse kann sich ein Entwickler auch sparen, indem er die Moq Library einsetzt. Dazu einfach das NuGet-Package „Moq“ zum Testprojekt hinzufügen und wie in Listing 6 eine Instanz der generischen Mock-Klasse erzeugen. Als generischer Typ-Parameter wird dabei das Interface IFriendData­Provider angegeben. Auf der Mock-Instanz wird mit der Setup-Methode die LoadFriends-Methode des IFriendDataProvider aufgesetzt. Es wird eine Liste mit zwei Friend-Objekten als Return-Wert festgelegt. Hinter den Kulissen erzeugt die Mock-Klasse einen dynamischen Proxy für das IFriendDataProvider-Interface. Dieser dynamische Proxy wird mithilfe des Castle-Frameworks erstellt. Über die Object Property der Mock-Instanz lässt sich der dynamische Proxy abgreifen, der in Listing 6 eine IFriendDataProvider-Instanz darstellt – und diese lässt sich jetzt wunderbar zum Testen des MainViewModels verwenden. Listing 6 Agile & DevOps Dependency Injection mit Autofac Nachdem die Load-Methode des MainViewModels fertiggestellt und getestet ist, wird eine IFriendData­ Pro­vider-Implementierung erstellt, die zur Laufzeit der Anwendung genutzt wird. Sie greift intern in der LoadFriends-Methode beispielsweise auf den Friend­ DataService zu, der vorher direkt im MainViewModel verwendet wurde (Listing 7). Ist der FriendDataProvider fertig, wird noch der Konstruktor des MainWindow angepasst, damit dieser ein MainViewModel entgegennimmt und es im Data­ Context speichert. Findet das Loaded-Event auf dem MainWindow statt, wird auf dem MainViewModel die Load-Methode aufgerufen (Listing 8). Die WPF kann das MainWindow jetzt nicht mehr selbst instanziieren, da es keinen parameterlosen Konstruktor gibt. Daher wird in der Datei App.xaml die Start­ up­Uri Property entfernt und in der Codebehind-Datei die OnStartup-Methode überschrieben. Darin wird eine MainWindow-Instanz erstellt, indem dem Konstruktor ein MainViewModel übergeben wird. Dem Konstruktor des MainViewModels wiederum wird eine Friend­ Listing 8 public partial class MainWindow : Window { private readonly MainViewModel _viewModel; public void ShouldLoadFriends() { var dataProviderMock = new Mock<IFriendDataProvider>(); public MainWindow(MainViewModel viewModel) { _viewModel = viewModel; InitializeComponent(); this.Loaded += MainWindow_Loaded; DataContext = _viewModel; } dataProviderMock.Setup(dp => dp.LoadFriends()) .Returns(new List<Friend> { new Friend {Id = 1, FirstName = "Thomas"}, new Friend {Id = 1, FirstName = "Julia"} }); IFriendDataProvider dataProvider = dataProviderMock.Object; var viewModel = new MainViewModel(dataProvider); viewModel.Load(); Assert.Equal(2, viewModel.Friends.Count); } Listing 7 public class FriendDataProvider : IFriendDataProvider { public IEnumerable<Friend> LoadFriends() { var dataService = new FriendDataService(); return dataService.GetFriends(); } } www.basta.net private void MainWindow_Loaded(object sender, RoutedEventArgs e) { _viewModel.Load(); } } Listing 9 public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var mainWindow = new MainWindow( new MainViewModel( new FriendDataProvider())); mainWindow.Show(); } } 12 DOSSIER DataProvider-Instanz übergeben (Listing 9). Schließlich wird am Ende die Show-Methode aufgerufen, um das Fenster anzuzeigen. Der Code aus Listing 9 hat jetzt ein kleines Problem: Jedes Mal, wenn sich der Konstruktor des MainWin­ dow, des MainViewModels oder des FriendDataProvi­ ders ändert, muss diese Stelle entsprechend angepasst werden. Das lässt sich mit einem Dependency-Injection-Framework verhindern. Denn genau das passiert in Listing 9: In das MainWindow wird eine MainView­ Model-Abhängigkeit injiziert, in das MainViewModel wird ein IFriendDataProvider injiziert. Ein Dependency-Injection-Framework kann diese Injektionen selbst vornehmen, indem ihm mittgeteilt wird, welche konkreten Typen für welche abstrakten Typen injiziert werden sollen. Auf dem Markt gibt es zahlreiche dieser Frameworks, ein sehr beliebtes ist Autofac. Um es einzusetzen, wird im Projekt eine Bootstrapper-Klasse erstellt, die eine Bootstrap-Methode enthält, die wiederum einen IContainer zurückgibt. Der IContainer hat die Informationen über die zu erstellenden Typen. Zum Erstellen des IContainer kommt bei Autofac die in Listing 10 verwendete ContainerBuilder-Klasse zum Ein- Listing 10 public class Bootstrapper { public IContainer Bootstrap() { var builder = new ContainerBuilder(); builder.RegisterType<MainWindow>().AsSelf(); builder.RegisterType<MainViewModel>().AsSelf(); builder.RegisterType<FriendDataProvider>().As<IFriendDataProvider>(); return builder.Build(); } } Listing 11 Agile & DevOps satz. Auf einer ContainerBuilder-Instanz werden mit RegisterType verschiedene Typen registriert, am Ende wird mit der Build-Methode ein IContainer erstellt. Listing 10 zeigt, wie das MainWindow und das Main­ ViewModel mit der Methode AsSelf registriert werden. Das bedeutet, wo auch immer diese konkreten Typen benötigt werden, kann Autofac sie erstellen und/oder injizieren. Der Typ FriendDataProvider wird ebenfalls registriert. Mit der As-Methode wird festgelegt, dass eine FriendDataProvider-Instanz immer dann verwendet wird, wenn eine IFriendDataProvider-Instanz benötigt wird. Der erstellte Bootstrapper ist fertig und kann nun in der OnStartup-Methode der App-Klasse genutzt werden, um den IContainer zu erstellen und schließlich das MainWindow zu erzeugen (Listing 11). Was dabei jetzt auffällt, ist, dass kein Konstruktor mehr aufgerufen wird, um das MainWindow zu erzeugen. Stattdessen wird die Resolve-Methode verwendet – eine ExtensionMethode für den IContainer, die im Namespace Autofac untergebracht ist. Mit der Resolve-Methode wird eine neue MainWindow-Instanz erzeugt; wenn sich jetzt der Konstruktor ändert, müssen die Abhängigkeiten lediglich im Container registriert werden – was in der Bootstrapper-Klasse passiert. Eine Anpassung der OnStartup-Methode ist nicht mehr notwendig, da das Dependency-Injection-Framework den Rest übernimmt. Methodenaufrufe testen Wird das ViewModel erweitert, kommt man schnell an den Punkt, an dem Methodenaufrufe getestet Listing 12 public class MainViewModel:ViewModelBase { public MainViewModel(IFriendDataProvider dataProvider) { ... DeleteCommand = new DelegateCommand(OnDeleteExecute, OnDeleteCanExecute); } public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); public ICommand DeleteCommand { get; } private void OnDeleteExecute(object obj) { var friendToDelete = SelectedFriend; var bootstrapper = new Bootstrapper(); var container = bootstrapper.Bootstrap(); _dataProvider.DeleteFriend(friendToDelete.Id); Friends.Remove(friendToDelete); SelectedFriend = null; var mainWindow = container.Resolve<MainWindow>(); mainWindow.Show(); } ... } } www.basta.net } 13 DOSSIER werden müssen. Beispielsweise zeigt Listing 12 ein DeleteCommand im MainViewModel. In der OnDe­ lete­Exe­cute-Methode wird der selektierte Freund verwendet und dessen ID an die DeleteFriend-Methode des DataProviders übergeben. Anschließend wird das Friend-Objekt aus der Friends Collection entfernt und Listing 13 [Fact] public void ShouldDeleteAndRemoveFriendWhenDeleteCommandIsExecuted() { var dataProviderMock = new Mock<IFriendDataProvider>(); dataProviderMock.Setup(dp => dp.LoadFriends()) .Returns(new List<Friend> { new Friend {Id = 1, FirstName = "Thomas"}, new Friend {Id = 1, FirstName = "Julia"} }); IFriendDataProvider dataProvider = dataProviderMock.Object; var viewModel = new MainViewModel(dataProvider); viewModel.Load(); var friendToDelete = viewModel.Friends.First(); viewModel.SelectedFriend = friendToDelete; viewModel.DeleteCommand.Execute(null); dataProviderMock.Verify(dp => dp.DeleteFriend(friendToDelete.Id),Times. Once); Assert.False(viewModel.Friends.Contains(friendToDelete)); Assert.Null(viewModel.SelectedFriend); } Listing 14 private void OnDeleteExecute(object obj) { var result = MessageBox.Show( "Möchten Sie den Freund wirklich löschen?", "Löschen", MessageBoxButton.OKCancel); if (result == MessageBoxResult.OK) { var friendToDelete = SelectedFriend; Agile & DevOps die SelectedFriend Property auf null gesetzt. Doch wie lässt sich all das testen? Listing 13 zeigt eine Testmethode, die genau den Inhalt der OnDeleteExecute-Methode aus Listing 12 testet. Interessant sind dabei die letzten drei Anweisungen, nachdem das DeleteCommand mit der Exe­ cute-Methode ausgeführt wurde. Zuerst wird auf der in der Variablen dataProviderMock gespeicherten Mock-Instanz mithilfe der Verify-Methode geprüft, ob die DeleteFriend-Methode genau einmal aufgerufen wurde – und zwar genau mit der ID des zu löschenden Freundes (friendToDelete). Anschließend wird geprüft, ob der zu löschende Freund aus der Friends Collection entfernt und die SelectedFriend Property wieder auf null gesetzt wurde. Dialoge anzeigen Was beim Laden der Daten galt, um das ViewModel testbar zu gestalten, gilt auch für das Anzeigen von Dialogen: Abhängigkeiten müssen abstrahiert und aus dem ViewModel entfernt werden. Ein typischer Fall ist das Anzeigen einer Ok Cancel MessageBox beim Löschen eines Freundes. In Listing 12 wurde die OnDeleteExe­cute-Methode gezeigt. In diese Methode lässt sich eine MessageBox wie in Listing 14 integrieren; die Anwendung wird wunderbar funktionieren. Das Problem ist nur, dass die MessageBox auch in jedem Unit-Test angezeigt wird, in dem das DeleteCom­ mand ausgeführt wird; beispielsweise in dem Unit-Test aus Listing 13. Der Unit-Test ist somit blockiert – Visual Studio zeigt beim Ausführen des Unit-Tests die Listing 15 public class MainViewModel:ViewModelBase { private readonly IMessageBoxService _messageBoxService; ... public MainViewModel(IFriendDataProvider dataProvider, IMessageBoxService messageBoxService) { _messageBoxService = messageBoxService; ... } private void OnDeleteExecute(object obj) { var result = _messageBoxService.Show( "Möchten Sie den Freund wirklich löschen?", "Löschen", MessageBoxButton.OKCancel); _dataProvider.DeleteFriend(friendToDelete.Id); Friends.Remove(friendToDelete); SelectedFriend = null; } } www.basta.net if (result == MessageBoxResult.OK) { ... } } 14 DOSSIER MessageBox an. Ein Build-Server, der die Unit-Tests ausführt, wird einfach blockiert sein. Somit gilt auch an dieser Stelle: Die MessageBox muss aus dem ViewModel abstrahiert werden. Um die MessageBox zu abstrahieren, wird das Interface IMessageBoxService eingeführt: public interface IMessageBoxService { MessageBoxResult Show(string message, string title, MessageBoxButton buttons); } Das MainViewModel wird erweitert, ein neuer Konstruktorparameter vom Typ IMessageBoxService kommt hinzu. Die erhaltene Instanz wird in einer Instanzvariablen gespeichert und in der OnDeleteExecute-Methode verwendet, was in Listing 15 zu sehen ist. Mit der Abstraktion der MessageBox in einen IMes­ sageBoxService lässt sich dieser IMessageBoxService im Unit-Test jetzt wunderbar „mocken“. Beispielsweise zeigt das folgende Codebeispiel, wie das MainView­ Model mit einem IMessageBoxService erstellt wird. Dabei wird die Methode Show des IMessageBoxSer­ vice aufgesetzt: var messageBoxServiceMock = new Mock<IMessageBoxService>(); messageBoxServiceMock.Setup(m => m.Show(It.IsAny<string>(), It.IsAny<string>(), MessageBoxButton.OKCancel)) .Returns(MessageBoxResult.OK); var viewModel = new MainViewModel(dataProvider, messageBoxServiceMock. Object); Zu beachten ist der Einsatz der Klasse It, die auch zur Moq Library gehört. Mit der generischen It.IsAnyMethode wird festgelegt, dass dieses Set-up der IMes­ sageBoxService.Show-Methode für einen beliebigen String als ersten Parameter und einen beliebigen String als zweiten Parameter gilt. Als dritter Parameter muss MessageBoxButton.OKCancel angegeben werden, Listing 16 public IContainer Bootstrap() { var builder = new ContainerBuilder(); Agile & DevOps und dann wird das Ergebnis MessageBoxResult.OK zurückgegeben. Mit diesem Code wird im Unit-Test jetzt keine Mes­ sageBox mehr angezeigt. Für den produktiven Code ist noch eine Implementierung von ImessageBoxService notwendig. Das kann einfach wie folgt aussehen: public class MessageBoxService : IMessageBoxService { public MessageBoxResult Show(string message, string title, MessageBoxButton buttons) { return MessageBox.Show(message, title, buttons); } } Damit die Anwendung läuft, ist der MessageBoxSer­ vice lediglich noch im Container von Autofac zu registrieren, damit dieser als IMessageBoxService in das MainViewModel injiziert wird. Dazu wird in der Boots­ trap-Methode des Bootstrappers RegisterType auf dem ContainerBuilder aufgerufen (Listing 16). Einfach F5 drücken, und die Anwendung startet und läuft ohne weitere Anpassungen mit dem neuen Mes­ sageBoxService. Fazit Dieser Artikel hat gezeigt, wie sich testbare ViewModels erstellen lassen. Dabei ist das Prinzip ganz einfach: Abhängigkeiten werden mithilfe von Interfaces abstrahiert und als Konstruktorparameter definiert. Dadurch lassen sich ViewModels mit 100 Prozent Test-Coverage entwickeln. Allerdings sollte man dazu sagen, dass sich Entwickler auf die komplexen Teile konzentrieren sollten. Das Laden von Freunden und das Anzeigen einer Mes­sage­Box wurde in diesem Artikel beispielhaft aufgeführt. In der Praxis empfehlen sich immer diese Stellen für Unit-Tests, an denen man als Entwickler darüber nachdenkt, doch den einen oder anderen Kommentar zu schreiben. Das Offensichtliche zu testen, ist nicht immer lohnenswert, aber das Herzstück zu testen, ist sehr zu empfehlen. Thomas Claudius Huber arbeitet als Principal Consultant in Basel bei der Trivadis AG. Er ist Microsoft MVP für Windows Development und Autor zahlreicher Bücher, Artikel und Pluralsight-Videos. Er spricht regelmäßig auf großen Entwicklerkonferenzen wie der BASTA! und freut sich immer über Feedback. www.thomasclaudiushuber.com builder.RegisterType<MessageBoxService>().As<IMessageBoxService>(); builder.RegisterType<MainWindow>().AsSelf(); builder.RegisterType<MainViewModel>().AsSelf(); builder.RegisterType<FriendDataProvider>().As<IFriendDataProvider>(); return builder.Build(); } www.basta.net Links & Literatur [1] TDD: https://de.wikipedia.org/wiki/Testgetriebene_Entwicklung [2] F.I.R.S.T.: http://agileinaflash.blogspot.de/2009/02/first.html [3] TDD und MVVM: https://www.pluralsight.com/courses/wpf-mvvm-testdriven-development-viewmodels 15 Agile & DevOps ©iStockphoto.com/stask DOSSIER Verschiedene Erweiterungs- und Anpassungsmöglichkeiten VSTS/TFS – ganz nach meinem Geschmack Visual Studio Team Services bzw. der Team Foundation Server sind mittlerweile sehr offene und interoperable Application-Lifecycle-Management-Produkte aus dem Haus Microsoft. Seit der anfänglichen Anpassbarkeit von Work-Item-Typen wurde der Funktionsumfang in Sachen Integration und Erweiterbarkeit stark vergrößert. Seit Kurzem kann auch die Web­oberfläche nach Belieben erweitert werden. Mit Update 3 können nun auch für den Team Foundation Server 2015 eigene Dashboard-Widgets erstellt werden. Es ist also an der Zeit, einen Blick auf die neuen Möglichkeiten zu werfen. von Marc Müller Ein anpassbares und erweiterbares ALM-Tool zu haben, ist heutzutage für den Erfolg von wesentlicher Bedeutung. Die Möglichkeiten, Work-Item-Typen zu erweitern oder gar neue Entitäten zu erstellen, kennt vermutlich jeder. Dieser Artikel legt den Fokus auf die Anpassung der Weboberfläche sowie auf die Integration von eigenen Services in Visual Studio Team Services (VSTS) und Team Foundation Server (TFS). Abbildung 1 zeigt, welche Integrationspunkte zur Verfügung stehen. www.basta.net Wie oft hatte man sich in der Vergangenheit gewünscht, an einer bestimmten Stelle einen zusätzlichen Button hinzuzufügen oder gar eigene Views einzublenden. Diese Möglichkeit ist mit der aktuellen Version nun gegeben. Die Erweiterungen in der Oberfläche enthalten Code in Form von JavaScript zur Interaktion mit dem System, die Visualisierung wird mittels HTML implementiert. Eigene Backend-Services können damit aber nicht integriert werden. Sobald Servicelogik erforderlich ist, kann dies nur als externer Service implementiert werden, also z. B. als eigenständige Webapplikation. 16 DOSSIER Diese Applikationen können über die REST-Schnittstelle mit dem VSTS bzw. TFS interagieren. Zur Benachrichtigung der externen Applikation stehen so genannte WebHooks zur Verfügung. Zu guter Letzt können dem TFS bzw. VSTS noch eigene Build-Tasks über die ExtensibilitySchnittstelle zur Verfügung gestellt werden. Weitere Details zu den einzelnen Erweiterungspunkten sind auf der Extensibility-Webseite von VSTS [1] zu finden. Neben einer detaillierten Auflistung werden entsprechende Beispiele [2] zur Verfügung gestellt. Agile & DevOps Abb. 1: Übersicht über die Integrationspunkte von Extension in VSTS/TFS Eigenes Dashboard-Widget erstellen In diesem Artikel wird die Extensibility-Schnittstelle anhand eines eigenen Dashboard-Widgets erklärt. Um die Komplexität klein zu halten, wollen wir ein einfaches Widget in Form einer Build-Ampel erstellen (Abb. 2). Die Ampel kommuniziert mit dem Build-System über ein REST-API, um den aktuellen Status periodisch zu erfragen. Da wir für verschiedene Build-Definitionen verschiedene Ampeln auf dem Dashboard positionieren möchten, benötigen wir ebenfalls eine Konfigurationsmöglichkeit für den Nutzer, in der er die Größe des Widgets sowie die gewünschte Build-Definition auswählen kann. UI-Extensions bestehen primär aus HTML und JavaScript und den üblichen Webartefakten wie Bildern und CSS-Dateien. Damit VSTS respektive TFS weiß, was wie und wo intergiert werden muss, beinhaltet jede Extension ein Manifest. Das Manifest, das als JSON-Datei definiert wird, beinhaltet alle Informationen, Dateispezifikationen sowie Integrationspunkte. Dazu kommt die Definition der Scopes, also die Definition der Zugriffsrechte der Erweiterung. Vor der Installation wird der Nutzer darüber informiert, auf was die Erweiterung Zugriff wünscht, und Abb. 2: Build-Traffic-Lights-Widget als Beispiel für diesen Artikel muss dies entsprechend bestätigen. Ist eine Erweiterung einmal ausgerollt, kann der Scope nicht mehr verändert werden. So wird verhindert, dass durch ein Update plötzlich mehr Berechtigungen als einmal bestätigt eingefordert werden können. Generell wird die Sicherheit großgeschrieben. Eine UI-Erweiterung läuft immer „sandboxed“ in einem dedizierten iFrame und erhält über ein SDK Zugriff auf den Listing 1: Auszug aus dem Manifest { "manifestVersion": 1, "id": "BuildTrafficLights", "version": "0.1.16", "name": "Build Traffic Lights", "scopes": [ "vso.build_execute" ], ... "contributions": [ { "id": "BuildTrafficLightsWidget", "type": "ms.vss-dashboards-web. widget", "targets": [ "ms.vss-dashboards-web.widget- www.basta.net catalog", "4tecture.BuildTrafficLights. BuildTrafficLightsWidget.Configuration" ], "properties": { "name": "Build Traffic Lights Widgets", "uri": "TrafficLightsWidget.html", ... "supportedScopes": [ "project_team" ]}}, { "id": "BuildTrafficLightsWidget. Configuration", "type": "ms.vss-dashboards-web.widgetconfiguration", "targets": [ "ms.vss-dashboards-web. widget-configuration" ], "properties": { "name": "Build Traffic Lights Widget Configuration", "description": "Configures Build Traffic Lights Widget", "uri": "TrafficLightsWidgetConfiguration. html" }}]} 17 DOSSIER VSTS/TFS – samt Services und Events. Sorgen wegen der Authentifizierung muss man sich bei der Verwendung des SDK keine machen, da der aktuelle Nutzerkontext übernommen wird. Jeder API-Call wird gegenüber den definierten und durch den Nutzer bestätigten Scopes geprüft und bei einem Fehlverhalten blockiert. Damit alle Artefakte sowie das Manifest ausgeliefert und installiert werden können, erstellen wir als Artefakt ein Paket in Form einer VSIX-Datei. Diese kann manuell auf den TFS geladen oder über den VSTS Marketplace vertrieben werden. Listing 2: „TrafficLightsWidget.html“ <body> <script type="text/javascript"> // Initialize the VSS sdk VSS.init({ explicitNotifyLoaded: true, setupModuleLoader: true, moduleLoaderConfig: { paths: { "Scripts": "scripts" } }, usePlatformScripts: true, usePlatformStyles: true }); // Wait for the SDK to be initialized VSS.ready(function () { require(["Scripts/TrafficLightsWidget"], function (tlwidget) { }); }); </script> <div class="widget" > <h2 class="title" id="buildDefinitionTitle"></h2> <div class="content" id="content"> </div> </div> </body> Agile & DevOps Grundsätzlich können wir die Erweiterung mit jedem Texteditor erstellen, jedoch wollen wir auch in diesem kleinen Beispiel den Entwicklungsprozess gleich sauber aufgleisen und verwenden hierzu Visual Studio. Für die Libraries und Zusatztools verwenden wir den Node Package Manager (npm). Damit wir die Paketerstellung automatisieren können, kommt Grunt als Task-Runner zum Einsatz. Wer das alles nicht von Hand aufbauen möchte, kann die Visual-Studio-Erweiterung VSTS Extension Project Templates [3] installieren. Hiermit verfügt Vi­ sual Studio über einen neuen Projekttyp, der das Erweiterungsprojekt mit allen Abhängigkeiten und Tools korrekt aufsetzt. Zudem möchten wir in unserem Beispiel nicht direkt JavaScript-Code schreiben, sondern TypeScript Listing 4: „TrafficLightsCollection.ts“ /// <reference path='../node_modules/vss-web-extension-sdk/typings/ /// VSS.d.ts' /> import Contracts = require("TFS/Build/Contracts"); import BuildRestClient = require("TFS/Build/RestClient"); export class TrafficLightsCollection { ... constructor(projectname: string, builddefinition: number, numberofbuilds: number, element: HTMLElement) { ... } public updateBuildState() { var buildClient = BuildRestClient.getClient(); buildClient.getBuilds(this.projectname, [this.buildDefinitionId, ..., this.numberOfBuilds).then((buildResults: Contracts.Build[]) => { this.builds = buildResults; this.renderLights(); }); }, err => { this.renderLights(); }); } } Listing 3: „TrafficLightsWidget.ts“ /// <reference path='../node_modules/vss-web-extension-sdk/typings/ /// VSS.d.ts' /> /// <reference path="trafficlightscollection.ts" /> import TrafficLights = require("scripts/TrafficLightsCollection"); function GetSettings(widgetSettings) {...} function RenderTrafficLights(WidgetHelpers, widgetSettings) { var numberOfBuilds = <number>widgetSettings.size.columnSpan; var config = GetSettings(widgetSettings); if (config != null) { var trafficLights = new TrafficLights.TrafficLightsCollection( VSS.getWebContext().project.name, config.buildDefinition, numberOfBuilds, document.getElementById("content")); } else {...} www.basta.net } VSS.require("TFS/Dashboards/WidgetHelpers", function (WidgetHelpers) { WidgetHelpers.IncludeWidgetStyles(); VSS.register("BuildTrafficLightsWidget", function () { return { load: function (widgetSettings) { RenderTrafficLights(WidgetHelpers, widgetSettings); return WidgetHelpers.WidgetStatusHelper.Success(); }, reload: function (widgetSettings) {...} }; }); VSS.notifyLoadSucceeded(); }); 18 DOSSIER Abb. 3: Projektstruktur des Build-Traffic-Lights-Widgets zur Implementierung verwenden. Abbildung 3 zeigt die Projektstruktur in Visual Studio für unsere Erweiterung. Unsere Manifestdatei befüllen wir mit den üblichen Informationen zum Autor sowie der Beschreibung der Erweiterung. Von entscheidender Bedeutung sind der Scope für die Berechtigungen sowie die Contribution Points, in denen die eigentliche Integration beschrieben wird. Da wir auf Build-Informationen lesend zugreifen möchten, wählen wir den Scope vso.build_execute. Als Contribution Points definieren wir zwei UI-Elemente: ein Widget-UI sowie ein Konfigurations-UI. In Listing 1 sind die entsprechenden Stellen fett hervorgehoben. Aufgrund des Manifests wird unsere Erweiterung richtig im Widgetkatalog aufgeführt, und aufgrund der Contribution Points werden die richtigen HTML-Dateien für die UI-Erweiterungen angezogen. Die Implementierung der Views ist nichts anderes als klassische Webentwicklung. Jedoch ist die Integration der Komponente über das zur Verfügung gestellte SDK von entscheidender Bedeutung. Das SDK liegt uns in Form einer JavaScript-Datei (VSS.SDK.js) in unserem Projekt vor. Da wir mit TypeScript arbeiten, laden wir ebenso die dazugehörigen Typinformationen. Nach der Referenzierung der Skriptdatei in unserer HTML-Datei können wir nun über das VSS-Objekt die Erweiterung registrieren. Zudem geben wir VSTS/TFS bekannt, wie die Runtime unsere JavaScript-Module finden und laden kann. In unserem Fall ist der gesamte Code in TypeScript implementiert und wird als AMD-Modul zur Verfügung gestellt. Da wir unsere Build-Ampel dynamisch in unserem TypeScript- www.basta.net Agile & DevOps Code aufbauen werden, enthält die HTML-Datei nur das Grundgerüst des Widgets (Listing 2). Damit unser Widget und seine Controls nicht vom Look and Feel von VSTS/FS abweichen, gibt uns das SDK die Möglichkeit, über WidgetHelpers.Include­ WidgetStyles() die Styles von VSTS/TFS zu laden und in unserem Widget zu verwenden. Das erspart uns einiges an Designarbeit. Listing 3 zeigt zudem das Auslesen der Konfiguration sowie der Instanziierung unseres AmpelRenderers TrafficLightsCollection. Um nun mit dem REST-API von VSTS/TFS zu kommunizieren, können wir über das SDK einen entsprechenden Client instanziieren. Der Vorteil dieser Vorgehensweise liegt darin, dass wir uns nicht über die Authentifizierung kümmern müssen. Die REST-Aufrufe erfolgen automatisch im Sicherheitskontext des aktuellen Nutzers. In Listing 4 ist der entsprechende Auszug zu sehen. Was jetzt noch fehlt, ist die Möglichkeit, unser Widget zu konfigurieren (Listing 5). Jedes Ampel-Widget benötigt eine entsprechende Konfiguration, in der die gewünschte Build-Definition ausgewählt und gespeichert wird. Die Grunddefinition der Konfigurationsansicht (HTML-Datei) verhält sich genauso wie die beim Widget. Im Code für die Konfigurationsansicht müssen zwei Lifecycle-Events überschrieben werden: load und onSave(). Hier können wir über das SDK unsere spezifischen Einstellungswerte als JSON-Definition laden und speichern. Damit die Livepreview im Konfigurationsmodus funktioniert, müssen entsprechende Änderungsevents publiziert werden. Die aktuellen Konfigurationswerte werden mittels TypeScript mit den UI-Controls synchron gehalten. Ein komplettes Code-Listing wäre für diesen Artikel zu umfangreich. Entsprechend steht der gesamte Code dieser Beispiel-Extension auf GitHub [4] zur Verfügung. Besuchen Sie auch folgende Session: TFS/VSTS mit Extensions an die eigenen Bedürfnisse anpassen Marc Müller Seit der anfänglichen Anpassbarkeit von Work-Item-Typen wurde der Funktionsumfang in Sachen Integration und Erweiterbarkeit für TFS und VSTS stark vergrößert. TFS bzw. VSTS können über Extension in den Bereichen Web UI, Dashboard Widgets und Build Tasks einfach erweitert werden. Nebst den Extensions bieten RESTAPI und Service Hooks ideale Möglichkeiten, um Drittkomponenten einfach in das ALM-Tool zu integrieren. In unserem Vortrag zeigen wir Ihnen, wie eigene Extensions erstellt werden und was der TFS in Sachen Erweiterbarkeit und Integration zu bieten hat. 19 DOSSIER Abb. 4: Grunt-Task zur Erstellung des Pakets Entwicklungsworkflow von Extensions Wer Extensions entwickelt, setzt sich primär mit klassischen Webentwicklungstechnologien auseinander. Doch wie bekommen wir nun die Extension in den TFS oder VSTS, und wie können wir debuggen? Als Erstes benötigen wir ein Extension Package in Form einer VSIX-2.0-Datei. Diese können wir anhand des Manifests und dem Kommandozeilentool tfx-cli erstellen. Das CLI wird als npm-Paket angeboten und entsprechend in unserem Projekt geladen. Wie bereits weiter oben erwähnt, möchten wir diesen Schritt als Grunt-Task in den Visual Studio Build integrieren. Wie in Abbildung 4 zu sehen ist, verknüpfen wir den Agile & DevOps Grunt-Task mit dem After-Build-Event von Visual Studio. Somit erhalten wir nach jedem Build das entsprechende Package. Der nächste Schritt ist das Publizieren in TFS oder VSTS. Im Fall von TFS wird die VSIX-Datei über den Extension-Manager hochgeladen und in der gewünschten Team Project Collection installiert. Soll die Extension in VSTS getestet werden, ist das Publizieren im Marketplace notwendig. Keine Angst, auch hier können die Extensions zuerst getestet werden, bevor sie der Allgemeinheit zugänglich gemacht werden. Extensions können außerdem als private Erweiterungen markiert werden. Somit stehen sie nur ausgewählten VSTS-Accounts zur Verfügung. Nun ist die Extension verfügbar und kann getestet werden. Hierzu wenden wir die Entwicklertools der gängigen Browser an. Die Extension läuft in einem ­iFrame; entsprechend kann die JavaScript-Konsole auf den gewünschten iFrame gefiltert, Elemente inspiziert und der TypeScript- bzw. JavaScript-Code mit dem Debugger analysiert werden. Einbindung von externen Services mit REST-API und WebHooks Die aktuelle Erweiterungsschnittstelle von VSTS/TFS erlaubt nur Frontend-Erweiterungen. Eigene Services oder Backend-Logik können nicht integriert werden. Entsprechend muss diese Art von Erweiterung extern als Windows-Service oder Webapplikation betrieben bzw. gehostet werden. VSTS und TFS stellen ein Listing 5: Widget-Konfiguration if (config.buildDefinition != null) { this.selectBuildDefinition.value = config.buildDefinition; } import BuildRestClient = require("TFS/Build/RestClient"); import Contracts = require("TFS/Build/Contracts"); } export class TrafficLightsWidgetConfiguration { ... constructor(public WidgetHelpers) { } public load(widgetSettings, widgetConfigurationContext) { this.widgetConfigurationContext = widgetConfigurationContext; this.initializeOptions(widgetSettings); this.selectBuildDefinition.addEventListener( "change", () => { this.widgetConfigurationContext .notify(this.WidgetHelpers.WidgetEvent.ConfigurationChange, this.WidgetHelpers.WidgetEvent.Args(this.getCustomSettings())); }); ... return this.WidgetHelpers.WidgetStatusHelper.Success(); } public initializeOptions(widgetSettings) { ... var config = JSON.parse(widgetSettings.customSettings.data); if (config != null) { www.basta.net } public onSave() { var customSettings = this.getCustomSettings(); return this.WidgetHelpers.WidgetConfigurationSave .Valid(customSettings); } } VSS.require(["TFS/Dashboards/WidgetHelpers"], (WidgetHelpers) => { WidgetHelpers.IncludeWidgetConfigurationStyles(); VSS.register("BuildTrafficLightsWidget.Configuration", () => { var configuration = new TrafficLightsWidgetConfiguration(WidgetHelpers); return configuration; }) VSS.notifyLoadSucceeded(); }); 20 DOSSIER Abb. 5: Registrierung des WebHook-Receivers in VSTS REST-API [5] zur Verfügung, mit dem fast alle Aktionen getätigt werden können. Entwickelt man mit .NET, so stehen entsprechende NuGet-Pakete zur Verfügung. Oft bedingen eigene Logikerweiterungen aber, dass man auf Ereignisse in VSTS/TFS reagieren kann. Ein Polling über das API wäre ein schlechter Ansatz. Entsprechend wurde in VSTS/TFS das Konzept von WebHooks implementiert. Über die Einstellungen unter Service Hooks können eigene WebHook-Empfänger registriert werden. Jede WebHook-Registrierung ist an ein bestimmtes Event in VSTS/TFS gebunden und wird bei dessen Eintreten ausgelöst. Der WebHook-Empfänger ist an und für sich relativ einfach zu implementieren. VSTS/TFS führt einen POST-Request an den definierten Empfänger durch, und der Body beinhaltet alle Eventdaten, wie zum Beispiel die Work-Item-Daten des geänderten Work Items bei einem Work-Item-Update-Event. Einen POST-Request abzuarbeiten, ist nicht weiter schwierig, jedoch ist es mit einem gewissen Zeitaufwand verbunden, die Routen zu konfigurieren und den Payload richtig zu parsen. Zum Glück greift uns Microsoft ein wenig Agile & DevOps unter die Arme. Für ASP.NET gibt es ein WebHookFramework [6], das eine Basisimplementierung für das Senden und Empfangen von WebHooks bereitstellt. Zudem ist ebenfalls eine Implementierung für VSTSWebHook-Events verfügbar, was einem das gesamte Parsen des Payloads abnimmt. Nach dem Erstellen eines Web-API-Projekts muss lediglich das NuGet-Paket Microsoft.AspNet.WebHooks.Receivers.VSTS [7] hinzugefügt werden. Zu beachten ist hierbei, dass das Framework sowie die Pakete noch im RC-Status sind und entsprechend über NuGet als Pre-Release geladen werden müssen. Um auf VSTS/TFS-Events reagieren zu können, muss von der entsprechende Basisklasse abgeleitet und die gewünschte Methode überschrieben werden (Listing 6). Das WebHooks-Framework generiert über die Konfiguration entsprechende Web-API-Routen. Damit nicht jeder einen Post auf den WebHook-Receiver absetzen kann, wird ein Sender anhand einer ID Identifiziert, die in der Web.config-Datei hinterlegt wird (Abb. 5). Der entsprechende URL muss dann noch im VSTS/TFS hinterlegt werden. Fazit Die hier gezeigte UI-Integration bietet weitere Möglichkeiten, den eigenen Prozess bis in das Standard-Tooling zu integrieren. Somit sind nahezu grenzenlose Möglichkeiten für eine optimale Usability und Unterstützung der Nutzer gegeben. Wer sich mit HTML, CSS und JavaScript/TypeScript auskennt, dem dürfte das Erstellen eigener Extensions nicht allzu schwer fallen. Für Nutzer können die neuen Funktionen nur Vorteile haben, zumal die Werbefläche von VSTS/TFS fast nicht mehr verlassen werden muss. Marc Müller arbeitet als Principal Consultant für Microsoft ALM sowie .NET-/Windows-Azure-Lösungen bei der 4tecture GmbH und wurde von Microsoft als Most Valuable Professional (MVP) für Visual Studio ALM ausgezeichnet. Sein ALM-Fachwissen sowie Know-how für Enterprise-Architekturen und komponentenbasierte verteilte Systeme konnte er in den letzten Jahren in viele Projekte einbringen. Als Trainer und Referent zählen die Ausbildung und das Coaching von ALM- und .NET-Projektteams zu seinen Schwerpunkten. Listing 6 public class VstsWebHookHandler : VstsWebHookHandlerBase { public override Task ExecuteAsync(WebHookHandlerContext context, WorkItemUpdatedPayload payload) { // sample check if (payload.Resource.Fields.SystemState.OldValue == "New" && payload.Resource.Fields.SystemState.NewValue == "Approved") { // your logic goes here... } return Task.FromResult(true); } } www.basta.net Links & Literatur [1]https://www.visualstudio.com/en-us/docs/integrate/extensions/ overview [2]https://github.com/Microsoft/vsts-extension-samples [3]https://visualstudiogallery.msdn.microsoft.com/a00f6cfc-4dbb-4b9aa1b8-4d24bf46770b?SRC=VSIDE [4]https://github.com/4tecture/BuildTrafficLights [5]https://www.visualstudio.com/en-us/docs/integrate/api/overview [6]https://github.com/aspnet/WebHooks [7]https://www.nuget.org/packages/Microsoft.AspNet.WebHooks. Receivers.VSTS 21 DOSSIER Web Development Nutzung von echtem Server-Push mit dem WebSocket-Protokoll Echtzeitchat mit Node.js und Socket.IO © S&S Media Was wäre heute eine Anwendung ohne die bekannten In-App Notifications? Neue Chatnachricht, neuer Artikel oder im Allgemeinen eine neue Benachrichtigung – oft symbolisiert durch ein kleines Icon mit einer Zahl zur Anzeige, wie viele neue Nachrichten uns erwarten. Kaum mehr wegzudenken und ständig verfügbar, auch ohne dass man eine Seite neu laden muss. Und das Ganze natürlich in Echtzeit. Wie das geht? WebSocket für Echtzeitkommunikation lautet das Zauberwort. von Manuel Rauber In der heutigen Zeit haben wir alle sicher schon mal ein Real-Time-Collaboration-Tool genutzt, ein Werkzeug, um in Echtzeit gemeinsamen mit anderen Personen ein Dokument zu editieren. Als Paradebeispiel sei hier Google Docs genannt, aber mittlerweile auch OfficeWeb-Apps wie Word Online. Je mehr Leute dabei sind, umso mehr bunte Marker springen munter in einem Dokument herum, fügen neue Texte ein oder ändern bestehende. Das alles passiert in Echtzeit. www.basta.net Prinzipiell gibt es verschiedene Möglichkeiten, das gemeinsame Editieren von Dokumenten zu ermöglichen. Die erste Möglichkeit ist ein typisches Intervallpolling, also das Senden einer Anfrage mit der Bitte um Aktualisierung zum Server in einem bestimmten Intervall. Das heißt, dass wir beispielsweise alle 10 Sekunden eine HTTP-­Anfrage zum Server schicken und als Antwort die Änderungen oder das komplette Dokument bekommen. Wirklich Echtzeit ist dies aber nicht, da wir bis zu 10 Sekunden (plus die Zeit zum Verbindungsaufbau und Übertragen der Daten) warten müssen, um neue 22 DOSSIER Video-Link: Chris Heilmann im Interview zum Edge-Browser auf der BASTA! Spring Web Development es sich allerdings nicht um HTTP-Anfragen, d. h. bei der Übermittlung der Daten müssen keine zusätzlichen Header mitgesendet werden. Die Datenpakete sind daher deutlich kleiner und beinhalten genau das, was gesendet wird, also genau unsere Änderungen im gemeinsamen Dokument. Darf ich Ihnen die Hand geben? Änderungen zu erhalten. Das Chaos, das hier in einem gemeinsamen Dokument entstehen würde, kann man sich sicherlich denken. Spaß sieht anders aus. Die zweite Möglichkeit ist die Nutzung von HTTP Long Polling. Hier schickt der Client eine Anfrage zum Server. Der Server hält die Verbindung so lange offen, bis tatsächlich eine Nachricht eintrifft, und beantwortet die Anfrage. Ist dies geschehen, baut der Client sofort wieder eine neue Verbindung zum Server auf, bis die nächste Nachricht zur Verfügung steht. Die Nachrichten stehen so schneller als in der ersten Variante zur Verfügung, das Ganze ist aber ineffizient, da mit jeder Anfrage und Antwort HTTP-Header übertragen werden. Auch hier kann man sich vorstellen, dass das für das Editieren eines Dokuments nicht wirklich sinnvoll geeignet ist, wenn viele Änderungen vorgenommen werden. Mit großer Wahrscheinlichkeit sind die eigentlichen Änderungen kleiner als der Overhead durch die HTTP-Header. Die dritte Möglichkeit ist die Nutzung des WebSocket-Protokolls. Dabei handelt es sich um ein Netzwerkprotokoll, das auf TCP aufsetzt, um eine bidirektionale Verbindung zwischen Client und Server aufzubauen. Client und Server können sich so jederzeit über eine Verbindung Daten schicken. Hierbei handelt Möchte ein Client eine WebSocket-Verbindung zu einem Server aufbauen, schickt der Client eine normale HTTP-Anfrage und sendet zusätzliche HTTP-Header mit (Listing 1), mit der Bitte um ein Upgrade auf das WebSocket-Protokoll. Unterstützt der Server am Endpunkt /chat das WebSocket-Protokoll, antwortet er mit dem HTTP-Statuscode 101 Switching Protocols (Listing 2). Jetzt können Client und Server über die entstandene WebSocket-Verbindung bidirektional kommunizieren. Dieser Prozess nennt sich auch „WebSocket Handshake“. Doch aufgepasst: Sobald eine WebSocket-Verbindung aufgebaut wurde, passiert – solange niemand Daten übermittelt – auf dieser Verbindung tatsächlich nichts mehr. Das Protokoll beinhaltet kein Keep-Alive. Vielmehr gehen Client und Server davon aus, dass nach dem Verbindungsaufbau die Verbindung steht, d. h. dass in der Zwischenzeit beispielsweise die WLANVerbindung schlafen gelegt wird, da die Verbindung keine Aktivität aufweist. Erst wenn einer der Teilnehmer versucht Daten zu senden, kann erkannt werden, dass die Verbindung nicht mehr existiert und neu aufgebaut werden muss. Vorteilhaft ist dies natürlich für mobile Endgeräte. Eine Verbindung, die keine Aktivität hat, verbraucht keine CPU-Kapazitäten und leert auch nicht den Akku. Listing 1: Request-Header für ein U ­ pgrade auf das WebSocket-Protokoll GET /chat HTTP/1.1 Host: beispiel-seite.de Connection: Upgrade Upgrade: websocket Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://beispiel-seite.de Sec-WebSocket-Protocol: chat Sec-WebSocket-Version: 13 Listing 2: Response-Header für das U ­ pgrade auf das WebSocket-Protokoll HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat www.basta.net Besuchen Sie auch folgende Session: Keyboards? Where we’re going, we don’t need keyboards! Don Wibier One of the cornerstones in Microsoft’s digital assistant Cortana are cognitive services. Instead of the traditional Screen/Keyboard/ Mouse combination for user interaction with your application, it offers different ways of handling user input. Think about vision, speech and language – the new way of communicating with your devices – but also how to analyze and structure these kinds of user input. This session will give you an introduction on the Cognitive Services Platform – show how it can help your end-users – and with live coding examples you will experience how easy it is to start using this incredibly cool API. 23 DOSSIER Web Development Als Nächstes benötigen wir folgenden Befehl zum In­ stallieren der benötigten Abhängigkeiten: npm i --save socket.io [email protected] concurrently node-static nodemon Folgende Abhängigkeiten werden installiert: Abb. 1: Darstellung des Frontends – noch ohne Funktion Bevor wir nun anfangen, einen Keep-Alive-Mechanismus oder den Reconnect selbst zu implementieren, existieren in der freien Wildbahn einige Bibliotheken, die uns diese Arbeit abnehmen und ein paar Schmankerl zusätzlich bieten. In der Node.js-Welt ist Socket. IO [1] eine sehr beliebte Bibliothek zur Umsetzung von Echtzeitdatenübertragung. Die Implementierung ist sehr einfach, aber auch sehr mächtig. Mit Socket.IO kann man von einfachen Chats bis hin zum gemeinsamen Editieren von Dokumenten alles implementieren. Socket.IO abstrahiert dabei das WebSocket-Protokoll und implementiert zusätzliche Features wie Keep-Alive zum Erkennen von Verbindungsabbrüchen mit automatischem Reconnect. Kann Socket.IO keine Verbindung über das WebSocketProtokoll aufbauen, fällt es auf die Nutzung von HTTP Long Polling zurück. Das ist natürlich, wie eingangs beschrieben, nicht mehr so effizient, aber wir müssen diesen Fallback nicht selbst implementieren, und unser Code funktioniert weiterhin. • socket.io beinhaltet die zuvor angesprochene Bibliothek zur Abstraktion des WebSocket-Protokolls. • bootstrap beinhaltet die nötigen CSS-Dateien von Bootstrap für das entwicklerhübsche Frontend. Wichtig ist hier die Angabe der Version @4.0.0-al­ pha.3, sodass wir die aktuellste Version installieren. • concurrently nutzen wir, um parallele Kommandozeilenbefehle auszuführen. •node-static beinhaltet einen kleinen Webserver zum Ausliefern von statischen Dateien. Mit ihm sehen wir das Frontend im Browser. • nodemon dient zum Starten unserer Server inklusive einem Live-Reloading-Mechanismus, sobald der Serverquelltext sich ändert. Der nächste Vorbereitungsschritt fügt in unserer pack­ age.json-Datei ein neues Skript ein. Dazu öffnen wir die package.json-Datei und ersetzen den vorhandenen Eintrag scripts mit Folgendem: "scripts": { "start": "concurrently \"nodemon src/server/chatServer.js\" \"static .\" \"open http://localhost:8080/src/client/index.html\"" } Wie wäre es mit einem Plausch? Im Rahmen des Artikels wollen wir gemeinsam einen kleinen Chat entwickeln, in dem man in Echtzeit kommunizieren kann. Dabei soll der Chat verschiedene Räume anbieten. Jeder Raum kann beliebig viele Chatter aufnehmen. Nachrichten, die in einem Raum geschickt werden, werden an alle im Raum befindlichen Teilnehmer geschickt. Zudem soll dem Chatter eine Liste aller Personen im Raum angezeigt werden. Das Aussehen der Oberfläche ist in Abbildung 1 zu sehen. Das fertige Beispiel ist auf GitHub [2] zu finden. Als Voraussetzung wird Node.js [3] in einer aktuellen Version benötigt. Zudem erzeugen wir einen Ordner chatSample, in dem wir alle Sources ablegen. In diesem Ordner öffnen wir nun auch eine Kommandozeile und geben folgenden Befehl ein: npm init -y Damit wird eine package.json-Datei erstellt, in der wir unsere Abhängigkeiten und Startskripte abspeichern. www.basta.net Besuchen Sie auch folgende Session: User Experience: Golden Rules and Common Problems of Web Views Dino Esposito You don’t have to be a UX designer to spot out some basic and common problems in the overall UX delivered by too many web views out there. And if you’re a UX designer, chances are that you miss some key points especially in the area of mobile devices and overall performance. This talk is about seven golden rules of UX applied to web pages – including drop-down content, input of dates, picking lists, images. Concretely the talk discusses patterns and JavaScript frameworks (Image Engine, WURFL.js, typeahead.js, various Bootstrap and jQuery plugins) that can be helpful in your everyday frontend development. The talk is agnostic of any “large” development framework like Angular or React, so no one will be left behind! 24 DOSSIER Wir nutzen concurrently, um drei Kommandozeilenbefehle gleichzeitig auszuführen (alternativ wären die drei Befehle in drei Kommandozeilen auszuführen). Der erste Befehl startet nodemon mit dem Parameter src/server/chatServer.js. Der Parameter ist die Datei, die von nodemon gestartet und überwacht werden soll. Jegliche Änderung dieser Datei führt zum Neustart des Node.js-Prozesses, sodass unsere Änderungen sofort zur Verfügung stehen. Das spart das Beenden und Neustarten der Kommandozeile. Da wir auf aufwendige Build-Prozesse verzichten, nutzen wir den zweiten Befehl, um einen Webserver mit nodestatic im gleichen Verzeichnis zu starten, in dem die package.json-Datei liegt. Der dritte Befehl öffnet das Frontend im Browser. Da node-static per Standard einen Webserver auf Port 8080 öffnet und unser Frontend unter src/client/ Web Development index.html zu erreichen ist, ergibt sich der im Skript angezeigte URL http://localhost:8080/src/client/index. html. Bevor wir das Skript starten können, müssen wir die benötigten Dateien anlegen. Das heißt, wir erstellen einen Ordner src und darin einen Ordner client und einen Ordner server. In den Ordner client legen wir eine index. html-Datei und in den Ordner server eine chatServer.jsDatei. Die Dateien können ohne Inhalt angelegt werden, sodass wir sie gemeinsam mit Leben füllen können. Sind die Dateien angelegt, kann auf der Kommandozeile der folgende Befehl ausgeführt werden: npm start Damit wird nun der Server gestartet und das Frontend öffnet sich. Selbstverständlich wird weder im Server et- Listing 3: „index.html“-Datei <!DOCTYPE html> <html> <head> <title>Sample Socket.io Chat</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <link href="../../node_modules/bootstrap/dist/css/bootstrap.css" rel="stylesheet"/> </head> <body> <div class="container"> <div class="row"> <div class="col-md-12"> <div class="jumbotron"> <h1 class="display-3">Socket.io Sample</h1> <p class="lead">This is a sample application to show the usage of Node.js and Socket.io based on a chat application.</p> <hr class="m-y-1"> <form class="form-inline" id="name-form"> <div class="form-group"> <label for="name" class="form-control-label">Please tell me your name:</label> <input type="text" class="form-control" id="name" name="name" value="Bob"> </div> </form> </div> </div> </div> <div class="row"> <div class="col-md-3"> <form id="new-room-form" action="javascript:"> <div class="form-group"> www.basta.net <input type="text" class="form-control" placeholder="New room name..." id="new-room-name"> </div> </form> <ul class="list-group" id="rooms"> </ul> </div> <div class="col-md-6"> <form id="add-message-form"> <div class="form-group"> <input type="text" class="form-control" id="message-text" placeholder= "If you've joined a room, type in a message and press enter to send it..."> </div> </form> <ul class="list-group" id="message-log"> <li class="list-group-item">You have to join a room in order to chat with someone.</li> </ul> </div> <div class="col-md-3"> <h3>Chatters</h3> <ul class="list-group" id="chatters"> </ul> </div> </div> </div> <script src="../../node_modules/socket.io-client/socket.io.js"></script> <script src="app.js"></script> </body> </html> 25 DOSSIER was passieren noch werden wir im Frontend etwas sehen, da die Dateien leer sind. Aber sie dienen für unser Skript als Grundlage, mit der wir nun arbeiten können. Einzig und allein im Browser müssen wir manuell per F5 bei Änderungen einen Refresh durchführen. Der Server wird durch nodemon automatisch neu starten. Vorne fangen wir an ... Der Inhalt der index.html-Datei ist in Listing 3 zu finden. Die wichtigsten Stellen sind einmal am Ende des <head>- und <body>-Bereichs. In <head> wird das CSS von Bootstrap geladen, während in <body> einmal die Listing 4: „chatServer.js“ 'use strict'; const socketIo = require('socket.io'); class ChatServer { constructor(port) { this._port = port; } start() { this._initializeSocketIo(); console.log('Server is up and running'); } _initializeSocketIo() { this._io = socketIo(this._port); this._io.on('connection', socket => { socket.room = ''; socket.name = ''; socket.emit('rooms', this._getRooms()); socket.on('join-room', msg => this._joinRoom(socket, msg)); socket.on('change-name', name => { socket.name = name; this._sendAllChattersInRoom(socket.room); }); socket.on('message', message => this._ io.in(socket.room).emit('message', { from: socket.name, message: message })); }); } } const server = new ChatServer(8081); server.start(); www.basta.net Web Development Socket.IO-Bibliothek geladen wird und eine app.js-Datei, in der wir zugleich unseren Client implementieren werden. Daher legen wir nun im Ordner src/client eine neue Datei mit dem Namen app.js an. Dazwischen ist ein HTML-Template zur Definition des Frontends gemäß der Abbildung 1, das wir nun auch sehen, wenn wir den Browser refreshen. ... und wechseln direkt nach hinten ... Schauen wir uns zunächst die Implementierung vom Server an. Dazu öffnen wir die chatServer.js und fügen den Inhalt aus Listing 4 ein. Zu Beginn laden wir die Socket.IO-Bibliothek und definieren die neue Klasse ChatServer. Der Konstruktor der Klasse benötigt einen Port, auf dem der Server gestartet werden soll. Die einzige öffentliche Methode start() initialisiert Socket.IO und startet damit den Server. Dazu übergeben wir der geladenen Bibliothek unseren Port, und schon haben wir einen WebSocket-Server mithilfe von Socket.IO am Laufen. Einfacher gehts nicht! Socket.IO arbeitet eventbasiert. Daher harmoniert es sehr gut mit Node.js und kann die Stärken von non-blocking I/O ausnutzen. So ist ein einzelner Prozess in der Lage, mehrere tausend oder gar hunderttausend Verbindungen gleichzeitig zu verwalten. Generell können wir bei Socket.IO mit der Methode on() auf ein Event reagieren. Das erste Event, das uns interessiert, ist das connection-Event. Es findet immer dann statt, wenn ein neuer Client sich verbindet. Im Callback erhalten wir den Client (Socket), der sich verbindet und auf dem wir arbeiten können. Als ersten Schritt initialisieren wir den Raum und den Namen des Listing 5: Die Methode „_joinRoom()“ _joinRoom(socket, roomName) { if (socket.room) { socket.leave(socket.room, () => { this._io.in(socket.room).emit('chatter-left', socket.name); this._internalJoinRoom(socket, roomName); }); return; } this._internalJoinRoom(socket, roomName) } _internalJoinRoom(socket, roomName) { socket.join(roomName, () => { socket.room = roomName; this._io.in(socket.room).emit('chatter-joined', socket.name); this._io.sockets.emit('rooms', this._getRooms()); this._sendAllChattersInRoom(socket.room); }); } 26 DOSSIER Sockets als leeren String. Diese Daten werden wir im Anschluss vom Client erhalten. Als Nächstes können wir mit socket.emit() Daten an genau diesen einen Client übermitteln. Der erste Parameter ist ein Schlüssel zur Identifikation der Daten. Dieser Schlüssel wird auf der Clientseite zu einem Event, das wir wieder per on()-Methode fangen können. Der zweite Parameter sind die eigentlichen Daten. Als Erstes wollen wir also dem Client alle vorhandenen Räume übermitteln. Die Methode _getRooms() (und Listing 6: Die Methode „_getRooms()“ _getRooms() { const rooms = {}; Object.keys(this._io.sockets.connected).forEach(socketId => { const socket = this._io.sockets.connected[socketId]; if (socket.room) { rooms[socket.room] = ''; } }); return Object.keys(rooms); } Web Development weitere, bisher nicht definierte Methoden) werden wir im Anschluss entwickeln. Anschließend implementieren wir mehrere socket. on(), um auf Daten zu reagieren, die uns vom Client geschickt wurden. Aufgepasst: Nutzen wir on() oder emit() auf der socket-Ebene, horchen bzw. kommunizieren wir auch nur genau mit diesem einen Client. Nutzen wir die Methoden stattdessen auf this._io-Ebene, horchen wir auf alle Clients oder senden allen Clients unsere Daten. Die socket.on()-Methode lauscht auf das Event joinroom, das empfangen wird, wenn der Client den Raum wechselt. Das Event change-name tritt ein, wenn der Client seinen Benutzernamen wechselt. Dies vermerken wir in unserem Socket und senden die Namensänderung an alle anderen Clients. Das dritte Event ist das message-Event. Es tritt ein, wenn der Client eine Nachricht schickt, die im Chatraum dargestellt werden soll. Erhalten wir dies am Server, möchten wir diese Nachricht an alle Clients im gleichen Raum schicken. Socket.IO kennt das Konzept von Räumen und bietet daher die Methode in() an, um eine Nachricht nur an Clients in einem bestimmten Raum zu schicken. Das ist natürlich der Raum, in dem der Client die Nachricht geschrieben hat. Per emit() schicken wir die Nachricht zusammen mit dem Namen des Clients an alle im Raum befindlichen Chatter. Listing 7: „app.js“ changeName(); }); !function () { 'use strict'; controls.newRoomForm.addEventListener('submit', function (event) { event.preventDefault(); createRoom(); }); var controls, socket; function initialize() { initializeControls(); initializeSocketIo(); } function initializeControls() { controls = { name: document.querySelector('#name'), newRoomName: document.querySelector('#new-room-name'), chatters: document.querySelector('#chatters'), rooms: document.querySelector('#rooms'), messageLog: document.querySelector('#message-log'), messageText: document.querySelector('#message-text'), nameForm: document.querySelector('#name-form'), newRoomForm: document.querySelector('#new-room-form'), addMessageForm: document.querySelector('#add-message-form') }; controls.nameForm.addEventListener('submit', function (event) { event.preventDefault(); www.basta.net controls.addMessageForm.addEventListener('submit', function (event) { event.preventDefault(); sendMessage(); }); } function initializeSocketIo() { socket = io('http://localhost:8081'); socket.on('connect', changeName); socket.on('rooms', refreshRooms); socket.on('message', function (message) { addMessage(message.message, message.from); }); } window.addEventListener('load', initialize); }(); 27 DOSSIER Abb. 2: Das Beispiel in Aktion Schauen wir uns als Nächstes die Methode _join­ Room() an, die ausgeführt wird, wenn der Client den Raum wechselt. Der Inhalt ist in Listing 5 zu finden. In _joinRoom() prüfen wir, ob der Socket sich in einem Raum befindet. Falls ja, nutzen wir die leave()Methode, um den Raum zu verlassen. Der Callback wird ausgeführt, sobald der Client diesen Raum tatsächlich verlassen hat. Das bedeutet, dass der Socket. Web Development IO-Server dem Client das Verlassen des Raums mitteilt und dieser eine Bestätigung schickt. Ist das passiert, schicken wir an alle Clients im alten Raum eine Nachricht, dass ein Client diesen Raum verlassen hat. Danach können wir in den neuen Raum durch den Aufruf der Methode _internal­ JoinRoom() wechseln. Das Betreten des Raums passiert nun mit der join()-Methode. Auch wird der Callback ausgeführt, sobald der Client den Raum tatsächlich betreten hat. Dann können wir den aktuellen Raum speichern und allen Clients im Raum eine Meldung schicken, dass der Client den Raum betreten hat. Zusätzlich senden wir allen Clients die aktuellen Räume und welche Chatter sich im aktuellen Raum befinden. Als Letztes müssen wir noch die Methode _get­ Rooms() implementieren. Ihre Implementierung ist in Listing 8: Weitere Methoden in der „app.js“ function changeName() { var newName = controls.name.value; socket.emit('change-name', newName); } function createRoom() { var newRoom = controls.newRoomName.value; changeRoom(newRoom); } function changeRoom(roomName) { socket.emit('join-room', roomName); addMessage('You are now in room ' + roomName); } function refreshRooms(allRooms) { while (controls.rooms.firstChild) { controls.rooms.removeChild(controls.rooms.firstChild); } allRooms.forEach(function (room) { var item = createListItem(room); changeRoom(room); }); controls.rooms.appendChild(item); }); } function addMessage(message, from) { var text = from ? '<strong>' + from + ':</strong> ' + message : message; controls.messageLog.appendChild(createListItem(text)); } function sendMessage() { var message = controls.messageText.value; socket.emit('message', message); controls.messageText.value = ''; } function createListItem(text) { var item = document.createElement('li'); item.classList.add('list-group-item'); item.innerHTML = text; return item; item.addEventListener('click', function () { www.basta.net } 28 DOSSIER Listing 6 zu finden. Die Methode _sendAllChattersIn­ Room() ist zur Übung dem Leser überlassen. Hier erzeugen wir uns eine Hashmap mit allen Räumen, indem wir über die verbundenen Sockets iterieren. Per Object. keys() wandeln wir die Hashmap in ein String-Array um. Damit sind die Arbeiten am Server abgeschlossen. Schauen wir uns nun das Frontend an. ... und wieder ab nach vorne Für das Frontend öffnen wir die Datei src/clients/app.js. Der Inhalt ist in Listing 7 zu finden. Zuerst initialisieren wir ein paar Controls, die auf unserer HTML-Seite liegen und essenziell für die Chatnutzung sind. Zudem hängen wir an die drei Formulare der Input-Elemente einen submit Event Listener, sodass wir auf deren Eingaben reagieren können. Wie auch auf der Serverseite werden weitere Methoden im Anschluss implementiert. Danach folgt die Initialisierung von Socket.IO. Durch die Einbindung der Bibliothek in der HTMLDatei steht uns die globale Funktion io() zur Verfügung. Als Parameter bekommt sie den Endpunkt von unserem Server. Wie beim Server können wir mit der on()-Methode auf Events reagieren. Das Event connect wird von Socket.IO emittiert, sobald der Client mit dem Server verbunden ist. Ist dies geschehen, schicken wir über die Methode changeName() unseren Namen zum Server. Das Event rooms haben wir auf Serverseite bereits emittiert. Empfangen wir es hier am Client, wollen wir die Darstellung der Räume aktualisieren. Erhalten wir das Event message fügen wir die Nachricht in unserer Nachrichtenliste ein, indem wir die Methode addMessage() nutzen und ihr die benötigten Parameter übergeben. Zuletzt hängen wir uns an das load-Event vom Browser, das gefeuert wird, wenn die Seite fertig geladen ist, sodass wir Zugriff auf alle Elemente haben. Schauen wir uns noch die restlichen Methoden an, die wir in Listing 7 benutzen, aber noch nicht definiert haben. Ein Blick in Listing 8 zeigt deren Implementierungen. Die Methoden createRoom(), changeName() und sendMessage() funktionieren alle nach dem gleichen Prinzip. Sie lesen aus dem <input>-Element den Wert aus und nutzen socket.emit() zum Versenden an den Server, worauf im Server die passende on()-Methode aktiv wird. Die Methoden refreshRooms() und addMessage() sind der Gegenpart zum Empfangen der Servernachrichten und dienen zum Aktualisieren der Räume und Empfangen von Nachrichten. Damit sind wir tatsächlich mit der Implementierung schon fertig und haben einen vollständigen Chat, der mit WebSocket in Echtzeit kommuniziert und Räume unterstützt. Abbildung 2 zeigt, wie der Chat in Benutzung und vollständiger Implementierung aussieht. Zur Übung sind dem Leser die Implementierungen www.basta.net Web Development des Empfangs der Events chatter-left, chatter-joined und refresh-chatters überlassen. Ein Blick in das GitHub Repository bietet eine Hilfestellung bei der Implementierung. Fazit Schaut man sich den Code an, der für die Kommunikation zuständig ist, sieht man, dass erstaunlich wenig dafür nötig war. Viel Code drumherum dient zum Aktualisieren der Oberfläche oder Helfermethoden zum Sammeln von Daten. Socket.IO bietet eine sehr einfache Abstraktion des WebSocket-Protokolls, bleibt aber dennoch sehr mächtig und unterstützt uns mit sinnvollen Keep-Alive-Mechanismen mit automatischem Reconnect. Es obliegt vielmehr der kreativen Nutzung, um interessante Echtzeitszenarien umzusetzen. Übrigens, es stehen auch Clientbibliotheken für C# zur Verfügung [4], um mit Socket.IO zu kommunizieren. Viel Spaß beim Implementieren von Echtzeitszenarien! Seitdem mit HTML5 und JavaScript moderne Cross-PlattformLösungen entwickelt werden können, begeistert sich Manuel Rauber für die Umsetzung großer Applikationen auf mobilen Endgeräten aller Art. Als Software Developer bei der Thinktecture AG unterstützt er die Entwicklung mobiler Cross-Plattform-Lösungen mit Angular 2, Cordova, Electron und Node.js oder .NET im Backend. Links & Literatur [1] http://socket.io/ [2] https://github.com/thinktecture/windows-developer-nodejs-socketio [3] https://nodejs.org [4] https://github.com/Quobject/SocketIoClientDotNet Besuchen Sie auch folgende Session: ASP.NET-Core- und MVC-Sicherheit – eine Übersicht Dominick Baier ASP.NET Core ist Microsofts nagelneue Cross-Plattform-Runtime zum Entwickeln von serverseitigen .NET-Anwendungen. MVC Core ist das dazugehörige Framework für Webanwendungen und APIs. Entsprechend neu sind auch die Konzepte für das Absichern von Anwendungen: Authentifizierung, Autorisierung, Datenschutz – es gibt eine Menge zu lernen. 29 DOSSIER Web Development Vom Gerät zum Live-Dashboard Mit Azure zur IoT-Infrastruktur Mit den Azure Event Hubs hat Microsoft einen Dienst im Portfolio, mit dem sich Millionen von Events per Sekunde erfassen lassen. Anwendungsszenarien gibt es viele: Beispielsweise könnte eine gewöhnliche Anwendung Telemetriedaten an den Event Hub senden. Ebenso ist es denkbar, dass Windkrafträder, Kühlschränke oder Raspberry Pis Sensordaten an einen Event Hub übermitteln – typischerweise das, was man sich unter IoT vorstellt. Mit einem Stream Analytics Job lässt sich der Event Stream aus dem Event Hub auslesen und für die Weiterverarbeitung mit einer SQL-ähnlichen Syntax aggregieren. Die aggregierten Daten lassen sich dann bspw. in Echtzeit in einem Live-Dashboard in Power BI anzeigen. Genau dieses Szenario wird in diesem Artikel beschrieben – von der Anwendung über einen Event Hub über Stream Analytics hin zum Power-BI-Live-Dashboard. von Thomas Claudius Huber Microsoft hat mit der „Cloud-first and Mobile-first“Strategie richtig an Fahrt aufgenommen. Was heute mit Microsoft Azure für jeden Entwickler in einfachen Schritten möglich ist, war vor ein paar Jahren noch nahezu undenkbar. So lässt sich mit etwas Know-how bereits ein hochskalierendes Backend zum Verarbeiten von Millionen von Events konfigurieren. Wie das geht, wird in diesem Artikel beschrieben. Los geht’s mit einem Blick auf das „Big Picture“. Das „Big Picture“ Die in diesem Artikel erstellte Architektur ist in Abbildung 1 dargestellt: Endgeräte senden Events an einen Azure Event Hub. Ein Stream Analytics Job liest den Event Stream aus dem Event Hub aus und aggregiert gegebenenfalls die Daten mit einer SQL-ähnlichen Abfragesprache. Der Stream Analytics Job kann die Daten in verschiedene Outputs schieben – in eine Azure-SQLDB, in einen Table Storage, in einen weiteren Event Hub usw. In diesem Artikel ist der Output des Stream Analytics Jobs, wie in Abbildung 1 dargestellt, Power BI. In Power BI lässt sich ein Live-Dashboard erstellen, das die aktuellen Daten nahezu in Echtzeit darstellt. In Abbildung 1 sind die Event-Produzenten Raspberry Pis. Doch das sendende Gerät ist beliebig: Es kann ein Kühlschrank, ein Toaster, ein Windrad, ein Motor, ein Smartphone oder beispielsweise auch ein ganz normaler PC sein. Letzten Endes ist es einfach ein Stück Software, www.basta.net das auf irgendeiner Art von Gerät oder sogar ebenfalls als Job in der Cloud läuft. Und diese Software sendet Events an den Event Hub. Szenarien für eine solche Event-getriebene Architektur gibt es viele. So könnte eine gewöhnliche Anwendung Telemetriedaten an einen Event Hub senden. Die Telemetriedaten ermöglichen Informationen über die Nutzung der Anwendung. Ebenso ist es denkbar, dass Geräte mit Sensoren aktuelle Sensordaten an den Event Hub liefern. Beispielsweise könnten Windräder die aktuell produzierten Kilowatt pro Stunde an den Energiedienstleister in Echtzeit überliefern. Es gibt keine Grenzen. In diesem Artikel werden keine Raspberry Pis wie in Abbildung 1 eingesetzt, da dies den Umfang des Artikels hinsichtlich Hardwareintegration sprengen würde. Stattdessen wird eine kleine WPF-Applikation [1] verwendet, die den Event Hub mit Events füttert. Damit lässt sich auf einfache Weise das ganze Szenario vom Client über den Event Hub über den Stream Analytics Job zum Power-BI-Live-Dashboard durchspielen. Mehr zu der verwendeten WPF-Applikation als Event-Sender erfahren Sie nach dem Erstellen des Event Hubs. Azure Event Hubs Über das Azure-Portal [2] lässt sich auf einfache Weise ein Event Hub erstellen. Nach dem Einloggen in das Azure-Portal wird über das + eine neue Ressource angelegt. Unter der Kategorie Internet of Things (IoT) befindet sich das Element „Event Hubs“. Damit lässt sich ein 35 DOSSIER Web Development Abb. 1: Klassische Architektur mit Event Hub, Stream Analytics und Power BI neuer Name­space für Event Hubs erstellen. Ein Event Hub als solcher gehört immer zu einem Event Hub Namespace. Dieser wiederum kann bis zu zehn Event Hubs enthalten. Wird im Azure-Portal auf das Element Event Hubs geklickt, öffnet sich der „Blade“ – so werden die einzelnen, horizontal nebeneinander geöffneten „Fenster“ im Azure-Portal genannt – zum Erstellen des Event Hub Namespace. Nach der Eingabe eines NamespaceNamens – in unserem Beispiel „WinDeveloper-EHNS“ – und der Angabe einer Ressourcengruppe lässt sich der Event Hub Namespace durch einen Klick auf den Cre­ate-Button anlegen (mehr Infos zu Ressourcengruppen finden sich im zugehörigen Infokasten). Den Event Hub erstellen Um den eigentlichen Event Hub zu erstellen, wird im AzurePortal über die neu angelegte Ressourcengruppe „WinDeveloperResGroup“ zum Event Hub Namespace navigiert. Auf dem Blade für den Event Hub Namespace befindet sich ein Button + Event Hub. Ein Klick darauf öffnet ein weiteres Blade zum Erstellen eines Event Hubs. In Abbildung 2 wurde in diesem Blade bereits der Name für den zu erstellenden Event Hub eingegeben. Ein Klick auf den CreateButton genügt, und der Event Hub ist angelegt. Neben dem Namen lässt sich auch die in Abbildung 2 zu sehende Message Retention einstellen, die festlegt, wie lange die Events im Event Hub verfügbar sind. Der Abb. 2: Einen Event Hub erstellen Defaultwert ist ein Tag, der Maximalwert beträgt sieben Tage. Neben der Message Retention lassen sich auch die Partitionen einstellen, die für den Event Hub verwendet werden. Dabei kann eine Zahl zwischen 2 und 32 angegeben werden. Die Partitionen eines Event Hubs sind wichtig für die Skalierung. Obwohl im Portal der maximal einstellbare Wert 32 Partitionen sind, lassen sich durch Supporttickets auch Event Hubs mit 1 024 oder mehr Partitionen erstellen. Doch was genau hat es mit der Skalierung und den Partitionen auf sich? Um diese Frage zu klären, muss eine wichtige Größe beachtet werden, die so genannte Throughput Unit (Durchsatzeinheit). Besuchen Sie auch folgende Session: Über Ressourcengruppen Mit dem neuen Azure-Portal [2] hat Microsoft das Konzept der Ressourcengruppen eingeführt, das es unter dem alten Azure-Portal [3] noch nicht gab. Ein Azure-Dienst, wie ein Event Hub Namespace oder ein Stream Analytics Job, wird immer einer Ressourcengruppe zugeordnet. Die Dienste einer Ressourcengruppe haben gemeinsame Berechtigungen und einen gemeinsamen Lebenszyklus. So lässt sich z. B. nicht nur ein einzelner Dienst entfernen, sondern bei Bedarf auch die ganze Ressourcengruppe. Zum Testen und Ausprobieren von Azure-Diensten empfiehlt es sich somit, eine neue Ressourcengruppe als „Spielplatz“ zu erstellen, wie dies auch beim Anlegen des Event Hub Namespace gemacht wird. Dann lässt sich nach dem Testen und Ausprobieren die komplette Ressourcengruppe mit einem Schwung entfernen. www.basta.net Enterprise-Apps mit Xamarin und den Azure App Services entwickeln Jörg Neumann Die Anforderungen an eine Business-App steigen stetig. Sie soll auf verschiedenen Plattformen laufen, auch von unterwegs Zugriff auf Unternehmensdaten bieten und natürlich offlinefähig sein. Für solche Aufgaben bieten die Azure App Services elegante Lösungen. Sie ermöglichen eine einfache Bereitstellung von Backend-Services, die Anbindung an unterschiedliche Datenquellen und eine Integration ins Unternehmensnetz. Zudem werden verschiedene Varianten der Authentifizierung und der Versand von Push-Benachrichtungen geboten. Jörg Neumann zeigt Ihnen, wie Sie mit Xamarin und den Azure App Services Enterprise-taugliche Apps entwickeln und betreiben können. 36 DOSSIER Die Throughput Unit Auf dem Blade des Event Hub Namespace lässt sich unter der Einstellung Scale die Anzahl der Throughput Units angeben – das ist der Durchsatz, den die Event Hubs leisten können. Der Defaultwert ist 1, der im Portal verfügbare Maximalwert 20. Eine Throughput Unit bedeutet, dass sich 1 MB und maximal 1 000 Events pro Sekunde in den Event Hub schreiben lassen. Zum Lesen aus dem Event Hub unterstützt eine Throughput Unit 2 MB pro Sekunde. Die eingestellten Throughput Units sind also von enormer Bedeutung für die Skalierung. Müssen mehr als 1 000 Events pro Sekunde verarbeitet werden, so muss die Anzahl der Throughput Units entsprechend erhöht werden. Und jetzt kommt das Zusammenspiel der Throughput Units und der Partitionen eines Event Hubs zum Vorschein. Die Partitionen beschreiben, wie die Daten physikalisch abgelegt und verteilt sind. Eine Einstellung von zwanzig Throughput Units und lediglich zwei Partitionen auf einen Event Hub wird nicht funktionieren: Das physikalische Schreiben in einen Event Hub mit nur zwei Partitionen bildet einen Flaschenhals bei zwanzig Throughput Units. Daher gilt die Grundsatzformel, dass die Anzahl der gewählten Partitionen in einem Event Hub größer oder zumindest gleich groß wie die Anzahl der Throughput Units des Event Hub Namespace ist. Die Kosten des Event Hubs Die Throughput Units spielen neben der Skalierung die zentrale Rolle für die Kosten eines Event Hubs. Die Kosten setzen sich prinzipiell aus zwei Größen zusammen, die im Basisprofil wie folgt aussehen: 1.Pro Million eingegangener Events sind 0,0236 Euro fällig 2.Pro Throughput Unit sind 0,0126 Euro pro Stunde fällig, was in etwa 9 Euro pro Monat entspricht Jetzt lässt sich leicht ausrechnen, dass sich mit einer Throughput Unit, die ja 1 000 Events pro Sekunde verarbeiten kann, pro Tag theoretisch 86 400 000 Events verarbeiten lassen (1000 * 60 * 60 * 24). Fallen diese Events an, sind die Kosten wie folgt: Der Tagespreis für eine Throughput Unit liegt bei 24 * 0,0126 Euro, was 0,3024 Euro entspricht. Dazu kommen die Kosten für eine Million Events von 0,0126 Euro. Bei 86 400 000 Events pro Tag wären die Kosten somit 86,4 * 0,0126 Euro, was 1,08864 Euro pro Tag ergibt. Die gesamten Ta- Web Development geskosten sind folglich 1,08864 Euro + 0,3024 Euro = 1,3910 Euro. Bei 31 Tagen laufen somit Monatskosten von 1 ­ ,3910 Euro * 31­ = ­43,1224 Euro auf. Eine erstaunlich kleine Zahl, wenn berücksichtigt wird, dass sich damit 86 Millionen Events pro Tag verarbeiten lassen. Event Hub vs. IoT Hub Wer sich bereits mit Azure beschäftigt hat, der weiß, dass es neben dem Event Hub auch einen IoT Hub gibt. Beide scheinen ähnliche Funktionalität zu bieten, denn auch der IoT Hub unterstützt Events wie der Event Hub. Doch welcher Hub ist wann zu verwenden? Es gibt einige Unterschiede zwischen einem Event Hub und einem IoT Hub [4] (Kasten „IoT Hub und Event Hub zusammen“). Die zentralen Unterschiede sind in Tabelle 1 dargestellt. In der offiziellen Azure-Dokumentation [5] sind noch weitere Unterschiede zwischen IoT Hub und Event Hub beschrieben. Ein sehr spannender Unterschied sind jedoch die Kosten. Im vorigen Abschnitt wurde berechnet, dass mit einem Event Hub 86 400 000 eingehende Events pro Tag für knapp 45 Euro im Monat möglich sind. Um mit dem IoT Hub diese 86 400 000 eingehenden Events pro Tag zu verarbeiten, ist die höchste Edition des IoT Hubs namens S3 erforderlich. Doch diese S3-Edition kostet sage und schreibe 4 216,50 Euro pro Monat. Folglich sollte dieser Kostenpunkt in einer Architekturentscheidung nicht unberücksichtigt bleiben. In diesem Artikel wird der Event Hub verwendet, der für eine reine Device-to-Cloud-Kommunikation eine feine Sache ist. Daten in einen Event Hub schreiben Um Daten in einen Event Hub zu schreiben, gibt es zwei Möglichkeiten: 1.Via Advanced Message Queuing Protocol (AQMP) 2.Via HTTP und REST-Schnittstelle IoT Hub und Event Hub zusammen In der Praxis kommen IoT Hub und Event Hub auch oft zusammen zum Einsatz. Dabei werden ein oder mehrere IoT Hubs zum Anbinden der Geräte an Azure verwendet. Die IoT Hubs selbst schreiben ihre Events in einen oder mehrere nachgelagerte Event Hubs, die wiederum ggfs. noch weitere Events aus anderen Quellen empfangen. Event Hub IoT Hub Kommunikation Device zur Cloud Device zur Cloud Cloud zum Device Security Event-Hub-weite Shared Access Policy Security auf Device-Ebene konfigurierbar Gerätemanagement Keine Hat eine Registrierung für die Geräte, mit der auch die Securityeinstellungen auf Geräteebene einhergehen Skalierung 5 000 gleichzeitige Verbindungen Millionen von verbundenen Geräten Tabelle 1: Zentrale Unterschiede zwischen Event Hub und IoT Hub www.basta.net 37 DOSSIER Für das Protokoll AQMP gibt es verschiedene Libraries, die sich nutzen lassen. Existiert für eine Plattform keine AQMP-Library, kann mit einem gewöhnlichen HTTPClient ein Event über die REST-Schnittstelle des Event Hubs veröffentlicht werden. Allerdings ist das SecurityToken entsprechend im HTTP-Header mitzugeben, was eventuell nicht ganz trivial ist. Auf GitHub [6] gibt es den EventHub.RestClientGenerator, der den HTTP-Client-Code für eine UWP-App erzeugt. Dieser Code lässt sich leicht auf beliebige andere Plattformen anpassen, da er lediglich einen reinen HTTP-POST-Aufruf enthält, um ein Event in Form eines JSON-Strings im Event Hub zu veröffentlichen. Wird eine .NET-Anwendung zum Schreiben in einen Event Hub genutzt, ist das Verwenden der REST-Schnittstelle nicht notwendig. Statt der REST-Schnittstelle empfiehlt sich das NuGet-Paket WindowsAzure.ServiceBus. Dieses Paket enthält die EventHubClient-Klasse, die das Schreiben von Events in den Event Hub via AQMP ermöglicht. Mit der statischen Methode CreateFromConnectionString wird eine EventHubClient-Instanz erstellt, was wie folgt aussieht: var client = EventHubClient.CreateFromConnectionString(connectionString); Doch jetzt stellt man sich als Entwickler die berechtigte Frage, wo es denn den Connection String gibt. Dazu wechselt man im Azure-Portal zum Event Hub und klickt unter den Einstellungen auf Shared Access Policies. Über den + Add-Button lässt sich eine neue Shared Access Policy einrichten. Dabei werden ein Name vergeben und die entsprechenden Claims für die Policy ausgewählt. Es gibt die selbsterklärenden Claims „Verwalten“, „Senden“, „Lesen“. In Folge wird eine Policy erstellt, die das Senden und Lesen, jedoch keine Änderungen (Manage) am Event Hub erlaubt. Nachdem die Shared Access Policy angelegt wurde, lässt sich diese über den Event Hub im Azure-Portal auswählen. Unter der Shared Access Policy wird dann der für diese Policy geltende Connection String angezeigt. Und genau dieser Connection String lässt sich für die statische CreateFromConnectionString-Methode der EventHub­Client-Klasse nutzen. Der Connection String sieht im für diesen Artikel generierten Beispiel wie folgt aus: Endpoint=sb://windeveloper-eh-ns.servicebus.windows.net/ ;SharedAccessKeyName=SendListenPolicy;SharedAccessKey=0v4Gn+ UtMWXOw4+4qGed7AVeEtG726U0G/wuC1S6Eyk=;EntityPath= windeveloper-eh01 Wie zu sehen ist, enthält der Connection String den Namespace als Endpoint. Der eigentliche Event Hub wird mit dem EntityPath-Attribut angegeben. Mit dem Connection String und der EventHubClient-Klasse lassen sich jetzt Events in den Event Hub schreiben. Dafür wird – wie zu Beginn des Artikels erwähnt – eine kleine WPF-Anwendung genutzt. www.basta.net Web Development Die Senderanwendung Zum Schreiben von Daten in den Event Hub wird an dieser Stelle eine kleine WPF-Anwendung genutzt, die auf GitHub [1] mitsamt Quellcode verfügbar ist. Die Anwendung besitzt eine SensorData-Klasse (Listing 1). Instanzen dieser SensorData-Klasse werden von der Anwendung als Event an einen Event Hub gesendet. Wie Listing 1 zeigt, hat eine SensorData-Instanz die Eigenschaften DeviceName, ReadTime, SensorType und Value. Wird die WPF-Anwendung gestartet, lässt sich darin der Event Hub Connection String einfügen und die Checkbox „Is sending data enabled“ anklicken. Damit legt die App los. Sie sendet alle zwei Sekunden zwei Events an den Event Hub, eines für den Sensortyp Temp (Temperatur) und eines für den Sensortyp Hum (Luftfeuchtigkeit). Über die Slider lassen sich die an den Event Hub gesendeten Werte für Temperatur und Luftfeuchtigkeit steuern. Ein Blick auf den Code der WPF-Anwendung zeigt die Methode, die zum Senden eines Events genutzt wird (Listing 2). Zuerst wird mit dem Connection String und der statischen CreateFromConnectionString-Methode eine EventHubClient-Instanz erzeugt. Anschließend wird die SensorData-Instanz mit der JsonConvertKlasse und deren statischer SerializeObject-Methode in einen JSON-String serialisiert. Dieser JSON-String wird UTF8-codiert dem Konstruktor der EventDataKlasse übergeben. Die EventData-Klasse stammt wie auch die EventHubClient-Klasse aus dem NuGet-Paket Windows­Azure.Storage. In der letzten Zeile wird in Listing 2 auf der EventHubClient-Instanz die SendAsync-Methode aufgerufen, die als Parameter die EventData-Instanz bekommt. Damit werden die Event-Daten an den Event Hub gesendet. Listing 1 public class SensorData { public string DeviceName { get; set; } public DateTime ReadTime { get; set; } public string SensorType { get; set; } public double Value { get; set; } } Hinweis Anstatt auf dem Event Hub eine Shared Access Policy anzulegen, gibt es auch die Möglichkeit, eine auf dem Event Hub Namespace erstellte Shared Access Policy zu nutzen. Diese kann dann über alle Event Hubs im Event Hub Namespace verwendet werden. Als Entwickler muss man abwägen, ob man den Zugriff auf Ebene der Event Hubs oder eben auf Ebene des Event Hub Namespace regeln möchte. 38 DOSSIER Web Development Stream-Analytics-Grundlagen Abb. 3: Azure Stream Analytics Stream Analytics erlaubt das Verarbeiten eines Event Streams in Echtzeit (Abb. 3). Der Event Hub kann bei Stream Analytics als Input definiert werden. Neben dem Event Hub kann auch ein IoT Hub und/oder ein Azure Blob Storage als Input definiert werden. Mit einer SQL-Variante lassen sich die Daten aus den verschiedenen Inputs im Stream Analytics Job aggregieren. In dem Beispiel dieses Artikels gibt es für den Stream Analytics Job lediglich einen Input, den Event Hub. Der Output eines Stream Analytics Jobs kann ebenfalls vielfältig sein. In diesem Artikel ist es ein Power-BI-Live-Dashboard, um die erhaltenen Daten nahezu in Echtzeit anzuzeigen. Stream Analytics aufsetzen Abb. 4: Die Ansicht des Stream Analytics Jobs im Azure-Portal Zwischenstand Soweit ist der Azure Event Hub aufgesetzt. Ebenso ist eine kleine WPF-Anwendung vorhanden, mit der sich Sensordaten an den Event Hub senden lassen. Die gesendeten Werte für Temperatur und Luftfeuchtigkeit können dabei über die beiden Slider in der Oberfläche der WPF-Anwendung eingestellt werden. Im nächsten Schritt werden die Events aus dem Event Hub mit einem Stream Analytics Job ausgelesen und in ein Power-BILive-Dashboard gepusht. Listing 2 public async Task<...> PublishAsync(string connectionString, SensorData sensorData) { try { var client = EventHubClient.CreateFromConnectionString( connectionString); string sensorDataJson = JsonConvert.SerializeObject(sensorData); var eventData = new EventData(Encoding.UTF8. GetBytes(sensorDataJson)); await client.SendAsync(eventData); ... } ... } www.basta.net Im Azure-Portal [2] wird wie auch zum Anlegen eines Event Hub Namespace über das + eine neue Ressource angelegt. Unter der Kategorie Internet of Things befindet sich das Element „Stream Analytics Job“. Wird darauf geklickt, öffnet sich Blade zum Erstellen eines Stream Analytics Jobs. Nach der Eingabe eines Namens wird die bereits existierende Ressourcengruppe WinDeveloperResGroup ausgewählt und der Create-Button geklickt. Ist der Job erstellt, lässt er sich über die Ressourcengruppe im Azure-Portal auswählen, um die Inputs, die Abfrage und die Outputs zu definieren (Abb. 4). Den Input definieren Ein Klick auf „Input“ (Abb. 4) erlaubt das Definieren des bereits erstellten Event Hubs als Input für den Stream Analytics Job. Dazu muss ein Input-Alias angegeben werden, der später in der SQL-Abfrage des Stream Analytics Jobs zum Referenzieren des Inputs genutzt wird. Hinweis Wie die an den Event Hub gesendeten JSON-Daten aussehen, ist frei definierbar. Die Struktur muss beim späteren Weiterverarbeiten jedoch bekannt sein, wenn bspw. in der StreamAnalytics-Abfrage bestimmte Attribute benötigt werden. Tipp Im GitHub-Projekt mit der WPF-Anwendung [1] ist neben einer Sender-Applikation auch eine Receiver-Applikation enthalten. Die Receiver-Applikation liest die Events aus einem Event Hub aus. Somit lassen sich Sender und Receiver gleichzeitig starten, um den Fluss der Events sehen zu können. 39 DOSSIER Web Development Den Output definieren Abb. 5: Die Abfrage des Stream Analytics Jobs Neben dem Input-Alias lässt sich auch das Serialisierungsformat wählen, in dem die Events im Event Hub vorliegen. Der Defaultwert ist JSON, was im hier verwendeten Beispiel korrekt ist, da die WPF-Anwendung die Sensordaten im JSON-Format an den Event Hub sendet. Weitere mögliche Formate sind CSV oder Avro. Ein weiterer, sehr wichtiger Punkt ist die Consumer Group. Ein Event Hub hat per Default eine Consumer Group mit dem Namen $Default. Wird keine Consumer Group angegeben, greift der Stream Analytics Job unter der $Default-Consumer-Group auf den Event Hub zu. Consumer Groups sind Teil des Event Hubs und wichtig für das Lesen/Konsumieren des Event Streams. Eine Consumer Group erlaubt fünf gleichzeitige Verbindungen zum Lesen von Events aus dem Event Hub. In der in diesem Artikel verwendeten Architektur greift lediglich der Stream Analytics Job auf den Event Hub zu, was somit exakt eine Verbindung darstellt. Wären es jedoch beispielsweise zehn Stream Analytics Jobs, müssten auf dem Event Hub weitere Consumer Groups erstellt werden. Microsoft empfiehlt, pro lesendem Dienst eine Consumer Group auf dem Event Hub zu erstellen. Ein Event Hub unterstützt via Azure-Portal maximal zwanzig Consumer Groups. In der in diesem Artikel verwendeten Architektur wird die $Default-Consumer-Group zum Lesen aus dem Event Hub genutzt, da der Stream Analytics Job der einzige Konsument des Event Streams ist. Tipp In Abbildung 5 ist oben ein Test-Button zu sehen, der allerdings deaktiviert ist. Wird auf die drei Pünktchen „...“ hinter dem Input „WinDevEH01“ geklickt, erscheint ein kleines Menü, das unter anderem den Menüpunkt Upload Sample data from file enthält. Damit lässt sich ein kleines Textfile hochladen, das die Events im JSON-Format enthält. Anschließend führt ein Klick auf den nach dem Hochladen der Daten aktiven Test-Button die aktuelle Query gegen diese Testdaten aus und zeigt die Ergebnisse im Browser an. Das ist sehr effektiv, um Konstrukte wie das Tumbling Window zu testen. www.basta.net Wird unter dem Blade für den Stream Analytics Job auf „Output“ geklickt (Abb. 4), lässt sich ein Output definieren, wie bspw. eine Azure SQL DB, ein Azure Table Storage, ein weiterer Event Hub und viele andere nützliche Speichermöglichkeiten. In diesem Artikel wird Power BI als Output gewählt. Damit Power BI als Output erstellt werden kann, muss eine Autorisierung mit einem gültigen Power BI-Account erfolgen. Unter [7] sollte man sich somit entsprechend registrieren, um die notwendigen Benutzerdaten im Output-Blade zur Autorisierung eingeben zu können. Ist die Autorisierung erfolgt, lassen sich neben dem Output-Alias – wieder wichtig für die SQL-Abfrage – Data Set und Table Name für Power BI benennen. Unter diesen Namen werden die Daten aus dem Stream Analytics Job in Power BI erscheinen. Wie üblich ein Klick auf „Create“, und der Output ist fertig definiert. Mit dem jetzt erstellten Input und dem Output kann es nun mit der Abfrage des Stream Analytics Jobs weitergehen. Die Abfrage erstellen Mit einem Klick auf „Query“ unter dem Blade für den Stream Analytics Job (Abb. 4) wird die Abfrage definiert. Die Abfrage lässt sich dabei direkt im Browser in einem einfachen Editor erstellen. Sie ist notwendig, um die Verbindung zwischen dem Input und dem Output eines Stream Analytics Jobs herzustellen. Dazu kommen die in den vorigen Abschnitten definierten Aliase für den Input und den Output zum Einsatz, was in Abbildung 5 zu sehen ist. Ein Stream Analytics Job wird üblicherweise verwendet, um die massive Menge an Event-Daten – es könnten Millionen sein – zu reduzieren. Dazu kommt klassisches Group By zum Einsatz, wie man es von SQL kennt. Doch im Stream Analytics Job wurde das SQL um ein paar Schlüsselfunktionen erweitert: So lassen sich beispielsweise mit einem so genannten Tumbling Window Events zeitbasiert in Zehnsekundenblöcke gruppieren und aggregieren. In diesem Artikel belassen wir es jedoch bei der einfachen Abfrage aus Abbildung 5, die die Daten aus dem Event Hub direkt an Power BI weitergibt, ohne die Daten zu reduzieren. Nachdem die Abfrage gespeichert wurde, ist ein letzter Schritt notwendig: Der Stream Analytics Job muss gestartet werden. Auf dem Blade für den Stream Analytics Job gibt es die Buttons Start und Stop. Ein Klick auf Start genügt, und der Job fährt hoch. Sobald der Job gestartet ist, verarbeitet er den Event Stream aus dem Event Hub und pusht die Daten in Power BI, wo sie sich für ein Live-Dashboard verwenden lassen. Die Daten in Power BI anzeigen Ist der Stream Analytics Job gestartet und werden aus der WPF-Anwendung Events an den Event Hub gesendet, so tauchen diese Daten jetzt im Power BI auf. Um dies zu sehen, genügt ein Log-in unter [7]. In der Navigation des Power-BI-Portals auf der linken Seite befindet sich unter Datasets der Punkt „Streaming datasets“. 40 DOSSIER Web Development Ein Klick darauf zeigt eine Übersicht der Streaming Datasets an, diese Übersicht zeigt jetzt das Dataset WinDev-DS (Abb. 6). WinDev-DS ist der Name, der beim Definieren des Outputs für den Stream Analytics Job angegeben wurde. Mit diesen eingehenden Daten lässt sich in Power BI ein kleines Dashboard erstellen, das die Daten anzeigt. Dazu wird in der linken Navigation unter Dashboard ein neues Dashboard über den +-Button hinzugefügt. Nachdem ein Name eingegeben wurde, wird das Dashboard selektiert. Im oberen Menü gibt es einen Add Tile-Button, der einen kleinen Abb. 6: Das Dataset in Power BI Dialog öffnet, über den sich eine Kachel zum Anzeigen der Livedaten definiert lässt. Links & Literatur Dazu hat der Dialog ganz unten den Punkt Real-time Data. Wird dieser Punkt ausgewählt, lässt sich anschlie[1]Testapplikationen zum Senden und Empfangen von Events an einen Azure ßend das aus dem Stream Analytics Job stammende DataEvent Hub: https://github.com/thomasclaudiushuber/EventHub.Clients set WinDev-DS selektieren. Als Visualisierungstyp kann [2]Das Microsoft-Azure-Portal: https://portal.azure.com ein Line Chart gewählt werden, und dabei sind folgende [3]Das alte Azure-Portal: https://manage.windowsazure.com Daten anzugeben: Axis wird auf die ReadTime-Spalte der [4]IoT Hub vs. Event Hub: https://docs.microsoft.com/en-us/azure/iot-hub/ Daten gesetzt (Sen­sorData-Klasse, Listing 1), Legend wird iot-hub-compare-event-hubs auf die Sensor­Type-Spalte der Daten gesetzt, und unter [5]Offizielle Azure-Dokumentation: https://docs.microsoft.com/de-de/azure/ Values wird die Value-Spalte der Sensordaten angegeben. [6]Event-Hub-REST-Client-Generator, um Events über die REST-basierte Zu guter Letzt wird ein Titel für die Kachel definiert. Beim Schnittstelle des Event Hubs zu veröffentlichen: https://github.com/ Erstellen wird die Kachel direkt auf dem Live-Dashboard thomasclaudiushuber/EventHub.RestClientGenerator angezeigt. Dabei sind in der Legende die beiden Sensoren [7]Power-BI-Portal: https://www.powerbi.com „Temp“ und „Hum“ zu sehen. Wird jetzt in der WPFApplikation – die die Events an den Event Hub sendet – ein Slider und somit der Wert eines Sensors verändert, ist dies unmittelbar im Power-BI-Live-Dashboard zu sehen. Fazit Dieser Artikel hat einen kleinen Einblick in das Zusammenspiel von Azure Event Hubs, Azure Stream Analytics und Power BI gegeben. Auch wenn hier lediglich eine kleine WPF-Anwendung Daten an den Event Hub sendet, ist die gebaute Architektur bereits ausreichend, um 1 000 Events pro Sekunde zu verarbeiten. Diese Skalierung und die einfachen Konfigurationsmöglichkeiten machen Azure zu einem sehr mächtigen Werkzeug. Und das Beeindruckende ist, dass bis auf die Clientanwendung keine einzige Zeile Code geschrieben wurde, um die Livedaten ins Power BI zu pushen – abgesehen von der Abfrage im Stream Analytics Job. Thomas Claudius Huber arbeitet als Principal Consultant bei der Trivadis AG. Er ist Microsoft MVP für Windows Development und Autor zahlreicher Bücher, Artikel und Pluralsight-Videos. Er spricht regelmäßig auf großen Entwicklerkonferenzen wie der BASTA!. www.thomasclaudiushuber.com www.basta.net Besuchen Sie auch folgende Session: Application-Gateways – Routing für Ihre Microservices Rainer Stropek Je mehr Microservices es werden, desto dringender brauchen Sie eine verlässliche und sichere Routinglösung, die den Traffic aus dem Web zum richtigen Endpunkt schickt. Rainer Stropek, langjähriger Azure MVP und MS Regional Director, startet in seiner Session mit einem Überblick über die Rolle von Application-Gateways in Microservices-Architekturen. Welche Aufgaben haben Application-Gateways in Verbindung mit Microservices? Welche Vorteile fügt ein professionelles Application-Gateway dem Microservices-Ansatz hinzu? Nach dieser Einleitung zeigt Rainer Demos mit dem OSS-Tool nginx und im Vergleich dazu den PaaS-Dienst Azure Application Gateway. 41 DOSSIER Data Access & Storage Was bringt der Neustart? Entity Framework Core 1.0 von Manfred Steyer Bei Entity Framework handelt es sich um die von Microsoft für .NET empfohlene Datenzugriffstechnologie. Seit seinem Erscheinen im Jahr 2008 hat das Produktteam den populären OR Mapper kontinuierlich weiterentwickelt. Somit liegt aktuell mit Version 6 eine ausgereifte und performante Lösung vor. Sie ist gut ins Ökosystem von .NET integriert und ermöglicht das Abfragen von Daten via LINQ. Allerdings haben sich in Entity Framework im Laufe der Zeit auch einige Mehrgleisigkeiten angesammelt. Beispielsweise existieren derzeit zwei Ansätze zum Abbilden von Entitäten auf Tabellen. Neben der Möglichkeit, diese Informationen in einer XML-Datei zu verstauen, kann das Entwicklungsteam hierzu seit Version 4.1 auch auf eine Konfiguration mittels Quellcode zurückgreifen. Daneben liegen derzeit auch zwei Programmiermodelle vor: Neben dem ursprünglichen ObjectContext existiert auch der neuere DbContext. Letzteren bindet Visual Studio ab Version 2012 standardmäßig ein. Er ist aus Sicht der Anwendung leichtgewichtiger, auch wenn er intern auf den ObjectContext fußt. Dazu kommen weitere Herausforderungen: Zum einen funktioniert Entity Framework 6 nicht mit dem neuen .NET Core, das auch unter Linux und Mac OS X läuft. Zum anderen adressiert das aktuelle Entity Framework lediglich relationale Datenbanksysteme. Auch wenn es sich dabei um das vorherrschende Datenbankparadigma handelt, kommen immer mehr Use Cases für alternative Ansätze auf. Unter dem Sammelbegriff www.basta.net ©iStockphoto.com/StudioM1 Mit Entity Framework (EF) Core 1.0 schreibt Microsoft sein populäres Datenzugriffsframework neu. Das primäre Ziel ist eine Unterstützung für das plattformunabhängige .NET Core. Daneben will das Produktteam Mehrgleisigkeiten beseitigen und NoSQL-Lösungen berücksichtigen. Da in Version EF Core 1.0 noch einige Aspekte fehlen, empfiehlt sich für Anwendungen jenseits .NET Core vorerst noch die aktuelle Version Entity Framework 6. NoSQL zusammengefasst, erleichtern diese Lösungen zum Beispiel auch den Umgang mit semistrukturierten Massendaten. Ziele und Nichtziele für das neue Entity Framework Um die eingangs erwähnten Herausforderungen zu adressieren, schreibt das Produktteam Entity Framework neu. Dies ist in erster Linie für die Unterstützung von .NET Core notwendig. Somit entsteht mit EF Core 1.0 eine Datenzugriffsbibliothek, die sowohl unter Windows und Linux als auch unter Mac OS X läuft und auch vor Universal-Windows-Apps nicht Halt macht. Im Zuge dessen beseitigt das Produktteam auch die entstandenen Mehrgleisigkeiten: Der alte ObjectCon­ text wird komplett entfernt, und das Mapping via XML-Dateien gehört ebenfalls der Vergangenheit an. Übrig bleiben der leichtgewichtige DbContext und die Konfiguration via Code. Auch wenn Letzteres in der Vergangenheit Code First genannt wurde, verhindert es nicht den Start mit einer bestehenden Datenbank. Dieses wohl am häufigsten vorkommende Szenario unterstützt EF Core 1.0, wie sein Vorgänger auch, mit einem Re­ verse-Engineering-Werkezeug. Dieses liest Metadaten aus der Datenbank aus und generiert Entitätsklassen sowie ein DbContext-Derivat. Auch wenn EF Core 1.0 ein vollständiger Neubeginn ist, möchte das Produktteam das API, soweit es sinnvoll möglich ist, nicht verändern. Entwickler, die Entity Framework in der Vergangenheit nutzten, sollen sich auch bei EF Core 1.0 heimisch fühlen. 42 DOSSIER Data Access & Storage Daneben betont das Produktteam, dass das neue Entity Framework die Unterschiede zwischen relationalen Datenbanken und NoSQL-Lösungen nicht „weg­ abstrahieren“ kann. Bestimmte Möglichkeiten existieren eben nur im relationalen Paradigma oder in bestimmten NoSQL-Lösungen, und daran kann auch eine moderne Datenzugriffsbibliothek nichts ändern. Aus diesem Grund wird auch EF Core 1.0 früher oder später die empfohlene Version werden. Allerdings bringt EF Core 1.0, das beim Verfassen dieses Texts als Release Candidate 1 (RC1) vorlag, auch ein paar neue Möglichkeiten. Die nachfolgenden Abschnitte gehen anhand einiger Beispiele, die unter [1] zu finden sind, darauf ein. Was noch nicht da ist? Mit Entity Framework Core 1.0 loslegen Das primäre Ziel für EF Core 1.0 ist das Bereitstellen einer Datenzugriffslösung für .NET Core. Um dieses Ziel bis zur Fertigstellung von .NET Core zu erreichen, fokussiert sich das Produktteam bei dieser ersten Version auf die unabdingbaren Features – oder anders ausgedrückt: Vieles, was man nicht unbedingt benötigt, wird vorerst weggelassen. Aus diesem Grund wird EF Core 1.0 auch weniger Features als sein Vorgänger, Entity Framework 6, mitbringen (Tabelle 1). Dazu gehören m:n-Beziehungen, die vorerst in Form von zwei 1:n-Beziehungen abzubilden sind, aber auch Lazy Loading, die Unterstützung für Stored Procedures oder Custom Conventions. Auch auf komplexe Typen zum Gruppieren mehrerer Eigenschaften einer Entität sowie auf die Vererbungsstrategien TPT und TPC müssen Core-Entwickler vorerst verzichten. Zum Abbilden von Vererbungsbeziehungen können sie jedoch auf die Strategie TPH zurückgreifen, die alle Typen einer Vererbungshierarchie in einer einzigen Tabelle platziert. Ebenso muss man noch ein wenig auf NoSQL-Provider warten, die aufgrund von Zeitdruck hinten angestellt wurden. Auch gilt es beim Aktualisieren des Codes nach Datenbankänderungen Abstriche zu machen, denn die Werkzeuge zum Reverse-Engineering generieren derzeit immer ein komplett neues Modell. Somit muss der Entwickler nach dem Aktualisieren der Datenbank händisch eingreifen. Die meisten Möglichkeiten, die in EF Core 1.0 fehlen, sollen mit späteren Versionen nachgereicht werden. Aus diesem Grund empfiehlt Microsoft für klassische Szenarien nach wie vor Version 6. Nur wer auf .NET Core setzt, soll – mangels Alternativen – heute schon zu EF Core 1.0 greifen. Auch wenn das Team in Redmond für Entity Framework 6 weitere (Wartungs-)Releases bereitstellen wird, liegt sein Fokus auf dem Core-Strang. Wie im .NET-Umfeld üblich, kommt EF Core 1.0 per NuGet. Da das NuGet-Paket EntityFramework.Com­ mands sämtliche nötigen NuGet-Pakete referenziert, kann man sich auf dieses Paket beschränken. Zusätzlich ist ein weiteres Paket für die gewünschte Datenbank einzubinden. Dies gilt nun auch für Microsofts Hausdatenbank SQL Server. Die vom hier betrachteten RC1 gebotenen Provider finden sich in Tabelle 2. Wie auch schon bei früheren Versionen ist davon auszugehen, dass einzelne Datenbankhersteller sowie Anbieter für Drittkomponenten Provider für weitere Datenbanken anbieten werden. Darüber hinaus möchte auch das Produktteam noch ein paar zusätzliche Provider für nicht relationale Datenbanken hinzufügen. m:n-Beziehungen Lazy Loading Nur TPH-Vererbung Komplexe Typen Custom Conventions Stored Procedures Update from Datebase NoSQL-Provider Tabelle 1: Einschränkungen in Entity Framework Core 1.0 Datenbank NuGet-Paket SQL Server EntityFramework.MicrosoftSqlServer PostgreSQL EntityFramework7.Npgsql SQLite EntityFramework.Sqlite In-Memory (zum Testen) EntityFramework.InMemory Konfiguration Die Konfiguration eines DbContext in EF Core 1.0 erfolgt auf den ersten Blick wie gewohnt: Der Entwickler überschreibt OnModelCreating und gibt dem übergebenen ModelBuilder die nötigen Informationen bekannt (Listing 1). Nach genauerem Betrachten fällt jedoch auf, dass so manche bekannte Methode nun „nur mehr“ eine Erweiterungsmethode ist. Ein Beispiel dafür ist To­Ta­ ble, die eine Klasse auf eine Tabelle abbildet. Der Grund hierfür ist ein pragmatischer: In NoSQL-Lösungen gibt es häufig keine Tabellen, und aus diesem Grund macht es auch keinen Sinn, ToTable direkt in das API aufzunehmen. Ein weiteres Beispiel dafür ist die neue Methode Besuchen Sie auch folgende Session: Entity Framework 6 – so schnell wie ein Tiroler beim Skifahren Christian Giesswein Gerade Entity Framework gilt als Allheilmittel, wenn es um die Anbindung einer Datenbank im .NET-Bereich geht. Doch kaum geht es etwas über das kleine Beispielprojekt hinaus, fangen die Probleme an. Die Performance geht in den Keller, der Kunde ist unzufrieden, als Entwickler wird einem vom Chef der Kopf gewaschen und niemand ist zufrieden. Doch gerade mit den fünf Tipps, die ich Ihnen in dieser Session auf den Weg geben möchte, wird Ihre Anwendung so schnell wie ein Tiroler beim Skifahren – versprochen. Tabelle 2: NuGet-Pakete für Datenbankprovider www.basta.net 43 DOSSIER Data Access & Storage HasSequence, die den Namen einer Sequenz für die Generierung von Autowerten bekannt gibt. Einen ähnlichen Ansatz wählt das Produktteam auch für Aspekte, die nur in ganz bestimmten Datenbankprodukten zu finden sind. Hier nutzt es auch Erweiterungsmethoden, die jedoch den jeweiligen Produktnamen widerspiegeln. Als Beispiel hierfür präsentiert Listing 1 die Methode ForSqlServerIsClustered. Eine weitere Neuerung ist die ebenfalls zu überschreibende Methode OnConfiguring, die ein DbContextOp­ tionsBuilder-Objekt entgegennimmt. Damit kann der Entwickler die adressierte Datenbank bekannt geben. Auch hierzu nutzt er Erweiterungsmethoden. Diese finden sich in den NuGet-Paketen der einzelnen Provider. Wer den DbContextOptionsBuilder nicht direkt im Kontext konfigurieren möchte, kann dies auch beim Start der Anwendung machen und den OptionsBuilder via Dependency Injection zur Verfügung stellen. Gerade beim Einsatz von ASP.NET Core 1.0 bietet sich dieser Weg an, zumal Dependency Injection dort omnipräsent ist. Shadow Properties Ein in der Vergangenheit immer wieder nachgefragtes Feature ist die Unterstützung von Spalten, die erst nach dem Kompilieren hinzugekommen sind. Das ist zum Beispiel bei Anwendungen der Fall, die Spalten dynamisch einrichten. EF Core 1.0 löst diese Herausforderung über Shadow Properties. Das sind Properties, die dem Kontext bekannt sind, sich jedoch nicht innerhalb der Entitäten Listing 1 protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Flight>().ToTable("Flights"); modelBuilder.HasSequence("Seq"); modelBuilder.Entity<Flight>() .HasKey(p => p.Id).ForSqlServerIsClustered(); [...] } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Data Source=[…]"); } wiederfinden. Um Shadow Properties zu nutzen, gibt die Anwendung diese zunächst beim Konfigurieren des Kontexts innerhalb der Methode OnModelCreating bekannt: modelBuilder.Entity<Flight>().Property(typeof(String), "PlaneType"); Nach dem Abfragen einer Entität kann die Anwendung solche Eigenschaften über den jeweiligen Change-Tracker-Eintrag in Erfahrung bringen. Diesen erhält sie, wie in vergangenen Versionen auch, mit der Methode Entry (Listing 2). Um Shadow Properties über LINQ-Abfragen zu nutzen, verwendet die Anwendung stellvertretend dafür die statische Methode EF.Properties. Diese ist mit dem Typ und dem Namen der zu adressierenden Eigenschaft sowie mit der jeweiligen Entität zu parametrisieren: Besuchen Sie auch folgende Session: Analyse von SQL Server auf Performanceengpässe Uwe Ricken Zum Tagesgeschäft eines DBA sowie von Beratern, die in Sachen „Microsoft SQL Server“ unterwegs sind, gehört die sorgfältige Analyse von SQL-Server-Systemen, wenn das Business oder der Kunde Performanceprobleme meldet. In dieser Session wird an Hand von konkreten Beispielen ein Microsoft SQL Server auf verschiedene Problemfälle geprüft, die zu Engpässen führen, wenn die Einstellungen nicht korrekt sind. Die verschiedenen Analysen werden durch Beispiele mit Erläuterungen zu den möglichen Auswirkungen demonstriert. Gleichzeitig wird ein Lösungsplan erarbeitet, an dem sich der Teilnehmer von den richtigen Einstellungen im Betriebssystem zu den Konfigurationseinstellungen in Microsoft SQL Server durcharbeiten kann. Last but not least werden Lösungswege für den Fall aufgezeigt, wenn die Performance innerhalb einer einzelnen Datenbank als nicht ausreichend bewertet wird. Alle möglichen Performanceengpässe werden auf einem installierten Microsoft SQL Server mittels Skripten simuliert und ausgewertet. Die folgenden Analyseschritte werden behandelt: • Einstellungen im Betriebssystem überprüfen und bewerten • Analyse der Datenbanken auf Konfiguration, Nutzung und Systemauslastung Listing 2 using (FlugDbContext ctx = new FlugDbContext()) { var flight = ctx.Flights.First(f => f.Id == 1); var planeType = ctx.Entry(flight).Property("PlaneType").CurrentValue; Console.WriteLine("PlaneType: " + planeType); } www.basta.net • Konfiguration und Analyse von TEMPDB • Waits and Latches: Worauf wartet Microsoft SQL Server und welchen Einfluss haben die verschiedenen Wartevorgänge auf die Performance der Anwendungen? • Index Maintenance – wie werden Indexe verwendet, welche Indexe fehlen, wie ist der physikalische Zustand von Indexen zu bewerten? 44 DOSSIER Data Access & Storage Abb. 1: Kombination aus nativem SQL und LINQ var flights = ctx.Flights .Where(f => EF.Property<string>(f, "PlaneType") == "Airbus") .ToList(); Natives SQL (raw SQL) Nicht jede Anforderung lässt sich mit OR Mapper lösen. Wenn alle Stricke reißen, hilft nur noch der Griff zu nativem SQL. Dies erlaubt Entity Framework zwar schon seit seinen ersten Tagen, Core 1.0 kann nun aber auch natives SQL mit einer LINQ-Anfrage kombinieren. Auf diese Weise entsteht eine neue Abfrage, die es komplett in der Datenbank ausführt. Ein Beispiel dafür findet sich in Listing 3. Es legt mit FromSql den auszuführenden nativen Listing 3 using (FlugDbContext ctx = new FlugDbContext()) { var flight = ctx.Flights .FromSql("select * from dbo.GetFlights(@p0, @p1)", "Vienna", "London") .Where(f => f.Date > DateTime.Today) .ToList(); Console.WriteLine("Count: " + flight.Count); } Listing 4 using (FlugDbContext ctx = new FlugDbContext()) { ctx.LogToConsole(); var flight = ctx.Flights .Where(f => f.Date >= DateTime.Today && CheckRoute(f, "Vienna-London")) .ToList(); // ... do stuff ... } www.basta.net Abb. 2: Ausführung einer Teilabfrage am Server SQL-Code fest. Dabei handelt es sich hier um den Aufruf einer Table-Valued Function (TVF). Darin befinden sich auch Parameter, die per Konvention die Namen p0 bis pN aufweisen. Die Werte für diese Parameter finden sich im zusätzlich an FromSql übergebenen Parameterarray. An die native Abfrage schließt das Beispiel per LINQ mit Where eine Einschränkung an. Ein Blick in den Profiler zeigt, dass Entity Framework die beiden Teile tatsächlich zu einer einzigen Abfrage zusammenfährt und sie an die Datenbank sendet (Abb. 1). Clientseitige Abfrageausführung EF Core 1.0 unterstützt nun auch Fälle, in denen es LINQ-Abfragen nicht vollständig nach SQL übersetzen kann. Dazu teilt es die Abfrage in einen serverseitigen und einen clientseitigen Teil. Ersterer wird nach SQL übersetzt und zur Datenbank gesendet. Letzterer wird zum Filtern der daraufhin erhaltenen Ergebnismenge am Client genutzt. Ein Beispiel dafür findet sich in Listing 4. Da Entity Framework keine serverseitige Entsprechung für den Aufruf der Methode Check­Route kennt, entscheidet es sich, sie am Client auszuführen. Auch hier beweist ein Blick in den Profiler, dass lediglich die Einschränkung auf das Datum in der Datenbank zur Ausführung kommt (Abb. 2). Da eine clientseitige Filterung zu Performanceproblemen führen kann, gibt Entity Framework im Zuge dessen eine Warnung ins Protokoll aus (Abb. 3). Um an das Protokoll zu kommen, ist ein Logger für Entity Framework zu registrieren. Das erfolgt an und für sich per Dependency Injection. Zur Vereinfachung kommt im betrachteten Beispiel stattdessen die benutzerdefinierte Methode LogToConsole zum Einsatz, die im Quellcode zu diesem Text unter [1] zu finden ist. Wer auf Nummer sicher gehen möchte, kann die clientseitige Ausführung von Abfragen auch deaktivieren. Dazu ruft er in der oben besprochenen Methode On­ Configuring nach dem Registrieren des Providers die Methode DisableQueryClientEvaluation auf: optionsBuilder .UseSqlServer(@"Data Source=[...]") .DisableQueryClientEvaluation(); 45 DOSSIER Data Access & Storage Dieser Aufruf veranlasst EF Core 1.0 zum Auslösen einer Ausnahme, wenn es eine LINQ-Abfrage nicht vollständig nach SQL übersetzen kann. Syntaxzucker für Eager Loading Eine kleine, aber feine Neuerung bringt .NET Core 1.0 im Bereich Eager Loading: Neben der Methode Inclu­ de, die einen direkten Nachbarn der adressierten Entität adressiert, steht nun auch eine Methode ThenInclude zur Verfügung. Diese bezieht sich auf den zuletzt referenzierten Nachbarn und gibt einen seiner Nachbarn an. Auf diese Weise lassen sich indirekte Nachbarn per Eager Loading laden. Listing 5 demonstriert das, indem es Flüge samt dessen Buchungen und den Passagieren der Buchungen per Eager Loading anfordert. Das Laden indirekter Nachbarn war zwar auch schon in früheren Versionen von Entity Framework möglich, allerdings war dazu ggf. eine etwas eigenwillige Schreibweise notwendig. Das hier betrachtete Beispiel hätte zum Beispiel wie folgt ausgesehen: var flight = ctx.Flights.Include( f => f.Bookings.Select(b => b.Passenger)).Where(...).ToList(); Dieses Beispiel bildet innerhalb von Include die Auflistung Bookings mit Select auf ein Booking-Objekt ab. Dieser Kunstkniff war notwendig, da die BookingsAuflistung im Gegensatz zu dessen Objekten nicht die gewünschte Eigenschaft Passenger aufweist. Unterstützung für getrennte Entitäten Die Methode Add markierte in vergangenen Versionen nicht nur das übergebene Objekt für das Einfügen, sondern den gesamten Objektgraphen, der über dieses Objekt erreichbar war. Im betrachteten RC1 kann der Aufrufer dieses Verhalten abschalten. Übergibt er den Wert Graph­ Behavior.SingleObject markiert Add lediglich das übergebene Objekt, das den Zustand Added erhält: Abb. 3: Warnung bei clientseitiger Ausführung sucht diese Methode die Zustände sämtlicher Objekte des Graphen auf Added oder Modified zu setzen. Um sich für einen dieser Zustände zu entscheiden, greift sie auf eine simple Heuristik zurück. Hat die Entität bereits einen Primärschlüssel, geht sie von einer bestehenden Entität aus und setzt ihren Zustand auf Modified. Ansonsten nimmt sie an, dass es sich um eine neue Entität handelt. Dies führt zum Zustand Added: ctx.Flights.Update(flight); Dass diese Strategie nur funktioniert, wenn die Datenbank beim Einfügen den Primärschlüssel vergibt, ist selbstredend. Wer nur das übergebene Objekt und nicht den gesamten Objektgraphen betrachten möchte, kann auch hier GraphBehavior.SingleObject übergeben. Mehr Kontrolle über die Zustände von wieder angehängten Objekten erlangt der Entwickler mit der neuen Methode TrackGraph: ctx.ChangeTracker.TrackGraph(flight, (node) => { // object entity = node.Entry.Entity; node.Entry.State = EntityState.Modified; }); Diese Methode nimmt ein Objekt sowie einen LambdaAusdruck entgegen. Sie durchläuft den gesamten Objektgraphen und übergibt die Change-Tracker-Einträge ctx.Flights.Add(f1, GraphBehavior.SingleObject); Getrennte Objekte können nun auch mittels Update wieder angehängt werden. Im Gegensatz zur bekannten Methode Attach, die nach wie vor vorhanden ist, ver- Listing 5 using (FlugDbContext ctx = new FlugDbContext()) { var flight = ctx.Flights .Include(f => f.Bookings) .ThenInclude(b => b.Passenger) .Where(f => f.From == "Vienna") .ToList(); // ... do stuff ... } www.basta.net Custom Conventions Marke Eigenbau Auch wenn Entity Framework Core 1.0 keine Unterstützung für Custom Conventions bietet, kommt der hier betrachtete Release Candidate 1 mit einer einfachen Alternative: Über die Eigenschaft Model des ModelBuilders lassen sich Metadaten zum Modell abrufen. Iteriert man diese Metadaten, lassen sich sämtliche Entitäten anhand einer Konvention konfigurieren. Das nachfolgende Beispiel nutzt dies, um für sämtliche Entitäten einen Tabellennamen festzulegen: foreach (var entity in modelBuilder.Model.GetEntityTypes()) { modelBuilder.Entity(entity.Name).ToTable(entity.ClrType.Name + "s"); } 46 DOSSIER Data Access & Storage der einzelnen Objekte nach und nach an den LambdaAusdruck. Dieser kann nun den Eintrag sowie die Entität betrachten und den Zustand entsprechend einer eigenen Logik setzen. Alternate Keys Neben Primärschlüsseln unterstützt EF Core 1.0 nun auch alternative Schlüssel (Alternate Keys). Dabei handelt es sich um eindeutige Schlüssel, auf die Fremdschlüssel verweisen können. Listing 6 demonstriert den Umgang damit. Es definiert für die Entität Flight neben dem Primärschlüssel Id einen alternativen Schlüssel FlightNum­ ber. Zusätzlich richtet es einen Fremdschlüssel ein, der den alternativen Schlüssel referenziert. Dazu nutzt es die Methode HasPrincipalKey. Ohne diese Methode würde Entity Framework davon ausgehen, dass sich der Fremdschlüssel auf den Primärschlüssel von Flight bezieht. Verbesserungen bei der Abfrageausführung EF Core 1.0 versucht nun auch, performantere Abfragen zu erzeugen. Beispielsweise realisiert es nicht mehr ausnahmslos Eager Loading mittels Joins. Stattdessen greift es in manchen Fällen auf mehrere in einem Batch übersendete Abfragen zurück, um eine unnötige Auskreuzung der benötigten Daten zu vermeiden. Auch für DML-Operationen nutzt Core 1.0 Batches und verringert somit die Anzahl der an die Datenbank zu übersendenden Nachrichten. Fazit Dank EF Core 1.0 können .NET-Teams in .NET-CoreProjekten und somit auch unter Linux, Mac OS X und in Universal-Windows-Apps ihre lieb gewonnene Datenzugriffstechnologie nutzen. Das bedingt ein Neuschreiben des Frameworks und öffnet somit auch die Türen für das Beseitigen von Mehrgleisigkeiten. Dies scheint eine gute Entscheidung zu sein, zumal sich in den letzten Jahren einige Spielarten in Entity Framework angesammelt haben. Durch den Fokus auf den DbContext und Code First konzentriert sich das Produktteam fortan auf die modernste Art der Nutzung von Entity Framework. Dieses Verfahren ist im Übrigen auch mit jenem vergleichbar, das NHibernate seit vielen Jahren erfolgreich nutzt. Die Zeiten, in denen der Entwickler mit einem schwerfälli- gen Designer ein XML-Mapping warten musste, gehören somit der Vergangenheit an. Wichtig dabei ist zu betonen, dass Code First auch den Start mit einer bestehenden Datenbank via Reverse-Engineering erlaubt. Die Unterstützung für NoSQL-Lösungen ist ebenfalls erfreulich. Somit können Entity-Framework-Entwickler ihr vorherrschendes Entity-Framework-Know-how zum Zugriff auf solche Datenbanken nutzen. Das bedeutet jedoch nicht, dass sie eine relationale Datenbank ohne Weiteres gegen ein NoSQL-Gegenstück tauschen können – die Unterschiede zwischen diesen Lösungen sind einfach zu groß. Auch wenn Microsoft das API so weit wie möglich aus dem Vorgänger Entity Framework 6.x übernimmt, wird es die eine oder andere Änderung geben, und eine automatische Portierung erscheint somit auch schwierig. Dazu kommt, dass EF Core 1.0 vorerst nicht alle Features von Entity Framework 6 mitbringen wird und Version 6 somit die empfohlene Version bleibt. Lediglich Entwickler, die für .NET Core entwickeln müssen, kommen mangels Alternativen nicht an EF Core 1.0 vorbei. Allerdings liegt der Fokus bei der Weiterentwicklung auf dem Core-Strang, und somit wird in absehbarer Zukunft EF Core 1.0 den Stellenwert haben, den heute Version 6 genießt. Manfred Steyer (www.softwarearchitekt.at) ist selbstständiger Trainer und Berater mit Fokus auf moderne Web- und Servicearchitekturen. Er ist Mitglied im Expertennetzwerk www.IT-Visions. de und schreibt für O’Reilly und Heise. In seinem aktuellen Buch „Moderne Datenzugriffslösungen mit Entity Framework 6“ behandelt er die vielen Seiten des OR Mappers aus dem Haus Microsoft. Links & Literatur [1]https://github.com/manfredsteyer/ef7-rc-samples/tree/master Besuchen Sie auch folgende Session: Offline First mit Angular und SQL Server – Es geht auch ohne Netz Thorsten Hans Listing 6 modelBuilder.Entity<Flight>().HasKey(p => p.Id); modelBuilder.Entity<Flight>().HasAlternateKey(f => f.FlightNumber); modelBuilder.Entity<Flight>() .HasMany(f => f.Bookings) .WithOne(b => b.Flight) .HasForeignKey(b => b.FlightNumber) .HasPrincipalKey(f => f.FlightNumber); www.basta.net Offline First, überall und jederzeit arbeiten, auch ohne Internetverbindung. Eine Anforderung, der sich immer mehr Softwarehersteller trotz 4G und freien WLANs stellen müssen. In diesem Vortrag werden Sie lernen, wie Sie Angular-Anwendungen mit Web-API und SQL Server Backend auch ohne Netz in vollem Funktionsumfang betreiben können: Egal ob iOS, Android, Windows, Linux oder macOS. Sie werden sehen, was Sie bei der Implementierung von Offline First beachten sollten und welche Fallstricke es aus dem Weg zu räumen gilt. 47 DOSSIER HTML5 & JavaScript TypeScript = JavaScript + x Die Alternative für JavaScript-Hasser An JavaScript scheiden sich die Geister: Die einen lieben die Sprache aufgrund ihrer Flexibilität, die anderen hassen sie aufgrund ihrer Komplexität. Aber auch die Liebhaber müssen feststellen, dass manche JavaScript-Entwickler Programmcode schreiben, der schwer lesbar und nicht gut zu warten ist. Sprachen, die von JavaScript abstrahieren, gibt es schon länger. Microsofts TypeScript setzt sich hier immer stärker durch, vor allem nachdem auch Google mit Angular 2 aufgesprungen ist. von Dr. Holger Schwichtenberg TypeScript hat sich rasant weiterentwickelt: Von der Erstankündigung am 02.10.2013 über die Version 1.0 am 02.04.2014 bis zur aktuellen Version 1.8.10 vom 09.04.2016. Maßgeblich an der Entwicklung von Type­ Script beteiligt ist Anders Hejlsberg, der Schöpfer von Turbo Pascal, Delphi und C#. Ein wichtiger Meilenstein für TypeScript war der 05.03.2015: An diesem Tag gab Google bekannt, dass das beliebte JavaScript-Framework Angular mit TypeScript entwickelt und dafür die eigene Sprache AtScript aufgegeben wird. TypeScript unterstützt aber nicht nur Angular 2, sondern viele JavaScript-Frameworks, einschließlich React mit seiner besonderen eingebetteten Tagsyntax. Werkzeuge Der TypeScript-Compiler liegt selbst in JavaScript vor (tsc.js). Für Windows gibt es eine direkt ausführbare Version tsc.exe. Den Compiler bekommt man von www. nuget.org [1] oder dem Node Package Manager [2]. In Visual Studio 2013 und Visual Studio 2015 wird der TypeScript-Compiler mitgeliefert (siehe Verzeichnis C:\ Program Files (x86)\Microsoft SDKs\Type­Script\). Da der Compiler häufig aktualisiert wird, sollte man immer die aktuellsten Visual-Studio-Gesamtupdates bzw. die Updates des TypeScript-Compilers einzeln installieren. www.basta.net Der TypeScript-Compiler erzeugt aus dem TypeScriptProgrammcode dann JavaScript-Programmcode (Abb. 1). Diese Transformation von TypeScript-Syntax zu JavaScript-Syntax kann zu drei Zeitpunkten erfolgen: 1. Nach jedem Speichern einer TypeScript-Datei 2. Im Rahmen der Übersetzung eines Projekts 3. Zur Laufzeit Bei den JavaScript-Versionen für das Kompilat hat der Entwickler die Wahl zwischen den Standards ECMA­ Script Version 3, ECMAScript Version 5 und dem aktuellen ECMAScript Version 2015 (alias Version 6). Bei der Ausgabe von Modulen hat der Entwickler inzwischen sogar fünf Modulformate zur Wahl: Asynchronous Module Design (AMD), CommonJS, Universal Module Definition (UMD), SystemJS und ECMAScript 2015 (ES6) (Abb. 1). In Visual Studio wählt der Webentwickler die wichtigsten Compileroptionen komfortabel in den Projekteinstellungen in Webprojekten (Abb. 2). Leider sind nicht alle TypeScript-Compiler-Optionen auf diesem Wege verfügbar. Manche Einstellungen (z. B. die Aktivierung einiger noch als „experimentell“ geltenden Sprachfeatures, wie Dekoratoren) muss man, wie in Listing 1, durch einen Eintrag in der XML-Projektdatei direkt vornehmen. 48 DOSSIER HTML5 & JavaScript Abb. 1: Der TypeScript-Compiler erzeugt aus den TypeScript-Dateien dann JavaScript-Dateien und außerdem noch Abbildungsdateien für den Debugger Der zweite Weg für die TypeScript-Compiler-Konfiguration ist, dem Projekt eine tscon­ fig.js-Datei hinzufügen (Listing 2). Dazu kann man in Visual Studio ein neues Element vom Typ TypeScript JSON Configura­tion File anlegen. Die von dem TypeScript-Compiler erzeugten .js-Dateien sind im Standard nicht Teil des Visual-Studio-Projekts. Man kann sie wie üblich über die Option Show all files im Solution Explorer einblenden. Fehler bei der TypeScript-Kompilierung landen in der Error List – wie man es erwartet. In eine Webseite einbetten muss man immer die .js-Datei. Visual Studio hilft hier mit: Beim Drag and Drop einer .ts-Datei in eine .html-Datei erstellt die IDE ein <script>-Tag mit Verweis auf die .jsDatei statt auf die .ts-Datei. Visual Studio bietet auch Debugging: Bei einem Laufzeitfehler oder einem Haltepunkt in der Type­Script-Datei zeigt die Entwicklungsumgebung im Debugger nicht den eigentlich ausgeführten JavaScript-Programmcode, sondern den ursprünglichen TypeScript-Programmcode an. Der Entwickler kann diesen wie üblich zeilenweise durchlaufen. Die für dieses komfortable Debugging notwendige Beziehung zwischen den Zeilen in der ausgeführten JavaScript-Datei und in der ursprünglichen TypeScript-Datei erhält Visual Studio durch eine Source-MapDatei (.map), die ebenfalls im Rahmen der TypeScript-Kompilierung entsteht (Abb. 1). Wer mit Visual Studio Code oder anderen „einfacheren“ Editoren arbeiten will, muss auf die Kommandozeilenoptionen zurückgreifen. Der folgende kurze Kommandozeilenbefehl sorgt dafür, dass zwei Type­Script-Dateien immer beim Speichern in ECMA­Script 5 und das Modulformat AMD übersetzt werden: tsc datei1.ts datei2.ts --watch --target es5 --module amd www.basta.net Abb. 2: Projekteinstellungen für TypeScript in Webprojekten in Visual Studio 2015 Listing 1: Ausschnitt aus den TypeScript-CompilerEinträgen in „.csproj“-Datei <PropertyGroup Condition="'$(Configuration)' == 'Debug'"> <TypeScriptExperimentalDecorators>true</TypeScriptExperimentalDecorators> <TypeScriptEmitDecoratorMetadata>true</TypeScriptEmitDecoratorMetadata> <TypeScriptTarget>ES6</TypeScriptTarget> ... </PropertyGroup> Listing 2: Beispiel für eine „tsconfig.js“-Datei "target": "es2015" }, "exclude": [ "node_modules", "wwwroot" ] { "compilerOptions": { "experimentalDecorators": true, "noImplicitAny": false, "noEmitOnError": true, "removeComments": false, "sourceMap": true, } 49 DOSSIER HTML5 & JavaScript --init. Damit legt man im aktuellen Verzeichnis eine tsconfig.js an, über die man dann den Compiler steuern kann. Übersetzung in JavaScript Die Sprache TypeScript stellt eine Übermenge von JavaScript da, das heißt, (fast) jede JavaScriptDatei ist auch gültiger Type­ ScriptProgrammcode. Das ist hilfreich für den Umstieg von JavaScript auf TypeScript bzw. für die Nutzung bestehenden JavaScript-Programmcodes aus Büchern und dem Internet. Allerdings steht in dem Satz oben bewusst ein „fast“: Denn den JavaScriptProgrammcode in Listing 3 (eine Filterdefinition für AngularJS) mag der TypeScript-Compiler nicht. Er bemängelt: error TS2322: Type 'string' is not assign­able to type 'number'. Das liegt daran, dass der Type­ScriptCompiler durch die Zeile var num = parseInt(n, 10) der Variablen num auf dem Wege der automatischen Typherleitung bereits den Typ number zuweist. Dann später hat der Autor Abb. 3: Onlineeditor und Ausführungsumgebung bilden eine „Spielwiese für TypeScript“ [4] dieses JavaScript-Codes [3] die Variable num nun leider als Zeichenkette Die Option --watch gibt es leider nur, wenn man tsc. verwendet. In JavaScript ist sowas möglich – aber nacmd aus Node.js-Paketen zum Kompilieren verwendet. türlich schlechter Programmierstil. Gut programmiertes Die tsc.exe, die in Visual Studio enthalten ist, kann JavaScript ist kein Problem für den TypeScript-Compidas Dateisystem leider nicht überwachen, dafür küm- ler. Auch in diesem Fall kann man die Situation retten, mert sich Visual Studio selbst um die Überwachung. indem man in Type­Script explizit num auf den Typ any Eine weitere wichtige Kommandozeilenaktion ist tsc deklariert: var num : any = parseInt(n, 10). Microsoft hat sich bei der TypeScript-Syntax von Anfang an bemüht, nahe am ECMAScript-2015-Standard zu sein, d. h., bei der Ausgabe in ECMAScript 2015 hat Listing 3: Diesen JavaScript-Code bemängelt der Compiler nur ganz wenig zu tun. Je niedriger die der Type­Script-Compiler Zielversion jedoch ist, desto mehr Programmcode muss // Quelle: http://jsfiddle.net/TheRoks/8uCT9/ der Compiler erzeugen, um die fehlenden JavaScriptvar app = angular.module('myApp', []); Sprachfeatures nachzubilden. Ein guter erster Anlaufpunkt für Neueinsteiger in Tyapp.filter('numberFixedLen', function () { peScript ist die interaktive, browserbasierte Codeumreturn function (n, len) { wandlung, die Microsoft unter [4] bietet: Während man var num = parseInt(n, 10); in der linken Fensterhälfte von Abbildung 3 Type­Script erlen = parseInt(len, 10); fasst, sieht man rechts schon das JavaScript-Ergebnis. Abif (isNaN(num) || isNaN(len)) { bildung 3 zeigt eine Vererbungshierarchie mit drei Klassen return n; in TypeScript und rechts das deutlich längere Pendant in } JavaScript. Man sieht sofort: TypeScript ist hier wesentnum = '' + num; lich prägnanter und eleganter. In dieser Webanwendung while (num.length < len) { kann man leider die JavaScript-Zielversion nicht wählen; num = '0' + num; die Ausgabe ist aktuell immer ECMAScript 5. } return num; }; }); www.basta.net Datentypen Das Typsystem von TypeScript unterscheidet zwischen primitiven und komplexen Typen. Zu den primitiven 50 DOSSIER HTML5 & JavaScript var Website3; // dies prägt Webseite3 auf any Website3 = " https://www.typescriptlang.org" Website3 = 456; // Dies ist hier möglich, da die Variable Website3 den Typ "any" erhalten hat Abb. 4: IntelliSense für typisierte Variablen in TypeScript Typen zählen number, boolean, string und void. Daneben existiert noch der Datentyp any, der jeden beliebigen Wert aufnehmen kann und somit das StanAbb. 5: Keine IntelliSense dardverhalten von Variablen für untypisierte Variablen in JavaScript widerspiegelt. Zu in JavaScript den komplexen Typen zählen Typen, die aus der Deklaration von Schnittstellen und Klassen hervorgehen. Im Gegensatz zu JavaScript kann beim Deklarieren einer Variablen mit var ein Datentyp angegeben werden. Dazu wird der Datentyp der Variablen mit einem Doppelpunkt nachgestellt. Im Zuge dessen kann der Entwickler bereits auch einen Initialwert angeben. Die TypeScript Language Specification [5] nennt auch null und undefined als Typen; diese kann man aber nicht in gleicher Weise nach dem Doppelpunkt angeben, sondern lediglich als Wert nach dem Gleichheitszeichen verwenden: var id: number = 123; var name: string = "Holger Schwichtenberg"; var autor: boolean = true; Auch wenn die explizite Typangabe fehlt, wird eine Variable durch die Initialisierung während der Deklaration geprägt (Typherleitung). In folgendem Codefragment kommt es daher in Zeile 2 zum Fehler Cannot convert 'number' to 'string': var Website1 = "https://entwickler.de "; Website1 = 456; // falsch !!! Etwas kurios ist dabei, dass der TypeScript-Compiler den Fehler zwar ausgibt, aber dennoch kompiliert und die monierte Zeile so nach JavaScript übernimmt, dass zur Laufzeit tatsächlich die Zahl 456 in der Variablen Website1 steht. Erst seit Version 1.4 des TypeScriptCompilers gibt es die Option noEmitOnError, die die 1:1-Ausgabe bei Kompilierfehlern verhindert. Wenn der Entwickler in TypeScript tatsächlich eine Variable will, deren Typ sich ändern darf, muss er any verwenden: var Website2: any = "http://www.dotnet-doktor.de"; Website2 = 456; Alternativ dazu erhält er eine any-Variable, wenn die Deklaration keine Initialisierung enthält: www.basta.net Microsoft hat sich in den letzten Jahren sehr bemüht, auch JavaScript-Entwickler mit IntelliSense zu unterstützen. Aufgrund der fehlenden Typisierung kann dies aber immer nur begrenzt möglich und begrenzt präzise sein. Abbildung 4 und 5 zeigen in der Gegenüberstellung schon an einem einfachen Beispiel eindrucksvoll, dass Visual Studio für TypeScript eine bessere Eingabe­ unterstützung anbietet als für JavaScript. Seit TypeScript 1.6 gibt es alternativ zu var auch die Schlüsselwörter let und const aus ECMAScript 2015: beide beschränken die Deklaration auf den aktuellen Block: if (...) { var x: number = 5; } print("x=" + x); // Ausgabe x=5 :-( nur Warnung if (...) { let y: string = "Holger Schwichtenberg"; } print("y=" + y); // Fehler! const z: boolean = true; z = false; // Fehler! Seit Version 1.4 von TypeScript gibt es Union Types. Sie bezeichnen eine Deklaration, bei der man einer Variablen zwei oder mehr verschiedene Typen zuweisen und damit in bestimmten Situationen die Deklaration auf any vermeiden kann: Besuchen Sie auch folgende Session: TypeScript für .NET-Entwickler Christian Wenz Mit TypeScript macht sich Microsoft daran, das – für viele Entwickler aus dem eigenen Kosmos – ungewohnte JavaScript zugänglicher zu machen, indem beispielsweise statisches Typing und bestimmte OOP-Features hinzugefügt werden. Nach einigen Anlaufschwierigkeiten hat TypeScript inzwischen auch außerhalb der MicrosoftWelt Traktion erhalten. Viel besser noch: Angular setzt auf TypeScript! Es ist also höchste Zeit, sich mit der Sprache zu beschäftigen. Diese Session stellt die Features von TypeScript vor und geht dabei auch auf die Toolunterstützung seitens Visual Studio und Co. ein. Damit sind Sie auch für die Entwicklung von Anwendungen auf Basis von Angular bestens gewappnet. 51 DOSSIER var mitarbeiterID: number | string; mitarbeiterID = 123; // OK mitarbeiterID = "A123"; // auch OK mitarbeiterID = true; // Fehler! Das folgende Fragment zeigt eine Typprüfung und eine Typkonvertierung. Für Letztere gibt es zwei Syntaxformen <typ>variable oder variable as typ: var eingabe: any = 5; if (typeof eingabe === 'number') { var zahl1 = (<number>eingabe) + 1; // alternativ: var zahl2 = (eingabe as number) + 2; } Arrays und Tupel Auch für typisierte Arrays gibt es in TypeScript zwei Syntaxformen: Array<number> oder number[]. Listing 4 zeigt weitere Besonderheiten: •Die for…of-Schleife (aus ECMAScript 2015, entspricht foreach in C#/Visual Basic .NET) •Das elegante Aufbrechen von Arrays in einzelne Variablen mit dem Sprachfeature Destructuring Seit Version 1.3 beherrscht TypeScript auch Tupel. Das sind Wertemengen, in denen jeder einzelne Wert auf einen anderen Typ deklariert sein kann. Das folgende Fragment zeigt ein Tupel, das aus einer Zahl, einer Zeichenkette und einem Bool-Wert besteht: HTML5 & JavaScript Klassen Das Herzstück von TypeScript sind Klassen. ECMA­ Script 2015 kann auch Klassen, aber mit TypeScript kann man Klassen auch in älteren ECMAScript-Versionen nutzen. Wie Listing 5 zeigt, werden Klassen, wie in C#, mit dem Schlüsselwort class eingeleitet. TypeScript erlaubt, ähnlich wie C# oder Java, die Definition von Schnittstellen. Um anzugeben, dass eine Klasse eine Schnittstelle implementiert, verwendet der Entwickler das Schlüsselwort implements. In Listing 5 implementiert die Klasse Kontakt die zuvor definierte Schnittstelle IKontakt. Im Gegensatz zu vielen anderen objektorientierten Sprachen ist die standardmäßige Sichtbarkeit immer public. Daneben kann der Entwickler Mitglieder mit pri­vate als nicht von außen nutzbare Mitglieder deklarieren. Seit Version 1.3 gibt es auch protected (nicht nutzbar von außen, aber nutzbar von angeleiteten Klassen). Konstruktoren werden mit dem Schlüsselwort con­ structor eingeleitet. Eine wichtige Besonderheit gibt es hier jedoch: Die Sichtbarkeit public, private oder pro­ tected darf man in Konstruktorparametern verwenden. Wenn die Parameter des Konstruktors so eingeleitet werden, werden diese Parameter automatisch zu Instanzmitgliedern. Das in anderen OO-Sprachen notwendige Umkopieren der Werte aus den Konstruktorparametern in die Instanz erfolgt automatisch. Eine Konstruktorimplementierungszeile wie this.id = id ist in TypeScript also überflüssig. Es ist nicht erlaubt, die in den Konstruktorparametern verwendeten Bezeichner nochmals für andere Properties der Klasse zu verwenden. Microsoft hatte var PersonTupel: [number, string, boolean]; PersonTupel = [123, "Holger S.", true]; var personName = PersonTupel[1]; Listing 4: Arrays in TypeScript type ZahlenArray = number[]; // Array<number> oder number[] var lottozahlen2: ZahlenArray; // Deklaration var lottozahlen: Array<number>; // Initialisierung lottozahlen = [11, 19, 28, 34, 41, 48, 5]; // Schleife über alle Elemente (seit Version 1.5) for (let z of lottozahlen) { print(z); }; // Destructuring (seit Version 1.5) let [z1, z2, z3, z4, z5, z6, zusatzzahl] = lottozahlen; print(`Zusatzzahl: ${zusatzzahl}`); www.basta.net Besuchen Sie auch folgende Session: Effizienter Datenfluss vom Entity Framework über Web-API bis zum JavaScript-Client Dr. Holger Schwichtenberg Es gibt genug Vorträge, die eine Technik detailliert diskutieren. In diesem Vortrag schauen wir hingegen auf die End-to-EndIntegration: Wie schreibt man heutzutage als .NETEntwickler eine Webanwendung und/oder HTML-basierte Cross-Platform-App unter Einsatz der aktuellen Techniken möglichst budgetsparend? Auf dem Server kommen Entity Framework Core und ASP.NET-Core-Web-API-basierte Microservices zum Einsatz, flankiert von Swagger und der zugehörigen Generierungen von Clientcode, den man sonst mühsam schreiben müsste. Der Client nutzt TypeScript und Angular sowie Electron und Cordova. Der .NETund Webexperte zeigt dies anhand eines eindrucksvollen End-to-End-Fallbeispiels. 52 DOSSIER solch ein Sprachfeature auch für C# 6.0 unter dem Namen „Primary Constructors“ in Arbeit, hat es aber leider wieder verworfen. Konstruktoren dürfen auch nicht überladen werden; es kann also pro Klasse nur einen Konstruktor geben. Den Methoden einer Klasse darf der Entwickler nicht das Schlüsselwort function voranstellen. Für alle Zugriffe auf Instanzmitglieder ist in TypeScript innerhalb der Klasse zwingend this voranzustellen. Statische Mitglieder erhalten den Zusatz static. Listing 5 zeigt ein Beispiel mit Namensraum, Schnittstelle, Enumeration, Klassen und Vererbung. Auch JSDoc-Kommentare wurden an einigen Stellen verwendet (aus aber Platzgründen nicht an allen). Um von einer bestehenden Klasse abzuleiten, verwendet der Entwickler, wie unter Java, das Schlüsselwort extends (siehe Klasse Kunde in Listing 5). HTML5 & JavaScript Ebenso kann man mit extends eine Schnittstelle von einer anderen ableiten. Es ist nur Einfachvererbung erlaubt. Anders als in C# und Visual Basic .NET werden Konstruktoren vererbt. Sobald eine Unterklasse aber einen eigenen Konstruktor realisiert, muss zwingend der Konstruktor der Basisklasse mit super() aufgerufen werden (siehe Konstruktor der Klasse Kunde in Listing 5). Alle Typen können in Namensräumen organisiert werden (Listing 5). Die Instanziierung von TypeScript-Klassen erfolgt mit dem Schlüsselwort new. Um zur Laufzeit zu prüfen, ob eine Variable ein Objekt eines bestimmten Typs enthält, kann der Entwickler den Operator instanceof heranziehen. Auch bei der Prüfung gegen eine Basisklasse liefert instanceof den Wert true. Destructuring kann man auch auf Objekte anwenden, wie das folgende Fragment zeigt: Listing 5: Beispiel mit Namensraum, Schnittstelle, Enumeration, Klassen und Vererbung /// <reference path="../TypeScriptHTMLApp/_Hintergründe/lib.d.ts" /> namespace Demo.Interfaces { // Schnittstelle export interface IKontakt { id: number; name: string; geprueft: boolean; erfassungsdatum: Date; toString(details: boolean): string; } // Aufzählungstyp export enum KundenArt { A = 1, B, C } } namespace Demo.GO { // #region Basisklasse /** Kontakt mit id, name, ort * @autor Dr. Holger Schwichtenberg */ export class Kontakt implements Interfaces.IKontakt { // ---------- Properties ohne Getter/Setter public geprueft: boolean; protected interneID: number; private _erfassungsdatum: Date; // ---------- Properties mit Getter/Setter get erfassungsdatum(): Date { return this._erfassungsdatum; } set erfassungsdatum(erfassungsdatum: Date) { this._erfassungsdatum = erfassungsdatum; } /** www.basta.net * @param id Kundenummer * @param name Kundenname */ constructor( public id: number, public name: string) { this.erfassungsdatum = new Date(); this.id = id; // Diese Zeile ist ueberfluessig !!! Kontakt.Anzahl++; } // Öffentliche Methode toString(details: boolean = false): string { { var e: string = `${this.id}: ${this.name}`; return e; } } // Statisches Mitglied static Anzahl: number; } // #endregion // #region Erbende Klasse export class Kunde extends Kontakt { public KundenArt: Interfaces.KundenArt; constructor(id: number, name: string, umsatz: number) { super(id, name); this.KundenArt = Interfaces.KundenArt.C; console.log("Umsatz", umsatz + 1.01); if (umsatz > 10000) this.KundenArt = Interfaces.KundenArt.A; if (umsatz > 5000) this.KundenArt = Interfaces.KundenArt.B; } toString() { return `Kunde ${super.toString()}`; } } // #endregion } // End Namespace 53 DOSSIER var k = new GO.Kunde(123, "H. Schwichtenberg", 99.98); var ausgabe1 = k.toString(); // Typprüfung if (k instanceof GO.Kunde) { ... } if (k instanceof GO.Kontakt) { ... } // Destructuring von Objekten let { id, name } = k; var ausgabe2: string = `${id}: ${name}`; Generische Klassen Bereits die oben gezeigte Syntaxform Array<number> ist der Einsatz einer generischen Klasse. Listing 6 zeigt die Realisierung einer eigenen generischen Klasse und deren Verwendung. Strukturelle Typäquivalenz (Duck Typing) Wie in anderen Sprachen auch kann man eine Variable auch auf eine Schnittstelle typisieren, also statt var k1: GO.Kunde auch var k2: Interfaces.IKontakt ­schreiben und dann anschließend eine Instanz von Kunde zuweisen mit k2 = new GO.Kunde(…);. Im Gegensatz zu Sprachen wie Java oder C# setzt TypeScript aber auf „strukturelle Typäquivalenz“. Das bedeutet, dass Objekte, die einer Variablen zugewiesen werden, nicht in der Vererbungshierarchie passen müssen. Es ist ausreichend, dass das zugewiesene Objekt dieselbe Struktur, sprich dieselben Mitglieder, wie der Typ der Variable aufweist. Man könnte also im Fall von Listing 5 das implements Interfaces.IKontakt auch weglassen und dennoch var k3: Interfaces.IKontakt = new GO.Kunde(…) schreiben. Das folgende Codefragment zeigt, wie der Entwickler auf Basis der strukturellen Typäquivalenz definieren kann, dass eine Variable nur auf ein Objekt mit einer bestimmten Struktur verweisen darf, indem für die Variable k2 definiert wird, dass sie nur für Objekte mit einer Eigenschaft id heranzuziehen ist. Aus diesem Grund kann auch eine Instanz von Kunde zugewiesen werden. Dass die Klasse Kunde neben der id noch weitere Eigenschaften hat, stellt hier kein Hindernis dar: Listing 6: Generische Klasse in TypeScript HTML5 & JavaScript var k2: { id: number; }; k2 = new Demo.GO.Kunde(456, "Holger Schwichtenberg", 99.98); var schluesselwert: number = k2.id; Funktionen und Lambda-Ausdrücke Auch beim funktionalen Programmieren bietet Type­ Script sinnvolle Erweiterungen. Bei der Deklaration von Funktionen kann der Entwickler für die einzelnen Parameter einen Typ definieren. Dasselbe gilt auch für den Rückgabewert der Funktion, dessen Typ der Funktionssignatur nachgestellt wird: function add(a: number, b: number): number { return a + b; } Auch Lamdba-Ausdrücke beherrscht TypeScript. Man kann damit sowohl einen Funktionstyp deklarieren: var add: (a: number, b: number) => number; als auch einen Funktionstyp implementieren: add = (a, b) => a + b; Beides ist natürlich auch in einer Zeile möglich: var print = (s: string) => console.log(s); Ähnlich wie in C# ist auch TypeScript häufig in der Lage, den Typ eines Ausdrucks herzuleiten, ohne dass der Entwickler ihn explizit anführen muss: // Typherleitung: Add liefert number! var add = (a: number, b: number) => a + b; Funktionen können natürlich auch Parameter sein, wie es sich für die funktionale Programmierung gehört. Ein Beispiel dafür zeigt Listing 7. Es definiert eine Funkti- Listing 7: Lambda-Ausdrücke und Clo­sures function filter(objs: Array<number>, callback: (item: number) => boolean): Array<number> { var ergebnismenge: Array<number> = new Array<number>(); for (var i: number = 0; i < objs.length; i++) { export class Verbindung<T1 extends Kontakt, T2 extends Kontakt> { constructor(public k1: T1, public k2: T2) { } public toString(): string { return `${this.k1.name} und ${this.k2.name}`; } } ... } var v = new Demo.GO.Verbindung<GO.Kontakt,GO.Kunde>(k1, k2); var ausgabe2 = v.toString(); var result = filter([1, 2, 3, 4, 5, 6], (item: number) => item % 2 == 0); ausgabe("Anzahl der Elemente in der Ergebnismenge: " + result.length); www.basta.net if (callback(objs[i])) { ergebnismenge.push(objs[i]); } } return ergebnismenge; 54 DOSSIER HTML5 & JavaScript Ein Dekorator ist ähnlich wie eine Annotation in Java oder Attribute in .NET: Der Entwickler zeichnet damit Klassen, Properties, Methoden oder Methodenparameter aus. on filter, die ein übergebenes Array unter Verwendung einer zu übergebenden Funktion filtert. Die Funktion, die an den Parameter callback übergeben wird, nimmt jeweils eine number entgegen und bildet diese auf einen bool ab. Dieser Rückgabewert sagt aus, ob das jeweilige Element Teil der Ergebnismenge sein soll. Die Implementierung der betrachteten Funktion iteriert durch das übergebene Array und ruft für jeden Eintrag die Funktion callback auf. Liefert diese Callback-Funktion dann true, erfolgt der Eintrag in die Ergebnismenge. Das betrachtete Listing demonstriert auch den Aufruf dieser Funktion. Dabei wird das Array [1, 2, 3, 4, 5, 6] sowie der Lambda-Ausdruck (item:number) => item % 2 == 0 übergeben, der dazu führt, dass alle geraden Werte in die Ergebnismenge aufgenommen werden. An der Stelle eines Lambda-Ausdrucks könnte man auch, wie unter JavaScript üblich, eine anonyme Funktion hinterlegen. Dekoratoren TypeScript versucht bisher stets, dem ECMAScriptStandard voraus zu sein. So gibt es bereits erste Möglichkeiten, die für eine kommende ECMAScript-Version geplant sind [6], in TypeScript. Die Unterstützung für Dekoratoren war ein Feature, das Google von Microsoft für Angular 2 gefordert hat. Ein Dekorator ist ähnlich wie eine Annotation in Java oder Attribute in .NET: Der Entwickler zeichnet damit Klassen, Properties, Methoden oder Methodenparameter aus. Anders als ein Attribut in .NET ist ein Dekorator aber keine Klasse, sondern eine Funktion, und die Dekoratorfunktion wird automatisch zur Laufzeit aufgerufen. Eine Dekoratorfunktion kann das betreffende Sprachkonstrukt analysieren, modifizieren oder auch ganz ersetzen. Dafür benötigt sie aber eine Reflection-Hilfsbibliothek mit Namen reflect-metadata [7], die eine Polyfill-Implementierung des für ECMAScript Version 7 geplanten Listing 8: Dekoratoren in Aktion } /// <reference path="node_modules/reflect-metadata/reflect.ts" /> } console.log("==================== Dekoratorbeispiele"); console.log("==================== Klasse"); // einfacher Klassendekorator ohne Factory function klassendekorator(klasse) { console.log("# Klassendekorator: " + klasse); } // Property-Dekorator mit Factory function range(min: any, max: any) { return function (target: any, name: string, descriptor: TypedPropertyDescriptor<number>) { let set = descriptor.set; console.log("# range1", target, name, descriptor, set); descriptor.set = function (value: number) { let type = Reflect.getMetadata("design:type", target, name); console.log("# range2", value, type); if ((value < min || value > max)) { console.error(`Range-Fehler bei ${name}: Wert ${value} <${min} oder >${max}!`); } else { console.log(`Range-OK bei ${name}: Wert ${value} >${min} und <${max}!`); } } www.basta.net @klassendekorator class Lottoziehung { private _zahl: number; @range(1, 49) set zahl(value: number) { this._zahl = value; } get zahl() { return this.zahl; } constructor(z: number) { this.zahl = z; } } console.log("==================== Client"); var ziehung1 = new Lottoziehung(1); ziehung1.zahl = 5; ziehung1.zahl = 123; ziehung1.zahl = 40; ziehung1.zahl = 20; ziehung1.zahl = 10; ziehung1.zahl = 800; 55 DOSSIER Metadata Reflection API darstellt. Wie bereits oben erwähnt, gehören Dekoratoren zu den experimentellen Features, die erst separat im TypeScript-Compiler aktiviert werden müssen. Microsoft behält sich hier eine Syntaxänderung vor, wenn der laufende Standardisierungsprozess die Syntax noch ändert. Eine einfache Dekoratorfunktion wird bei der Initialisierung aufgerufen. Eine Dekoratorfunktion kann eine weitere Funktion zurückliefern. Diese Fabrikfunk- Listing 9: „ModulA.ts“ export class KlasseA { test(): string { return ("KlasseA: Test"); } } Listing 10: „ModulB.ts“ import {KlasseA} from './ModulA' export module xy { // Class export class KlasseB { test(): string { var a1 = new KlasseA(); return ("1. " + a1.test() + " 2. KlasseB:Test"); } } } HTML5 & JavaScript tion wird dann bei jeder Verwendung aufgerufen. Die Parameter der Dekoratorfabrikfunktion variieren je nach Dekoratorart. Der Dekorator wird dann mit einem @-Zeichen beginnend zugewiesen. Listing 8 zeigt zwei Dekoratoren am Beispiel einer Lottozahlziehung, bei der es leider ungültige Zahlen geben kann, was mit dem PropertyDekorator range festgestellt wird. Der Klassendekorator hat keine aktive Funktion in dem Beispiel. Er könnte aber die Klassendefinition verändern, was hier aber aus Platzgründen nicht gezeigt wird. Ein Sprachkonstrukt kann mehrere Dekoratoren aufweisen. Die Aufrufreihenfolge ist festgelegt: Erst werden Dekoratoren auf Instanzmitgliedern, dann auf statischen Mitgliedern und dann auf Konstruktoren aufgerufen. Erst zum Schluss folgen Klassendekoratoren. Listing 8 zeigt nur ein Beispiel ausgewählter Möglichkeiten. Module und Verweise TypeScript kann zusammen mit einem Modulsystem (Abb. 1 und 2) oder ohne Modulsystem (Auswahl in Visual Studio in Abbildung 2 None) verwendet werden. Ohne Modulsystem müssen: • die verwendeten TypeScript-Dateien mit einem <reference>-Tag zu Beginn einer .ts-Datei eingebunden werden: /// <reference path="modulA.ts" />. Solch ein Tag entsteht in Visual Studio per Drag and Drop einer .ts-Datei in eine andere. • alle verwendeten TypeScript-Dateien explizit per <script>-Tag in die HTML-­Datei eingebunden werden: <script src="ModulA.js"></script>. Solch ein Tag entsteht in Visual Studio per Drag and Drop einer .ts-Datei in eine HTML-Datei. Alternativ dazu kann man mit der Compileroption --out alle Listing 11: „app.ts“ /// <reference path="../jslibs/jquery.d.ts" /> import {xy} from './ModulB' // ModulB.ts enthält noch mal // explizites Modul import * as A from './ModulA' export function main() { $("#C_Ausgabe").append(new Date() + "<hr>"); var ma = new A.KlasseA(); var a1 = "Modul A: " + ma.test() console.log(a1); $("#C_Ausgabe").append(a1 + "<br>"); var mb = new xy.KlasseB(); var a2 = "Modul B: " + mb.test() console.log(a2); $("#C_Ausgabe").append(a2 + "<br>"); } main(); www.basta.net Listing 12: „index.html“ <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <script src="/JSLibs/jquery-2.0.3.js"></script> <script src="jspm_packages/system.js"></script> <script> //SystemJS konfigurieren System.config({ defaultJSExtensions: true }); System.import('app').catch(console.error.bind(console));</script> </head> <body> Modulclient <hr /> <div id="C_Ausgabe"></div> </body> </html> 56 DOSSIER TypeScript-Dateien in eine .js-Datei übersetzen. Dann muss nur diese eine .js-Datei referenziert werden. • optional können innerhalb einer TypeScript-Datei mit dem Schlüsselwort namespace Namensräume gebildet werden. Wenn Namensräume zum Einsatz kommen, müssen alle zu exportierenden Klassen und Funktionen den Zusatz export erhalten (Listing 5). Mit einem Modulsystem bildet jede TypeScript-Datei ein eigenständiges Modul. Das Modulsystem erfordert immer eine Zusatzbibliothek zur Laufzeit, wie z. B. RequireJS [8] oder SystemJS [9], die eingebunden und konfiguriert werden muss. Der Nutzer muss dann die zu verwendeten Klassen explizit importieren: • Import einer Klasse: import {KlasseA} from './ModulA' • Import aller Exporte: import * as A from './ModulA' Damit entfallen dann aber <reference>- und <script>Tags für jedes Modul (Listing 9 bis 12). In diesem Fall kann man mit dem Schlüsselwort module innerhalb einer Datei verschiedene Module bilden (Listing 9). Deklarationsdateien Listing 11 zeigt die Einbindung einer Deklarationsdatei (d.ts). Deklarationsdateien sind enorm wichtig, denn sie liefern die TypeScript-Typdefinitionen für bestehende JavaScript-Bibliotheken, die nicht in TypeScript geschrieben wurden. Dadurch kann der TypeScriptEntwickler auch mit solchen Bibliotheken typsicher und mit Eingabeunterstützung arbeiten. Listing 13 zeigt einen Ausschnitt aus jquery.d.ts zur Deklaration der jQuery-Schnittstelle. Ein großer Fundus für solche Deklarationsdateien ist [10]. Es ist notwendig, die benötigen Deklarationsdateien zu Beginn in eine Type­Script-Datei einzubinden: /// <reference path="libs/typings/jquery/jquery.d.ts" /> HTML5 & JavaScript sual Studio solche Kommentare erkennt, die Zusatzinformationen beinhalten. Fazit JavaScript erweitert zwar nun langsam seine Sprachfeatures: Kaum ein Browser versteht heute schon ECMAScript 2015. Mit TypeScript kann man alle ECMA­Script-2015-Features und mehr jedoch bereits heute überall nutzen. Auch wenn dieser Beitrag nicht alle TypeScriptSprachfeatures darstellen konnte, hat er doch klar aufgezeigt, dass TypeScript dem Programmierstil wesentlich näher liegt, der aus OOP-Hochsprachen wie C#, Visual Basic .NET und Java bekannt ist, als reines JavaScript. Gerade wenn man nun von solchen Sprachen auf die Webentwicklung umsteigen will (oder muss), gibt es keinen Grund, warum man noch direkt mit JavaScript arbeiten sollte. Ein Kollege in unserer Firma meinte letztens, es wäre wichtig, auch das, was hinten rauskommt, zu beherrschen. Ich habe dem entgegnet: Gut, dann muss du ja nun auch MSIL-Bytecode beherrschen, wenn du C# schreibst. Natürlich ist das Verhältnis von JavaScript zu Type­ Script noch etwas anders, weil TypeScript ja eine Obermenge von JavaScript ist, C# aber nicht von MSIL. Aber ich meine tatsächlich, dass man heute nicht mehr lernen muss, wie man mit Prototypen eine Vererbung in JavaScript nachbildet, wenn man TypeScript hat, das den lästigen und unübersichtlichen Code für den Entwickler generiert. Dr. Holger Schwichtenberg, alias „der Dotnet-Doktor“, ist technischer Leiter des auf .NET und Webtechniken spezialisierten Beratungs- und Schulungsunternehmens www.IT-Visions.de mit Sitz in Essen. Er gehört durch zahlreiche Fachbücher und Vorträge auf Fachkonferenzen zu den bekanntesten Experten für .NET und Visual Studio in Deutschland. [email protected] www.dotnet-doktor.de, www.it-visions.de Bei den drei Schrägstrichen handelt es sich um keinen Schreibfehler, sondern um die Konvention, über die Vi- Listing 13: Ambiente Deklarationen für jQuery („jquery.d.ts“) interface JQuery { ... replaceAll(target: any): JQuery; replaceWith(func: any): JQuery; text(): string; text(textString: any): JQuery; text(textString: (index: number, text: string) => string): JQuery; toArray(): any[]; unwrap(): JQuery; .. } Links & Literatur [1] Install-Package Microsoft.TypeScript.Compiler [2] npm install -g typescript [3] http://jsfiddle.net/TheRoks/8uCT9/ [4] http://www.typescriptlang.org/play/ [5] https://github.com/Microsoft/TypeScript/blob/master/doc/ TypeScript%20Language%20Specification.docx?raw=true [6] https://github.com/tc39/ecma262/blob/master/README.md [7] https://www.npmjs.com/package/reflect-metadata [8] http://requirejs.org/ [9] https://github.com/systemjs/systemjs [10] https://github.com/DefinitelyTyped/DefinitelyTyped www.basta.net 57 DOSSIER HTML5 & JavaScript cand om/lv oto.c ckph ©iSto Asynchrones TypeScript y TypeScript lernt async/await In JavaScript sind viele APIs asynchron. Im Gegensatz zu .NET stehen synchrone Funktionen zum Zugriff auf Netzwerk, Datenbanken oder das Dateisystem überhaupt nicht zur Verfügung. Die Folge ist, dass man in JavaScript-Programmen vor lauter Callbacks häufig den eigentlichen Algorithmus nicht mehr sieht. Der Code ist schwer lesbar und fehleranfällig. Frameworks wie Async [1] verbessern die Situation, sind aber gewöhnungsbedürftig. Mit der Version 1.7 hat TypeScript in Sachen asynchrone Funktionen mit C# gleichgezogen: async/await wurde eingeführt. Dieser Artikel zeigt, welche Verbesserungen async/await bringt, wie das Sprachkonstrukt einzusetzen ist und welche Einschränkungen es aktuell gibt. von Rainer Stropek Meiner Ansicht nach versteht man Softwareentwicklungskonzepte am besten, wenn man sich klarmacht, welches Problem damit gelöst wird. Lassen Sie uns unsere Reise zu async/await damit beginnen. JavaScript-Entwicklung sowohl am Client als auch am Server hat viel mit asynchroner Programmierung zu tun. Wann immer I/O-Operationen ausgeführt werden sollen, hat man keine andere Wahl, als Funktionen asynchron aufzurufen. Auch wenn die Details sich von Fall zu Fall unterscheiden, das Grundkonzept ist immer das Gleiche: Man gibt im Funktionsaufruf eine Callback-Funktion an, die aufgerufen werden soll, wenn die Operation fertig ist. Das www.basta.net Problem dabei ist, dass die Struktur des Codes aufgrund der Callbacks nicht mehr dem Algorithmus entspricht, den man eigentlich programmieren möchte. Die technische Notwendigkeit der asynchronen Programmierung diktiert die Struktur des Codes. Dadurch wird er schwer zu lesen und zu warten. Lassen Sie uns dieses Problem zum besseren Verständnis an einem Beispiel betrachten. Angenommen wir möchten den nachfolgenden, einfachen Algorithmus umsetzen: • Öffne Verbindung zur Datenbank • Lies alle Personen mit Vornamen „John“ • Iteriere über alle ermittelten Personen 58 DOSSIER Video-Link: „Vor der App kommt die (Service-)Architektur!“ Interview mit Christian Weyer • Wenn die aktuelle Person ein Kunde ist, lies die Kundendaten und gib Personen- und Kundendaten am Bildschirm aus • Wenn die aktuelle Person ein Lieferant ist, lies die Lieferantendaten und gib Personen- und Lieferantendaten am Bildschirm aus • Schließe die Datenbankverbindung Diesen Algorithmus möchten wir mit TypeScript und Node.js programmieren. Als Datenbank verwenden wir exemplarisch MongoDB. (Falls Sie mit dem Code experimentieren wollen, finden Sie ihn auf GitHub [2]) . „Callback Hell“ Listing 1 zeigt die Umsetzung unseres Algorithmus mit TypeScript ohne Verwendung von Frameworks wie Async oder async/await. Wie Sie sehen, wird bei jedem MongoDB-Aufruf eine Callback-Funktion angegeben. Der Code wird dadurch schwer verständlich. Man spricht daher auch von der „Callback Hell“. Beden- HTML5 & JavaScript ken Sie, dass unser Beispiel bewusst einfach gehalten ist. Echte Programme in der Praxis sind weitaus komplexer, das Problem wird dadurch noch verschärft. Ganze Webseiten wie etwa unter [3] widmen sich der Frage danach, wie man Code, der Callbacks intensiv verwendet, trotzdem noch halbwegs gut strukturieren kann. Aber auch diese Lösungen sind alles andere als optimal. Async Frameworks wie Async bieten Hilfsfunktionen, um der Callback Hell zu entkommen. Statt endlos verschachtelter Callbacks hat man Konstrukte wie async. Waterfall, um eine Sequenz von asynchronen Funktionsaufrufen auszuführen. Es würde den Rahmen dieses Artikels sprengen, wenn wir das Async-Framework hier im Detail beschreiben. Wer sich einen tiefergehenden Überblick über die gebotenen Funktionen verschaffen möchte, findet die Dokumentation unter [1]. Listing 2 zeigt unser Beispiel mit Async. Man sieht, dass der Code dem eigentlichen Algorithmus schon näher kommt, die Umsetzung hat allerdings noch immer eine Menge Nachteile. Das API von Async ist für Anfänger erfahrungsgemäß schwer verständlich. Der Code ist gespickt mit Callbacks in Form von LambdaFunktionen. Sie sind nicht von Natur aus notwendig. Es ist Komplexität, die sich aus den Einschränkungen der Sprache im Umfang mit den asynchronen Funktionen ergibt. Es muss also noch besser gehen. Listing 1: Asynchroner Code mit Callbacks import * as mongodb from 'mongodb'; // Call helper function to close DB if done closedb(); }); }); } else { // Read supplier details for person db.collection("Supplier", (err, supplColl) => { supplColl.findOne({ id: p.supplierId }, (err, suppl) => { // Print person and supplier details console.log(''John ${p.lastName} works for ${suppl. supplierName}.'); // Open Database mongodb.MongoClient.connect("mongodb://10.0.75.2:27017/demo", (err, db) => { // Read all persons with first name "John" db.collection("Person", (err, coll) => { coll.find({ firstName: "John" }).toArray((err, res) => { // In order to close DB after we are done, we have to use a counter var counter = res.length; var closedb = () => { if (--counter == 0) db.close(); }; // For each person for (var i = 0; i < res.length; i++) { var p = res[i]; // If Person is customer if (p.isCustomer) { // Read customer details for person db.collection("Customer", (err, custColl) => { custColl.findOne({ id: p.customerId }, (err, cust) => { // Print person and customer details console.log('John ${p.lastName} works for ${cust. customerName}.'); www.basta.net // Call helper function to close DB if done closedb(); }); }); } } }); }); }); 59 DOSSIER async/await Listing 3 zeigt unser Beispiel mit async/await. Keine Sorge, falls das Grundkonzept von async/await neu für Sie ist; wir werden gleich zu den Details kommen. Zuvor werfen Sie bitte einen Blick auf den Code, den wir mit async/await schreiben können. Der TypeScriptCode entspricht fast eins zu eins dem Algorithmus, den wir umsetzen wollten. Er ist leicht zu schreiben und HTML5 & JavaScript leicht verständlich. Genau das ist die Motivation hinter async/await. Promises Lassen Sie uns jetzt einen genaueren Blick auf die Funktionsweise von async/await in TypeScript werfen. Technisch gesehen unterbricht await die Ausführung der aufrufenden Funktion, bis die jeweilige asynchrone Ope- Listing 2: Asynchroner Code mit Callbacks import * as mongodb from 'mongodb'; import * as async from 'async'; var database: mongodb.Db; async.waterfall([ // Open Database callback => mongodb.MongoClient.connect("mongodb://10.0.75.2:27017/ demo", callback), // Read all persons with first name "John" (db, callback) => { database = db; db.collection("Person", callback); }, (coll, callback) => coll.find({ firstName: "John" }).toArray(callback), (res, callback) => { // In order to call the callback after we are done, we have to use a counter var counter = res.length; var markOneAsProcessed = () => { if (--counter == 0) callback(); }; // For each person for (var i = 0; i < res.length; i++) { var p = res[i]; // Read customer details for person callback => database.collection("Customer", callback), (custColl, callback) => custColl.findOne({ id: p.customerId }, callback), // Print person and customer details (cust, callback) => { console.log(`John ${cust.lastName} works for ${cust.customerName}.`); callback(); }, ], (err, result) => markOneAsProcessed()); } else { async.waterfall([ // Read supplier details for person callback => database.collection("Supplier", callback), (supplColl, callback) => supplColl.findOne({ id: p.supplierId }, callback), // Print person and supplier details (suppl, callback) => { console.log(`John ${suppl.lastName} works for ${suppl.customerName}.'); callback(); }, ], (err, result) => markOneAsProcessed()); } } // If Person is customer if (p.isCustomer) { async.waterfall([ } ], (err, result) => database.close()); Listing 3: async/await import * as mongodb from ‘mongodb’; var cust = await db.collection("Customer").findOne({ id: p.customerId }); async function run() { // Open Database var db = await mongodb.MongoClient.connect("mongo db://10.0.75.2:27017/demo"); // Print person and customer details console.log(`John ${p.lastName} works for ${cust.customerName}.`); } else { // Read supplier details for person var suppl = await db.collection("Supplier").findOne({ id: p.supplierId }); // Read all persons with first name "John" var persons = await db.collection("Person").find({ firstName: "John" }).toArray(); // Print person and supplier details console.log(`John ${p.lastName} works for ${suppl.supplierName}.`); // For each person for (var i = 0; i < persons.length; i++) { var p = persons[i]; } } db.close(); // If Person is customer if (p.isCustomer) { // Read customer details for person www.basta.net } run(); 60 DOSSIER ration fertig ist. Sobald das der Fall ist, wird die Ausführung beim nächsten Statement der aufrufenden Funktion automatisch wieder aufgenommen. Die Voraussetzung dafür, dass in einer Funktion await verwendet werden kann, ist das Präfix async. Listing 3 zeigt dies gleich in der ersten Zeile nach import. Worauf kann await warten? Woher weiß await, wann die aufgerufene asynchrone Operation fertig ist? Schreibt der TypeScript-Compiler den Code um, sodass im Endeffekt wieder die Callbacks aus Listing 1 herauskommen? Nein, async/await verwendet ECMAScript 2015 Promi­ ses [4]. Jede Funktion, die ein Promise-Objekt zurückgibt, kann in Verbindung mit await verwendet werden. Ein Promise-Objekt ist ein Proxy für ein Funktionsergebnis, das erst später vorliegen wird, da es asynchron ermittelt werden muss. Das Objekt hat einen von drei Zuständen: • Pending: Initialer Status, die asynchrone Operation läuft noch. • Fulfilled: Die asynchrone Operation wurde erfolgreich beendet. • Rejected: Die asynchrone Operation wurde mit Fehler beendet. In unserem Beispiel habe ich bewusst auf MongoDB als Datenbank zugrückgegriffen, da das Node.js-API von Listing 4: Promises interface IForm { name: string; } interface IPokemon { forms: IForm[]; } function sleep(seconds: number) : Promise<void> { return new Promise<void>((resolve, reject) => setTimeout(() => resolve(), seconds * 1000)); } function getPokemonName(pokemonId: number): Promise<string> { return new Promise<string>((resolve, reject) => { needle.get(`http://pokeapi.co/api/v2/pokemon/${pokemonId}/`, (err, res) => { if (!err && res.statusCode == 200) { let pokemon = <IPokemon>res.body; resolve(pokemon.forms[0].name); } else { reject("Could not get pokemon data"); } }); }); } www.basta.net HTML5 & JavaScript MongoDB sowohl Callbacks als auch Promises unterstützt. Gibt man beim Aufruf von MongoDB-Funktionen keinen Callback an, bekommt man ein Promise-Objekt zurück, auf das man mit await warten kann. Was macht man aber, wenn man ein Callback-basierendes, asynchrones API hat, das noch keine Promises unterstützt? Es ist recht einfach, sich selbst einen Wrapper zu schreiben, der aus einem Callback ein Promise-Objekt macht. Listing 4 zeigt zwei Beispiele. Das erste, die Methode sleep(), macht aus setTimeout() ein awaitable Promise-Objekt. Die zweite Methode verwendet die Library Needle, um ein RESTful Web-API aufzurufen. Beachten Sie im zweiten Fall, dass das Promise-Objekt abhängig vom Ergebnis des Web-API-Aufrufs resolved oder rejected ist. Generators Wer besonders neugierig ist und ganz genau wissen möchte, wie man async/await Promises im Hintergrund verwendet, dem empfehle ich, einen Blick auf den vom Type­Script-Compiler erzeugten JavaScriptCode zu werfen. Sie werden sehen, dass der entstehende Code Promises in Verbindung mit ECMAScript 2015 Generators [5] nutzt. Sich durch den generierten JavaScript-Code zu graben, ist für die erfolgreiche Verwendung von async/await zwar nicht notwendig – die beiden neuen Schlüsselwörter sollen Ihnen schließlich die technischen Details abnehmen. Eine interessante Übung ist es aber allemal, also los. Sind C#-Entwickler unter den Lesern? Für sie kann ich die Sache abkürzen: Generators sind wie IteratorBlocks mit yield return in C#. Sowohl in C# als auch in TypeScript handelt es sich um Funktionen, die verlassen werden, dabei ein Ergebnis zurückgeben und später wieder betreten werden. Der gesamte Kontext, also die Listing 5: Generator function *gen() : Iterable<string> { for(let i=0; I < 5; i++) { console.log("Yielding item..."); yield `Item ${i}`; console.log("Yielded item."); } } for (let s of gen()) { console.log(`Got item ${s}`); } /* * This will print: Yielding item... Got item Item 0 Yielded item. Yielding item... Got item Item 1 Yielded item. ... */ 61 DOSSIER Listing 6: Fehlerbehandlung async function run() : Promise<void> { try { var name = await getPokemonName(25); console.log(`Pokemon ${25} is ${name}.`); // The following line will produce an exception name = await getPokemonName(99999); } catch(ex) { console.log(`Error "${ex}" happened.`); } } Listing 7: Generierter JavaScript-Code function run() { return __awaiter(this, void 0, Promise, function* () { try { var name = yield getPokemonName(25); console.log(`Pokemon ${25} is ${name}.`); // The following line will produce an exception name = yield getPokemonName(99999); } catch (ex) { console.log(`Error "${ex}" happened.`); } }); } HTML5 & JavaScript lokalen Variablen inklusive ihrer Werte, bleibt dabei erhalten. Listing 5 zeigt ein Beispiel. Die Methode gen() ist eine Generator-Funktion. Der Stern vor dem Funktionsnamen kennzeichnet sie als solche. Mit dem Schlüsselwort yield wird die Ausführung der Methode unterbrochen und ein Ergebnis, hier eine Zeichenkette, zurückgegeben. Die aufrufende Funktion erhält die Kontrolle. Sie durchläuft in diesem Beispiel mit einer for..of-Schleife das Ergebnis der Generator-Funktion. Wichtig ist, dass bei jedem weiteren Schleifendurchlauf wieder zurück in die gen()-Methode gesprungen wird. Diese startet nicht wieder von vorne, sondern führt die Ausführung beim nächsten Statement nach dem letzten yield fort. Alle Variablen, in unserem Beispiel vor allem die Laufvariable i, bleiben erhalten. Was haben die Generators also mit async/await zu tun? Sie kümmern sich darum, dass nach dem Beenden einer asynchronen Operation die Ausführung der Funktion an der richtigen Stelle wiederaufgenommen wird. Der Vergleich von Listing 6 (TypeScript) und Listing 7 (generierter JavaScript-Code) macht klar, was damit gemeint ist. Aus dem await in TypeScript wird yield in JavaScript. Wie wir zuvor schon gesehen haben, gibt yield die Kontrolle an die aufrufende Funktion zurück, sorgt dafür, dass der Kontext gesichert wird und setzt die Ausführung später genau an der richtigen Stelle fort. Fehlerbehandlung Zur klareren Struktur des Codes trägt bei async/await auch die Fehlerbehandlung bei: Man kann nämlich try/ catch verwenden. Wird das zugrunde liegende Promise- Listing 8: Parallele asynchrone Funktionen async function run() : Promise<void> { ... // Siehe Listing 6 } run(); /* * Note that run does NOT block the execution of the program. * Therefore, the following lines start another async. operation * in parallel. As a result the program will print something like: Heartbeat... Heartbeat... Heartbeat... Pokemon 25 is pikachu. Heartbeat... Error "Could not get pokemon data" happened. Heartbeat... */ var cb = () => { console.log("Heartbeat..."); setTimeout(cb, 1000); }; cb(); www.basta.net Besuchen Sie auch folgende Session: Web-APIs mit Node.js und TypeScript – für .NET-Entwickler Manuel Rauber Full-Stack TypeScript: Im Frontend werkeln moderne Single-Page-Application-Frameworks wie Angular, während im Hintergrund leichtgewichtige Web-APIs zur Kommunikation arbeiten. Dank Node.js sprechen beide Welten eine gemeinsame Sprache: TypeScript – ein um Typen angereichertes Superset von JavaScript. Ob async/await, Klassen, Eigenschaften oder Generics – all das ist mit TypeScript möglich. Lassen Sie uns gemeinsam einen Blick in die Node.js-Welt mit TypeScript werfen und dabei Aspekte von modernen Web APIs – wie die Anbindung von Datenbanken oder die Echtzeitkommunikation – beleuchten. Mit wenigen Zeilen Code schafft Manuel Rauber von Thinktecture eine Grundlage für Ihr „Next Generation“ Web-API. 62 DOSSIER Objekt rejected, kommt es automatisch zu einer Excep­ tion. Listing 6 zeigt ein Beispiel. In einem try-Block wird die in Listing 4 gezeigte Methode getPokemonName() aufgerufen. Darin wird das Promise-Objekt rejected, falls kein Pokémon mit der spezifizierten ID gefunden werden konnte. Dadurch kommt es zu einer Exception und der catch-Block wird ausgeführt. HTML5 & JavaScript Webentwickler brauchen eventuell noch etwas Geduld. Das Rückwärtskompilieren nach ECMAScript 5 oder sogar 3 steht noch nicht zur Verfügung, ist aber für die Version 2.1 von TypeScript bereits angekündigt. Alles in allem ist async/await ein wichtiger Schritt für die Sprache TypeScript und ein weiterer Grund, TypeScript statt JavaScript zu schreiben. Gleichzeitige, asynchrone Operationen Es ist wichtig zu bedenken, dass durch await nicht die gesamte Programmausführung gestoppt wird. Es ist möglich, mehrere parallellaufende asynchrone Funktionen zu starten. Listing 8 zeigt das Prinzip. Die Methode run() ruft Web-APIs asynchron auf (Listing 6). Das Hauptprogramm läuft aber weiter. Dadurch kann es weitere asynchrone Funktionen starten (Listing 8). Durch setTimeout() wird ein Timer gestartet, der jede Sekunde eine Meldung am Bildschirm ausgibt. Timer und Web-APIs laufen parallel. Durch async/await wird bei Ende einer asynchronen Methode die Programmausführung an der jeweils richtigen Stelle wieder aufgenommen. Limitierungen Klingt alles zu schön um wahr zu sein? Zugegeben, einen Nachteil hat async/await in TypeScript (noch): Man kann die Schlüsselwörter nur verwenden, wenn man ECMAScript 6 (ES6) bzw. ECMAScript 2015 (ES2015) als Zielumgebung wählt. Diese Option kann man entweder in der Kommandozeile dem TypeScriptCompiler tsc mit der Option --target übergeben oder sie in der .tsconfig-Datei einstellen. Um in den Genuss von async/await zu kommen, braucht man also eine ES2015-kompatible Ausführungsumgebung. Aktuelle Node.js-Versionen erfüllen diese Anforderung. Viele Browser, die Web­entwickler (noch) unterstützen müssen, jedoch noch nicht. Die gute Nachricht ist, dass Microsoft an der Umsetzung von async/await für ES5 und ES3 arbeitet. Sie ist für die TypeScript-Version 2.1 angekündigt [6]. Dass Microsoft die Unterstützung der alten ECMAScript-Versionen nicht von Anfang an eingebaut hat, ist nachvollziehbar. Dort stehen weder Generators noch Promises zur Verfügung. Der generierte ECMAScript-Code wird daher kaum mehr Ähnlichkeiten mit dem geschriebenen TypeScript-Code haben. Vor solchen weitreichenden Generierungen von Code ist Microsoft bisher im TypeScript-Compiler zurückgeschreckt. Das Feedback der TypeScript-Community war aber so deutlich, dass man sich schließlich umstimmen ließ. Fazit C#-Entwickler haben schon vor langer Zeit async/await kennen und lieben gelernt. Jetzt liefert Microsoft diesen Produktivitäts-Boost auch in TypeScript. Der Code wird leichter zu schreiben, besser lesbar und damit stabiler und leichter zu warten. Die Node.js-User unter den Lesern können sofort loslegen und async/await verwenden. www.basta.net Rainer Stropek ist IT-Unternehmer, Softwareentwickler, Trainer, Autor und Vortragender im Microsoft-Umfeld. Er ist seit 2010 MVP für Microsoft Azure und Microsoft Regional Director. In seiner Firma entwickelt Rainer mit seinem Team die Zeiterfassung für Dienstleistungsprofis time cockpit. www.timecockpit.com Links & Literatur [1] http://caolan.github.io/async/ [2] https://github.com/rstropek/Samples/tree/master/ TypeScriptAsyncAwait [3] http://callbackhell.com/ [4] https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/ Global_Objects/Promise [5] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/ Global_Objects/Generator [6] https://github.com/Microsoft/TypeScript/wiki/RoadmapAPI Summit Besuchen Sie auch folgende Session: Yarn – npm, nur besser Rainer Stropek npm ist aus der modernen Softwareentwicklung nicht wegzudenken. Egal ob TypeScript-Compiler, DevOps CLI Tools oder JavaScript-Framework, alles kommt aus npm. Umso wichtiger ist es, ein performantes, verlässliches Tool zu haben, um seine npm-Pakete zu verwalten. Yarn hat in dieser Hinsicht die Nase gegenüber npm in einigen Punkten vorne: Package Caching, Security, Stabilität und Offlineunterstützung sind nur einige Beispiele dafür. In dieser Session stellt Rainer Stropek Yarn vor. Er demonstriert die Unterschiede zu npm und bringt Anwendungsbeispiele für Server (Node.js) und Client (Angular). 63 DOSSIER User Interface Conversational UIs: Ein erster Blick auf das Microsoft Bot Framework Das GeBOT der Stunde? Die Build-Konferenz brachte Ende März mit dem Microsoft Bot Framework eine unerwartete Neuigkeit. Entwicklern soll es leicht gemacht werden, eigene Bots zu schreiben – also möglichst intelligente Programme, die in einem Chat Antworten geben und Aktionen durchführen können. Was steckt dahinter? Wo hilft das Framework? Und: Wozu eigentlich? von Roman Schacherl und Daniel Sklenitzka Jetzt haben wir uns gerade erst damit abgefunden, dass Konsolenanwendungen (CLI, Command Line Interfaces) nicht der Gipfel der Usability sind. Nach GUI kam NUI (Natural User Interface) – wobei sich die Interpretation zum Teil nur auf die Verwendung des Fingers statt der Maus reduzierte. Und dann kamen Siri, Google Now und Cortana. Plötzlich finden Benutzer Gefallen daran, mit Software zu kommunizieren – sei es mündlich oder schriftlich. Nicht die TV-App zu starten, den Sender auszuwählen, ins Hauptabendprogramm zu scrollen und die Beschreibung zu öffnen – sondern das Vorhaben einfach in Worte zu gießen: „Was läuft heute Abend auf 3sat?“. www.basta.net Genau in diese Kerbe schlagen Conversational User Interfaces (CUI). Die Schnittstelle zwischen Mensch und Computer nähert sich immer mehr einer Menschzu-Mensch-Kommunikation an und verdient immer öfter die Bezeichnung „intelligent“. Damit das zuverlässig funktioniert, ist ein tiefes Verständnis von Sprache erforderlich. Gesprochene Sätze müssen analysiert und in Text umgewandelt werden, bei geschriebenen Texten dürfen auch kleinere Tipp- oder Rechtschreibfehler der Erkennung keinen Abbruch tun. Darüber hinaus muss ein CUI über den aktuellen Kontext Bescheid wissen: Wer spricht? Was wurde bereits gesagt? Was muss nicht gesagt, kann aber aus anderen Informationsquellen (Internet, Kalender, Kontakte) ermittelt werden? Was wur- 64 DOSSIER User Interface de bereits erlernt und kann als gegeben hingenommen werden? Es ist das Ende von harten Fakten, Interpretation ist angesagt: „um 10 Uhr“ bedeutet am Morgen etwas anderes als am späten Abend. Kurzum: Das Entwickeln von Conversational User Interfaces erfordert weitaus mehr als ein paar aneinandergereihte if-Statements, die dem Benutzer vordefinierte Phrasen zurückwerfen. Es geht um das Verständnis von Sprache – mit allem, was dazugehört. Das Microsoft Bot Framework Das Microsoft Bot Framework [1] stellt einen Werkzeugkasten für die Entwicklung derartiger Schnittstellen – eben Bots – zur Verfügung. Es unterstützt bei der Entwicklung (Bot Builder SDK), der Verteilung/Bewerbung (Bot Directory) und der Integration von Bots in andere Kommunikationsplattformen (Bot Connector). Darüber hinaus stehen Cognitive Services [2] bereit, Listing 1 public enum AmountType { Hours, Days } [Serializable] public class Alert { public string Project { get; set; } public int Amount { get; set; } public AmountType AmountType { get; set; } } Listing 2 [Serializable] public class AlertDialog : IDialog<Alert> { private Alert alert; public async Task StartAsync(IDialogContext context) { context.Wait(MessageReceivedAsync); } private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<Message> argument) { var message = await argument; await context.PostAsync("Ich habe Sie leider nicht verstanden."); context.Wait(MessageReceivedAsync); } } www.basta.net Video-Link: Entwickeln für die HoloLens & Microsoft Bot Framework – Interview mit Roman Schacherl um beispielsweise beim Verständnis von Texten oder Bildern assistierend zur Seite zu stehen – nicht ausschließlich für Bots, aber gerade dort sehr hilfreich. Bot Builder SDK Das SDK steht auf GitHub unter einer Open-SourceLizenz sowohl für C# als auch für Node.js zur Verfügung. Dieser Artikel beschäftigt sich im Folgenden mit der C#-Variante. Im Wesentlichen ist ein Bot nichts anderes als ein REST-Service, der ein bestimmtes Protokoll spricht: public async Task<Message> Post([FromBody]Message message) { ... } Man bekommt also eine Message, und muss auch wieder eine Message zurückliefern, wobei immer sämtliche Informationen des Gesprächs hin- und hergeschickt werden – die Implementierung muss zustandslos erfolgen, damit der Bot später beliebig skaliert werden kann. Darüber hinaus beinhaltet eine Message diverse Metainformationen (Sprache, Teilnehmer, …). Besuchen Sie auch folgende Session: Build, Test, Distribute: App-Entwicklung mit Visual Studio Mobile Center automatisieren Jörg Neumann Großartige Apps entstehen nur durch ein kontinuierliches Einbeziehen der Anwender. Direktes, zeitnahes Feedback und die Analyse des Nutzungsverhaltens sind hierbei essenziell. Doch wie schafft man es, eine lauffähige App nach jedem Sprint automatisiert auf die unterschiedlichen Devices der Betatester zu verteilen? Das Visual Studio Mobile Center bietet hierfür eine elegante Lösung. Es bietet nicht nur eine Verteilung über die Cloud, sondern bringt mit seinen Analyse- und Feedbackmöglichkeiten alles mit, um den Anwender in die Entwicklung zu integrieren. Zudem bietet es ein Mobile Backend sowie eine Test-CloudIntegration. Jörg Neumann stellt das System vor und zeigt Ihnen, wie Sie Ihren Entwicklungsprozess vollständig automatisieren können. 65 DOSSIER Man kann entweder mit einer leeren ASP.NET-Anwendung starten, das SDK über NuGet hinzufügen (Microsoft.Bot.Connector) und einen entsprechenden HTTP-Endpunkt implementieren – oder man installiert das Bot Application Template [3], das einem bei der Erstellung eines Projekts diese Schritte abnimmt. Für die weitere Implementierung dieses Service stellt das SDK über ein weiteres NuGet-Package Microsoft. Bot.Builder zwei verschiedene Hilfestellungen zur Verfügung: Dialoge und FormFlow. Diese sollen nun anhand desselben Beispiels vorgestellt werden: Ziel ist es, einen Bot für ein Projektmanagement- und Zeiter- User Interface fassungssystem zu entwickeln, der einem bei der Einrichtung von Benachrichtigungen behilflich ist. Diese sollen versandt werden, wenn in einem Projekt ein bestimmter Teil des (Zeit-)Budgets aufgebraucht ist. Der Bot muss also in Erfahrung bringen, für welches Projekt und nach welchem Zeitraum (in Stunden oder Tagen) die Benachrichtigung erstellt werden soll. Listing 1 zeigt eine entsprechende Klasse. Dialoge Der Begriff „Dialog“ ist im Bot Framework im ursprünglichen Wortsinn zu verstehen – und nicht wie Listing 3 private async Task MessageReceivedAsync (IDialogContext context,IAwaitable<Message> argument) { var message = await argument; if (message.Text.Equals("neuer alarm", StringComparison.CurrentCultureIgnoreCase)) { else { await context.PostAsync("Ich habe Sie leider nicht verstanden"); context.Wait(MessageReceivedAsync); } PromptDialog.Text(context, ProjectReceivedAsync, "Für welches Projekt?", "Entschuldigung - welches Projekt?"); } } Listing 4 private async Task ProjectReceivedAsync(IDialogContext context, IAwaitable<string> result) { this.alert = new Alert() { Project = await result }; PromptDialog.Choice( context, AmountTypeReceivedAsync, new[] { "Tagen", "Stunden" }, $"Soll der Alarm für {this.alert.Project} nach Stunden oder Tagen erfolgen?"); } private async Task AmountTypeReceivedAsync(IDialogContext context, IAwaitable<string> result) { var amountType = await result; this.alert.AmountType = (amountType == "Tage" ? AmountType.Days : AmountType.Hours); } private async Task AmountReceivedAsync(IDialogContext context, IAwaitable<long> result) { this.alert.Amount = (int)await result; PromptDialog.Confirm( context, FinishedAsync, $"Möchtest du einen Alarm für das Projekt {alert.Project} nach {alert. Amount}{(alert.AmountType == AmountType.Days ? "Tagen" : "Stunden")} setzen?"); } private async Task FinishedAsync(IDialogContext context, IAwaitable<bool> result) { if (await result) { // add alarm PromptDialog.Number( context, AmountReceivedAsync, $"Nach wie vielen {(this.alert.AmountType == AmountType.Days ? "Tagen" : "Stunden")} soll der Alarm eingestellt werden?"); context.Done(this.alert); } else { context.Done<Alert>(null); } } www.basta.net 66 DOSSIER User Interface Abb. 1: Bot Framework Emulator sonst in der Softwareentwicklung üblich als Synonym für ein Anwendungsfenster. Ein Dialog ist eine – im besten Falle wiederverwendbare – kurze Konversation zwischen Benutzer und Bot, die eine bestimmte Information als Ergebnis liefert. Ein Dialog kann dabei weitere Dialoge aufrufen, sodass sich ein Gespräch mit einem Bot meist aus mehreren Dialogen zusammensetzt. Das Framework definiert ein (recht überschaubares) Interface für einen Dialog: public interface IDialog<out T> { Task StartAsync(IDialogContext context); } Listing 2 zeigt die erste Version der Klasse AlertDialog, die dieses Interface implementiert. Listing 5 public async Task<Message> Post([FromBody]Message message) { if (message.Type == "Message") { return await Conversation. SendAsync(message, () => new www.basta.net DiaryDialog()); } else { return HandleSystemMessage(message); } } Das Pattern, das dabei verwendet wird, sieht auf den ersten Blick etwas ungewöhnlich aus, man gewöhnt sich aber schnell daran: Durch den Aufruf von context. Wait wartet man auf die nächste Nachricht und gibt eine Callback-Methode an. In unserem einfachen Beispiel wird diese Methode sofort aufgerufen, da ja bereits eine Nachricht verfügbar ist. Bemerkenswert ist, dass auch der Zugriff auf die Message mittels await erfolgen muss – mit dem Warten auf die Nachricht hat das aber nichts mehr zu tun. Mit PostAsync kann man eine Antwort senden, um danach wieder mittels Wait auf die nächste Antwort zu warten. Gespräche mit dieser ersten Version unseres Bots sind vielleicht noch etwas eintönig (böse Zungen behaupten allerdings, für den Einsatz im Kundendienst mancher Unternehmen würde es bereits reichen). Listing 3 zeigt den nächsten Schritt: Wir prüfen, ob die eingegangene Message gleich dem Text „Neuer Alarm“ ist. Wenn dem so ist, starten wir einen neuen Dialog, um den Namen des Projekts zu erfragen. Die Klasse PromptDialog bietet bereits mehrere wiederverwendbare Dialogimplementierungen, mit denen sich diverse Standarddatentypen erfragen lassen – in diesem Fall ein Text. Als Parameter anzugeben ist wiederrum ein Callback, außerdem der Text für die Frage und optional eine zweite Frage, falls die erste Antwort unverständlich ist. Aus diesen Bausteinen können wir nun den Alert-Dialog fertig implementieren, sodass nach und nach die alert-Variable unserer Klasse befüllt wird (Listing 4). Dabei wird immer das beschriebene Muster 67 DOSSIER Abb. 2: FormFlow: Je nach Datentyp werden unterschiedliche Fragen gestellt User Interface Abb. 3: Deployment als API-App in Azure verwendet. In den Callback-Methoden kann jeweils mit await result auf den Rückgabewert des Dialogs zugegriffen werden. Durch den Aufruf von context.Done beenden wir schließlich den aktuellen Dialog. Im Service können wir den Alert-Dialog schließlich starten (Listing 5). Zeit für einen ersten Test: Starten Sie das Projekt und merken Sie sich den lokalen Port, unter dem die Website läuft (üblicherweise Port 3978). Natürlich könnten Sie jetzt mit Postman oder Fiddler Requests austauschen – einfacher geht es aber mit dem Bot Framework Emulator [4, Abb. 1]. Geben Sie in der oberen Leiste den URL (z. B.: http://localhost:3978/api/messages) an und achten Sie darauf, dass die App-ID und das App Secret den Werten aus der Web.config entsprechen – vorerst können wir die Standardwerte (YourAppId, YourAppSecret) belassen. Anschließend steht Ihnen das linke Chatfenster zur Kommunikation zur Verfügung, beim Klicken auf das blaue Icon in den Antworten sehen Sie auch das dazugehörige JSON-Objekt. Übrigens: Wenn Sie CLI und CUI mischen möchten, dann können Sie auch ein Kommandozeilenprogramm als Emulator bauen; den entsprechenden Sourcecode finden Sie online unter [5]. Einfache Dialoge lassen sich also recht schnell implementieren, und auch komplexere Konversationen sind durch die Verschachtelung von mehreren Dialogen gut handhabbar. Bei der Erfassung von komplexeren Datenstrukturen als unserer Alert-Klasse kann es aber trotzdem schnell aufwendig werden, alle Eigenschaften „von Hand“ abzufragen. Hier kommt die zweite Variante ins Spiel. FormFlow Die Grundidee von FormFlow ist, auf Basis einer C#Klasse (Form) automatisch einen entsprechenden Dialog Listing 6 public enum AmountType { [Describe("Stunden")] [Terms("Stunde", "Stunden")] Hours, [Describe("Tagen")] [Terms("Tag", "Tage", "Tagen")] Days } [Serializable] public class Alert { [Describe("Projekt")] [Prompt("Für welches Projekt?")] www.basta.net public string Project { get; set; } [Describe("Art")] [Prompt("Wie soll der Alarm eingestellt werden? {||}")] public AmountType? AmountType { get; set; } [Numeric(1, 100)] [Describe("Limit")] [Prompt("Nach wie vielen {AmountType} soll der Alarm eingestellt werden?")] public int Amount { get; set; } public static IForm<Alert> BuildForm() { return new FormBuilder<Alert>() .Message("Hallo, ich helfe dir bei der Erstellung.") .AddRemainingFields() .Confirm("Möchtest du einen Alarm für das Projekt {Project} nach {Amount} {AmountType} setzen?") .Message("Der Alarm wurde erstellt.") .Build(); } } 68 DOSSIER zu erstellen, der alle Felder und Eigenschaften befüllt. Dazu müssen wir die Alert-Klasse um eine statische Methode erweitern, die mittels der Klasse FormBuilder eine IForm<Alert> erstellt: public static IForm<Alert> BuildForm() { return new FormBuilder<Alert>().Build(); } Mithilfe dieser können wir dann einen entsprechenden Dialog erzeugen: return await Conversation.SendAsync(message, () => FormDialog. FromForm(Alert.BuildForm)); Die FormFlow Engine fragt dann Schritt für Schritt nach jedem Feld, wobei die Fragen je nach Datentyp unterschiedlich gestellt werden. Bei Enumerationen werden beispielsweise gleich alle Optionen aufgelistet (Abb. 2). Ist man mit diesem Standardverhalten nicht zufrieden, kann man auf verschiedene Arten eingreifen. So kann man mittels eines Fluent-API beispielsweise den Begrüßungstext und die Abschlussfrage formulieren oder mittels Attributen die Fragen nach den jeweiligen Feldern beeinflussen. Dabei kommt eine eigene Pattern Language mit Platzhaltern zum Einsatz, mit der etwa die Werte einzelner Felder oder die verfügbaren Optionen angezeigt werden können (Listing 6). Eine detaillierte Beschreibung aller Features ist unter [6] zu finden. Auf diese Weise ist es mit minimalem Aufwand möglich, selbst große Objekte zu befüllen, auch wenn man im Vergleich zur manuellen Implementierung etwas Flexibilität verliert. Dank der zahlreichen Konfigurationsmöglichkeiten fällt das allerdings kaum ins Gewicht, und da auf Basis der Form ebenfalls ein Dialog erstellt wird, lassen sich die beiden Ansätze auch wunderbar kombinieren. User Interface (achten Sie auf die komplette Angabe des URLs, also inkl. der Route auf /api/messages). Auch die App-ID ist wichtig; der hier eingegebene Wert muss mit dem Wert aus der Web.config im Bot-Projekt übereinstimmen und sollte spätestens jetzt auf einen sinnvollen Namen (ohne Leer- und Sonderzeichen) geändert werden. Die Aufnahme ins Bot Directory, die mittels Option beantragt werden kann, ist noch Zukunftsmusik. In diesem Verzeichnis werden später alle Bots aufgelistet, derzeit sind aber nur die Microsoft-eigenen Kandidaten gelistet. Nach der Erstellung werden Sie zur Übersichtsseite weitergeleitet (Abb. 4), auf der Sie u. a. das Primary app secret finden. Kopieren Sie diesen Schlüssel, passen Sie damit die Web.config an und updaten Sie den Bot durch ein erneutes Deployment. Bot Connector Zusätzlich sehen Sie auf der Übersichtsseite auch noch den dritten Tätigkeitsbereich des Microsoft Bot Frameworks: den Bot Connector. Über welches Interface möchten Sie mit dem Bot „sprechen“? Bisher haben Sie nur den Emulator genutzt, für Endbenutzer ist das aber wenig sinnvoll. Ihr Bot muss dort sein, wo Ihre Benutzer sind: auf Skype, Slack oder GroupMe. Er soll per E-Mail oder SMS antworten können. Oder einfach auf Ihrer Website für Fragen aller Art zur Verfügung stehen. Alle diese Szenarien deckt der Bot Connector ab. Microsoft hat nicht nur Schnittstellen zu den Kommunikationsplattformen („Kanäle“) entwickelt; in bebilderten Schritt-für-SchrittAnleitungen werden auch die notwendigen Einstellungen detailliert beschrieben. Eine Integration des Bots in Slack ist dadurch in weniger als 5 Minuten erledigt (Abb. 5). Für die ausgewählten Kanäle stehen auch fertige HTML- Deployment und Registrierung Besuchen Sie auch folgende Session: Da es sich bei einem Bot-Projekt technologisch um ein Web-API handelt, kann das Deployment wie bei herkömmlichen ASP.NET-Anwendungen erfolgen – also auch auf On-Premise-Servern. Dass die Integration in Microsoft Azure leicht gemacht wird, überrascht aber natürlich wenig. In Visual Studio kann mittels Pub­lish (Kontextmenü des Projekts) direkt ein neuer App-Service in Azure erstellt werden (API-App); der Bot ist nach wenigen Eingaben online (Abb. 3). Sollte Ihr Bot großen Anklang finden und von vielen Benutzern konsultiert werden, spricht nichts gegen ein Scale-out der AzureInstanzen: Dank Zustandslosigkeit des Service können während einer Konversation problemlos die Server gewechselt werden. Im nächsten Schritt erfolgt die Registrierung des Bots. Unter [1] können Sie einen neuen Bot anlegen; vor allem der URL zum Message Endpoint ist relevant Design-First Development mit Storyboards www.basta.net Jörg Neumann Erfolgreiche Apps sehen nicht nur gut aus, sondern bieten vor allem eine exzellente Usability. Besonders im Enterprise-Umfeld spielen intuitive Bedienung und die proaktive Unterstützung der Anwender eine entscheidende Rolle. Um dies zu erreichen, müssen alle Stakeholder in den Designprozess eingebunden werden. Hierbei können Storyboards helfen, denn anders als statische Wireframes vermitteln sie dem Anwender einen guten Eindruck vom Verhalten der App. Zudem bieten sie eine solide Grundlage für die Entwicklung. Jörg Neumann zeigt Ihnen an Beispielen aus der Praxis, wie gute Storyboards entworfen werden und welche Faktoren für ein gutes App-Design wichtig sind. 69 DOSSIER User Interface Abb. 4: Registrierung des Bots Roman Schacherl (MVP) und Daniel Sklenitzka sind Gründer der Firma softaware gmbh und entwickeln mit ihrem Team individuelle Lösungen auf Basis von Microsoft-Technologien. Beide sind Autoren mehrerer Fachartikel, nebenberuflich Lehrende an der FH Hagenberg (Österreich) und als Sprecher auf Entwicklerkonferenzen tätig. www.softaware.at Abb. 5: Verwendung des Bots in Slack Einbettungscodes zur Verfügung, um das Chat-Control auf der eigenen Website platzieren zu können. Fazit Warum nicht neue Benutzer mit einem Bot um die Vervollständigung des Userprofils bitten (wie bei Slack hervorragend gelöst)? Warum nicht einfache Aktionen durch einen Bot abwickeln anstatt Wartungsmasken zu bauen? Warum ein FAQ nicht einmal anders lösen? Das Microsoft Bot Framework ist eine faszinierende Spielwiese. Nicht für jede Anwendung sofort sinnvoll einsetzbar – aber auf alle Fälle einen Blick in die Zukunft wert. Viel Spaß und liebe Grüße an Ihren ersten Bot! www.basta.net Links & Literatur [1] Microsoft Bot Framework: https://dev.botframework.com [2] Cognitive Services: https://www.microsoft.com/cognitive-services [3] Bot Application Template: http://aka.ms/bf-bc-vstemplate [4] Emulator: http://aka.ms/bf-bc-emulator [5] C#-Referenz: http://docs.botframework.com/sdkreference/csharp/ [6] FormFlow-Dokumentation: http://docs.botframework.com/sdkreference/ csharp/forms.html 70