Continuous Delivery mit Azure DevOps, Teil 2: Bauen auf dem Server

Im zweiten Teil der dreiteiligen Artikelserie geht es um das Einrichten eines Server-Builds in Azure DevOps Services und Team Foundation Server (TFS).

In Pocket speichern vorlesen Druckansicht 2 Kommentare lesen
Continuous Delivery mit Azure DevOps, Teil 2: Bauen auf dem Server
Lesezeit: 20 Min.
Von
  • Dr. Holger Schwichtenberg
Inhaltsverzeichnis

Kurz zur Rekapitulation: Im ersten Teil wurde ein Projekt in Azure DevOps Services mit zwei Git-basierten Quellcode-Repositories eingerichtet. Neben den Aufgaben wurde bereits Quellcode für ein ASP.NET-Core-Serverprojekt (Miracle List-Backend) und ein Angular-Client-Projekt (Miracle List-Client) in die Cloud gebracht, der sich jeweils lokal auf dem Entwicklersystem übersetzen und auch einwandfrei testen ließ.

Sich zufrieden zu geben mit "es läuft doch auf meinem Rechner" gehört sicherlich zu den häufigsten Fehlerquellen in der Softwareentwicklung. Es gilt nämlich zu beweisen, dass sich wirklich alle benötigten Dateien im Quellcodeverwaltungssystem befinden und auch ein "nacktes" System die Projekte übersetzen und validieren kann.

Mehr Infos

Continuous Delivery mit Azure DevOps – die Serie

Im neuen Azure-DevOps-Portal steht eine Rakete als Symbol für die "Azure Pipelines". In dem Menüpunkt gibt es dann die Unterscheidung in "Build" und "Release". Ein Build übersetzt das Projekt und startet Unit-Tests zur Überprüfung, üblicherweise aber keine Integrationstests, die Prozesse und Ressourcen wie Webserver oder Datenbankserver erfordern. Solche Integrationstests sind in der Release-Pipeline vorgesehen, die erst angestoßen wird, wenn der Build und die Unit-Tests erfolgreich waren.

Beide Pipeline-Arten bestehen aus einzelnen Schritten, die jeweils zu einer Task-Art gehören. Die Benutzeroberfläche für Build- und Release-Pipelines ist ähnlich, aber dennoch gibt es Unterschiede. Die Build-Prozesse gibt es im Team Foundation Server seit der ersten Version im Jahr 2005. Die ursprünglich XAML-basierte Build-Definition hat Microsoft 2015 durch ein neues System abgelöst, das intern auf JSON-Dokumenten basiert. Die Release-Pipelines stammen hingegen aus dem Aufkauf des Werkzeugs InRelease der Firma InCycle 2013.

Der einfachste Weg zu einem Build-Prozess führt über die Schaltfläche Set up build, die in der Quellcodeverwaltung (Menü Repos | Files) angeboten wird. Damit ist der Bezug zum richtigen Quellcode-Repository schon geklärt. (Zur Erinnerung: Es kann in einem Azure-DevOps-Projekt ein TFVC-Repository und beliebig viele Git-Repositories geben.) Entwickler können nun aus rund 20 Vorlagen für einen Build-Prozess auswählen. Es gibt nicht nur Vorlagen für typische Projektarten der .NET-Welt (.NET Desktop, ASP.NET, ASP.NET Core, Xamarin, UWP), sondern auch für Java mit Maven oder Ant, Node.js, Objective-C mit Xcode, Go und Python.

Für das mit ASP.NET Core erstellte "Miracle List"-Backend wählt man am besten die Vorlage "ASP.NET Core". Hier ist zu beachten, dass es auch noch eine Vorlage "ASP.NET Core (.NET Framework)" gibt, die sich aber nur eignet, wenn ASP.NET Core auf dem klassischen .NET Framework laufen soll. Für eine echte Cross-Plattform-Serveranwendung muss ASP.NET Core auf .NET Core laufen, und das erreicht man mit der Vorlage "ASP.NET Core" ohne weiteren Namenszusatz. Das Ergebnis der Anwendung dieser Vorlage zeigt Abbildung 1.

Mit der Vorlage "ASP.NET Core" angelegte Build-Pipeline (Abb. 1)

Das Webportal hat eine Build-Pipeline mit sechs Schritten geschaffen:

  1. Abholen des Quellcodes aus dem Git-Repository.
  2. Laden der benötigten NuGet-Pakete, was notwendig ist, da es sich um ein mehr oder weniger "nacktes" Build-System handelt. Es ist sicherzustellen, dass alle benötigten NuGet-Pakete in der richtigen Version vorliegen.
  3. Dann erfolgt die Übersetzung mit dem Kommandozeilenwerkzeug dotnet build.
  4. Im vierten Schritt werden die Unit-Tests ausgeführt. Standard ist, dass alle Projekte, die im Ordner /Tests liegen, als Unit-Test-Projekte betrachtet werden. Dafür steht in der Eigenschaft "Path to project(s)" der Wert "**/*[Tt]ests/*.csproj".
  5. Anschließend wird das Ergebnis mit dotnet publish in eine verteilbare Form gebracht (hier werden alle nicht benötigten DLLs und andere unnötige Dateien aussortiert). Dabei ist die Option "Zip Published Projects" aktiv, da eine Veröffentlichung als Zip-Paket in Azure DevOps üblich ist.
  6. Schließlich wird mit dem Task "Publish Artifact" das Zip-Ergebnis in ein Ausgabeverzeichnis gepackt, auf das dann eine folgende Release-Pipeline zugreifen kann.

Welches Betriebssystem diese Build-Schritte ausführt, stellen Nutzer bei "Agent Pool" ein. Microsoft bietet vorgefertigte virtuelle Systeme als Build-Agents in seiner Azure-Cloud an:

  • Unter "Hosted" (ohne Zusatz) verbirgt sich ein Windows Server 2012 R2 mit Visual Studio 2012.
  • "Hosted VS2017" ist ein Windows Server 2016 mit Visual Studio 2017.
  • "Hosted macOS" bietet macOS 10.13 mit Xcode 8, 9 und 10.
  • "Ubuntu 16.04" bietet .NET Core SDK.
  • "Hosted Linux Preview" ist veraltet und wurde im Dezember 2018 eingestellt.

Die Dokumentation von Microsoft ist zu diesem Thema leider äußerst unvollständig. So findet man erst durch eigene Analyse der virtuellen Systeme über spezielle Test-Pipelines, die die Umgebungsvariablen und das Dateisystem auswerten sowie testweise Werkzeuge starten, heraus, dass auf den Agents auch ein .NET Core 2.1 SDK und verschiedene Java-Versionen (zwischen 7 und 11), teilweise auch Go, Python, PowerShell Core, Maven, Gradle, Android SDK und Chrome Web Driver für Selenium vorinstalliert sind.

Zudem ist die Konfiguration nicht durchgängig gleich: So gibt es PowerShell Core nur unter den macOS- und Ubuntu-Agenten. Der Task "PowerShell" bietet ein Häkchen "Use PowerShell Core", was zur Ausführung von pwsh.exe der PowerShell Core statt powershell.exe der Windows PowerShell führt. Der Task läuft damit aber dann nicht unter den Windows-Agenten, weil eben dort PowerShell Core nicht vorhanden ist. Immerhin laufen alle Builds auf den Microsoft-Agents unter administrativen Rechten, das heißt, Zusatzinstallationen sind möglich. Die Cloud liefert jedoch bei jedem Build-Durchlauf dafür eine frische virtuelle Maschine, Downloads aus dem Netz und Installationen ziehen demnach die Build-Laufzeiten in die Länge. Für die Installationen und Build-Ausgaben sind insgesamt 10 GByte Festplattenspeicher in den Agent-VMs verfügbar.

Alternativ kann man hier auch seine eigene virtuelle Maschine in der Azure-Cloud oder im eigenen Rechenzentrum bereitstellen. Das ist insbesondere geboten, wenn man die kostenfreie Version der Azure DevOps Services nutzt, bei der die Rechenzeit für alle Pipelines auf 1800 Minuten im Monat begrenzt ist (vgl. Teil 1 dieser Serie). Auf dem virtuellen System muss man dann die Agent-Software von Azure DevOps installieren (siehe Schaltfläche Download agent in Project Settings | Pipelines | Agent Pools) und die Firewall entsprechend konfigurieren.

Wenn man nach dem Anwenden der Vorlage keine grafische Darstellung des Ablaufs wie in Abbildung 1, sondern nur ein YAML-Listing wie im folgenden Listing sieht, dann hat man in den Benutzereinstellungen das Preview-Feature "New YAML pipeline creation experience" aktiviert.

# ASP.NET Core
# Build and test ASP.NET Core projects targeting .NET Core.
# Add steps that run tests, create a NuGet package, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core

pool:
   vmImage: 'Ubuntu 16.04'

variables:
   buildConfiguration: 'Release'

steps:
- task: DotNetCoreCLI@2
   displayName: Restore
   inputs:
     command: restore

     projects: '$(Parameters.RestoreBuildProjects)'


- task: DotNetCoreCLI@2
   displayName: Build
   inputs:
     projects: '$(Parameters.RestoreBuildProjects)'

     arguments: '--configuration $(BuildConfiguration)'


- task: DotNetCoreCLI@2
   displayName: Test
   inputs:
      command: test

      projects: '$(Parameters.TestProjects)'

      arguments: '--configuration $(BuildConfiguration)'


- task: DotNetCoreCLI@2
   displayName: Publish
   inputs:
     command: publish

     publishWebProjects: True

     arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)'

     zipAfterPublish: True


- task: PublishBuildArtifacts@1
   displayName: 'Publish Artifact'
   inputs:
      PathtoPublish: '$(build.artifactstagingdirectory)'

Microsoft will es ermöglichen, dass man in Zukunft anstelle des Zusammenklickens auch die Schritte einer Build-Pipeline in YAML-Syntax erfassen kann. Leider ist es derzeit ein Entweder-oder: entweder klicken (Designer-Pipeline) oder YAML (YAML-Pipeline) tippen. Ein laufender Wechsel zwischen den Ansichten wie bei der Windows Presentation Foundation (WPF) zwischen XAML und Designer ist nicht möglich.

Nun können berechtigte Nutzer den Übersetzungsvorgang jederzeit manuell starten, indem sie auf Queue klicken. Im Sinne von Continuous Integration will man den Build aber jedes Mal starten, wenn neuer Quellcode in Azure DevOps eintrifft. Das stellt man unter "Triggers" bei dem Häkchen "Enable continuous integration" ein.

Unter Pipelines | Builds sehen die Nutzer die erstellte Build-Pipeline. Ein Klick auf die Pipeline führt zur Ansicht "History", in der sie Durchläufe der Pipeline und den Erfolg beobachten können. Bei einem einzelnen Build-Durchlauf erkennt man unter "Logs" die Details der Konsolenausgabe. Diese Ausgaben kann man auch bei einem laufenden Build-Vorgang im Browser mitverfolgen: Per Websocket-Verbindung wird der Browser vom Webserver auf dem aktuellen Stand gehalten (s. Abb. 2).

Log-Ansicht während des Durchlaufs einer Build-Pipeline (Abb. 2)

Es ist zu beobachten, dass Azure DevOps in der Log-Konsolenausgabe verschiedene Farben verwendet. Jedoch bieten eigene Werkzeuge und Skripte leider keine Farbausgaben. Eine Ausgabe in einem PowerShell-Skript-Task mit

Write-Host "Server nicht erreichbar" -ForegroundColor red

führt zu einem weißen statt rotem Text im Protokoll. Nur spezielle Ausgaben, die mit "##vso" eingeleitet werden, generieren farbige Ausgaben:

Write-Host "##vso[task.logissue type=warning;] Server nicht erreichbar"

Zur Erinnerung an den ersten Teil dieser Artikelserie: VSO steht für Visual Studio Online und war einer der vielen Vor-Vorgängernamen von Azure DevOps (vgl. Teil 1). Microsoft kommt von den alten Beziehungen an einigen Stellen nicht mehr los.

Wenn man den Quellcode des "Miracle List"-Backend verwendet, dann sollte der Build erst mal nicht gelingen. Das liegt daran, dass es in der Quellcodeverwaltung auch ein Projekt VSTools.csproj gibt, das kein .NET-Core-, sondern ein .NET-"Full"-Framework-Projekt ist. Es dient nur dazu, ein Klassendiagramm (.cd-Datei) mit Visual Studio zu erstellen, was in einem .NET-Core-Projekt lange Zeit nicht möglich war. Das Projekt lässt sich nicht mit den .NET-Core-Werkzeugen übersetzen.

Hier gilt es, im Task-Build, der im Standard alle .csproj-Dateien kompiliert, eine Ausnahme festzulegen. Dafür müssen Nutzer eine Zeile einfügen, die mit einem Ausrufezeichen beginnt:

**/*.csproj
!Tools/*

Das Texteingabefeld für den Build-Pfad besitzt zwar eine Sprechblase, die leitet den Benutzer aber nicht zu einer Dokumentation der Syntax. Allerdings lässt sich feststellen, dass die Eigenschaft "Path to project(s)" beim Task "Build .NET Core" ausgegraut ist (s. Abb. 3). Man sieht eine Klammer; ein Klick zeigt eine Sprechblase, die erklärt, dass diese Eigenschaft mit einem Pipeline-Parameter verknüpft ist. Durch Klick auf "Pipeline" (s. Abb. 1) ändern Anwender diesen. Nach der Änderung können sie den Build-Prozess durch Klick auf Save & queue erneut anstoßen.

Ein verknüpfter Parameter in dem "Build .NET Core"-Task (Abb. 3)

Allerdings wird auch mit dieser Anpassung der Build-Prozess nicht erfolgreich sein. Dieses Mal zeigt die Ablaufverfolgung, dass die Unit-Tests fehlschlagen, weil sie eine lokale SQL-Server-Datenbank ansprechen, die natürlich während des Pipeline-Ablaufs gar nicht auf dem Build-System vorhanden ist. Die erste naheliegende Vermutung ist, dass die falschen Tests ausgeführt wurden. Es waren aber nicht die falschen Tests, sie waren nur falsch konfiguriert.

Entity Framework Core, der objektrelationale Mapper für .NET Core, der im "Miracle List"-Backend für den Datenbankzugriff zum Einsatz kommt, besitzt einen In-Memory-Treiber, mit dem man auf elegante Weise Datenbankzugriffe ohne eine echte Datenbank und auch ohne das Implementieren von Mock-Objekten testen kann. Die implementierten Unit-Tests können wahlweise gegen eine echte Datenbank laufen (als Integrationstests) oder gegen die In-Memory-Datenbank (als echte Unit-Tests). Das Umschalten erfolgt durch eine Codezeile, die sich nach der Konfigurationseinstellung ConnectionStrings:MiracleListDB richtet. Diese Einstellung steckt in der appsettings.json-Datei, lässt sich aber durch eine Umgebungsvariable mit genau diesem Namen überschreiben. Umgebungsvariablen können Bediener im Azure-DevOps-Build-Prozess leicht durch die Registerkarte "Variables" (s. Abb. 1) setzen. Hier gibt es eine Tabelle, in der der Eintrag mit Name = "ConnectionStrings:MiracleListDB" und Value = "InMemoryDB" zu ergänzen ist.

Im nächsten Versuch wird der Build-Prozess erstmals bis zum Ende durchlaufen. Allerdings zeigt ein Blick auf die Registerkarten "Tests" oder "Summary" (s. Abb. 4) des Build-Durchlaufs, dass von den 24 Unit-Tests drei kein Ergebnis lieferten.

Drei von 24 Tests sind weder erfolgreich noch gescheitert (Abb. 4).

Das liegt daran, dass der In-Memory-Treiber leider doch nicht alle Fähigkeiten einer relationalen Datenbank besitzt. Drei der implementierten Tests können daher nicht als Unit-Tests, sondern nur als Integrationstests laufen. Dass ein Test eine echte Datenbank erfordert, müssen die Testautoren definieren. Das folgende Listing zeigt einen dieser drei Tests, der mithilfe des XUnit-Frameworks und der Erweiterung SkippableFact realisiert ist. Hier wird eine Computed Column getestet, die die In-Memory-Datenbank im Gegensatz zu einem SQL Server nicht berechnet.

namespace UnitTests
{
 publicclassTaskManagerTest
 {
  [SkippableFact] // NUGET: Xunit.SkippableFact
  [Trait("Category", "Integration")]
  publicvoid CreateTaskDueInDaysTest()
  {
   Skip.If(DAL.Context.ConnectionString == "", "Only runs as integration test as the InMem-DB does not support Default Values and Computed Columns!");

   var um = new UserManager("CreateTaskTestUser", true);
   um.InitDefaultTasks();
   var tm = new TaskManager(um.CurrentUser.UserID);
   var cm = new CategoryManager(um.CurrentUser.UserID);
   var t = new BO.Task();
   t.CategoryID = cm.GetCategorySet().ElementAt(0).CategoryID;
   t.Due = DateTime.Now.AddDays(3);
   tm.CreateTask(t);
   Assert.True(t.TaskID > 0);

   Assert.Equal(BO.Task.DefaultTitle, t.Title); // Default Value Test: not supported in InMem-DB
   Assert.Equal(3, t.DueInDays);// Computed Column Test: not supported in InMem-DB
  }
...
 }
}

Da alle diese nicht ohne Datenbank ausführbaren Unit-Tests mit [Trait("Category", "Integration")] annotiert sind, können Build-Prozess-Autoren sie einfach ausschließen. Dafür tragen sie im Task "Unit Tests .NET Core" in der Eigenschaft "Arguments" den folgenden zusätzlichen Parameter ein.

--filter Category!=Integration

DieseTest-Case-Filter-Syntax ist auf GitHub dokumentiert. Damit werden dann die drei Integrationstests aus dem Unit-Test-Lauf ausgeschlossen, sodass schließlich 100 Prozent der verbliebenen 21 Tests erfolgreich sein werden. Auf der Summary-Seite eines Build-Durchlaufs finden Anwender oben in der Liste unter "Build artifacts published" einen Hyperlink zu der erstellten Zip-Datei (s. Abb. 4). Diese können sie aus der Cloud herunterladen und prüfen.

Spannend ist es, dass die Build-Agents von Microsoft das .NET Core SDK schon vorinstalliert haben, andernfalls würde bereits der dotnet restore-Befehl am Anfang nicht laufen. Eine Herausforderung kann aber die Versionsnummer sein. Wenn man das "Miracle List"-Backend nicht auf einem Windows-System, sondern unter dem vordefinierten Agent "Ubuntu 16.04" übersetzen lässt, bricht der Package Restore mit der Fehlermeldung ab, dass das .NET SDK 2.141.0 installiert sei, aber 2.1.300 benötigt werde. Um das zu beheben, könnte man den Code auf das neuste SDK aktualisieren oder aber man installiert vor der ersten Ausführung eines .NET-Core-Kommandozeilenbefehls das passende .NET Core SDK auf das Build-System. Dazu gibt es den Task ".NET Core Tool Installer", den man durch Klick auf das Pluszeichen neben "Agent job 1" (s. Abb. 1) hinzufügt und an die erste Stelle verschiebt. In der Eigenschaft "Version" ist dann "2.1.300" einzutragen.

Abbildung 1 zeigt, dass die Build-Pipeline nicht direkt aus den Task-Schritten besteht, sondern es noch die Hierarchieebene "Agent Job 1" gibt. Eine Pipeline kann mehrere Jobs enthalten. Neben Jobs, die auf Agents laufen, gibt es auch sogenannte "Agentless Jobs" alias "Server Jobs", die auf dem DevOps-Server selbst laufen; diese bieten aber nur wenige Funktionen. In den Eigenschaften eines Jobs kann man festlegen, dass ein Job parallel auf mehreren Agents laufen soll. Eine Pipeline lässt sich als JSON-Datei ex- und importieren. Die in diesem Beitrag dargestellten Pipelines sind herunterladbar.

Die Build-Pipelines bieten unter "Library" die Möglichkeit, auch Pipeline-übergreifende Variablen zu speichern. Bei Task Groups können Anwender Folgen von Tasks zur Wiederverwendung ablegen. Zu einer Task Group kommen sie, indem sie in einer Pipeline mehrere Schritte markieren und dann im Kontextmenü Create Task Group wählen. Beauftrager eines Builds erhalten automatisch per E-Mail eine Erfolgsnachricht.

Andere Benutzer können sich über die "Notification Settings" entsprechende Abonnements einrichten. Es gibt zahlreiche Ereignistypen für alle Azure-DevOps-Dienste, die sich mit Filtern eingrenzen lassen. In den Eigenschaften einer Build-Pipeline legt man unter "Retention" fest, wie lange die Build-Ergebnisse aufbewahrt werden. Unter Option findet man einen Hyperlink zu einer dynamisch erzeugten Grafikdatei, die anzeigt, ob der letzte Build erfolgreich war. Zudem kann man bei jedem fehlgeschlagenen Build automatisch ein Work Item eines beliebigen Typs erzeugen.

Beim Erstellen des Build-Prozesses für den HTML- und Angular-basierten Cross-Platform-Client lässt sich nicht auf eine Vorlage wie bei dem ASP.NET-Core-Build zurückgreifen. Hier erstellt man eine Build-Pipeline komplett aus einzelnen Schritten mit vordefinierten Tasks. Microsoft stellt ein paar Dutzend solcher Tasks im Standard in der Weboberfläche bereit, und auf dem Azure DevOps Marketplace gibt es über 550 Erweiterungen für Azure Pipelines. Es ist dokumentiert, wie man eigene Erweiterungen für Azure DevOps mit PowerShell oder Node.js schreibt. Mit dem Pluszeichen (s. Abb. 5) ergänzt man Tasks, deren Reihenfolge man per Drag & Drop jederzeit ändern kann. Einzelne oder mehrere markierte Schritte kann man per Kontextmenü deaktivieren, klonen oder löschen.

Die Build-Pipeline für den Angular-Client entsteht durch Auswahl und Anpassung vordefinierter Tasks (Abb. 5).

Die Build-Pipeline für den "Miracle List"-Client besteht aus acht Schritten:

  1. Mit einem "Node Tools Installer" wird Node.js 8.x installiert. Die Version wird in der Eigenschaft "Version Spec" erfasst.
  2. Dann wird mit einem "npm"-Task die Angular CLI installiert. Man wählt als "Command" den Eintrag "custom" und als Argument "install -g @angular/cli@1.5.0".
  3. Danach folgt mit der gleichen Task-Art und dem Befehl "install" das Herunterladen aller benötigten npm-Pakete von www.npmjs.org.
  4. Ein dritter "npm"-Task startet dann die Unit-Tests (Befehlsart: "custom", Argument: "run test-headless").
  5. Nun folgt ein Task mit dem Namen "Publish Test Results". Dieser sorgt dafür, dass die Testergebnisse, die im vorherigen Schritt im JUnit-Format in der Datei unitests.xml persistiert wurden, nun in Azure DevOps für die Testergebnisanzeige importiert werden – sonst weiß Azure DevOps nicht, ob die Tests erfolgreich waren.
  6. Im sechsten Schritt erfolgt nun der Produktions-Build der Angular-Anwendung. Hierzu wird ein Task des Typs "Command Line" verwendet. Dabei ist bei "Tools" der Wert "ng" und bei "Arguments" der Wert "build --prod" einzutragen.
  7. Das Angular-Kommandozeilenwerkzeug ng erzeugt ein Ausgabeverzeichnis "/Dist". In Azure DevOps ist aber die Weitergabe als Zip-Datei üblich. Daher folgt ein Task des Typs "Archive Files", bei dem als " Root folder or file to archive" der Wert "dist" erfasst wird und als Ausgabedatei folgender Pfad mit vordefinierten Azrue DevOps-Variablen: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip. Das Archiv erhält damit bei jedem Durchlauf einen eindeutigen Namen, da sich die Build-ID ändert.
  8. Nun kommt im letzten Schritt die Bereitstellung der Zip-Datei für die Folge-Pipelines, die es auch schon bei der Build-Pipeline für den Server gab (s. Abb. 1).

Sowohl der ASP.NET-basierte Server als auch der Angular-Client werden nun serverseitig bei jedem Code-Check-in automatisch übersetzt und getestet. Aber die Tests sind noch nicht vollständig, denn es wurde noch nicht die Weboberfläche, die Web-API und das Zusammenspiel mit einer echten Datenbank getestet. Im dritten Teil dieser Serie geht es dann um diese Integrations- und UI-Tests sowie das vollautomatische Ausliefern der Software im Sinne von Continuous Delivery, wenn die Tests erfolgreich waren.

Dr. Holger Schwichtenberg
leitet das Expertennetzwerk www.IT-Visions.de, das Beratung, Schulungen und Softwareentwicklung im Umfeld von Microsoft-, Java- und Web-Techniken anbietet. Er selbst schreibt Software in C# und JavaScript/TypeScript, hält Vorträge auf Fachkonferenzen und ist Autor zahlreicher Fachbücher. (ane)