C# “switch expression” – manchmal muss man aufpassen

Wie bei vielen Dingen ist es im Nachhinein klar warum etwas Komisches passiert aber im ersten Moment überrascht es dann doch. Hier ein kleiner Aufreger bei der eigentlich sehr praktischen “switch expression” von C#.

Angefangen hat es mit einer Extension Methode auf JsonNode, die im direkten Spielen mit der JSON Deserialisierung für einfache Datentypen direkt die nativen Werte liefern soll.

public static object? ToJsonScalar(this JsonNode node)
{
    switch (node.GetValueKind())
    {
        case JsonValueKind.True: return true;
        case JsonValueKind.False: return false;
        case JsonValueKind.Null: return null;
        case JsonValueKind.String: return node.GetValue<string>();
        case JsonValueKind.Number: return node.GetValue<double>();
        default: return node;
    }
}

Benutzt wird das dann wie folgt und das liefert auch die richtige Ausgabe (System.Double):

var num = JsonNode.Parse("3.14")!;

Console.WriteLine(num.ToJsonScalar()?.GetType());

Die Überraschung gab es dann bei der manuellen Umstellung auf eine “switch expression” – die automatische Konvertierung macht es besser, aber hier geht es ja ums Prinzip:

public static object? ToJsonScalar(this JsonNode node)
    => node.GetValueKind() switch
    {
        JsonValueKind.True => true,
        JsonValueKind.False => false,
        JsonValueKind.Null => null,
        JsonValueKind.String => node.GetValue<string>(),
        JsonValueKind.Number => node.GetValue<double>(),
        _ => node,
    };

Sieht gleich aus? Ist es aber nicht, die Ausgabe lautet nun:

System.Text.Json.Nodes.JsonValuePrimitive`1[System.Double]

Was ist passiert? Ähnlich wie ein ternärer Ausdruck (?:) muss die “switch expression” einen einzigen Datentyp melden. Der Compiler entscheidet sich hier für den Default (_) und das ist ein JsonNode. Da alle anderen Datentypen einen impliziten Casting Operator haben, wird im Beispiel aus der Zahl direkt wieder ein JsonNode – genauer halt ein JsonValuePrimitive<double>.

Die automatische Konvertierung des switch Statements erzeugt einen (object) Cast bei JsonValueKind.Number, ich habe die Variante auf dem Default bevorzugt:

public static object? ToJsonScalar(this JsonNode node)
    => node.GetValueKind() switch
    {
        JsonValueKind.True => true,
        JsonValueKind.False => false,
        JsonValueKind.Null => null,
        JsonValueKind.String => node.GetValue<string>(),
        JsonValueKind.Number => node.GetValue<double>(),
        _ => (object?)node,
    };

Happy Coding

Jochen



Windows Installer und die Tabellen ServiceInstall und ServiceControl

Das Installationsprogramm von VCR.NET ist mit dem WiX Toolset 3.8 entwickelt, das letztlich Windows Installer Tabellen erstellt – auf einer insgesamt sehr eingängigen Philosophie. Zum Feintuning des Windows Dienstes VCR.NET Recording Service habe ich bisher Custom Actions verwendet und mir aber nun zum Ziel gesetzt, möglichst viel Standardfunktionalitäten des Windows Installers zu nutzen. Das hat zwar hier funktioniert, aber nur mit Haken und Ösen und nicht übertragbar auf größere Projekte. Trotzdem ein kleiner Erfahrungsbericht.

Die Installation des Dienstes erfolgt über die ServiceInstall Tabelle des Windows Installers – mit Hilfe des WiX Elementes ServiceInstall. An sich sehr einfach, einsichtig und elegant bis zu der Tatsache, dass die StartType Spalte eine Zahl ist und nicht dynamisch durch Properties des Installationsvorgangs berechnet werden kann. Ich möchte aber gerne dem Anwender die Möglichkeit geben, den Dienst auch mit manuellem Start (Vorgabe ist automatischer Start) zu installieren – etwa, wenn man sich nur den Quellcode anschauen möchte und nicht plötzlich nach dem nächsten Booten irgend einen Windows Dienst laufen haben will. Die scheinbar offizielle Lösung ist, für beide Varianten eigene Installer Komponenten mit unterschiedlichen Bedingungen zu hinterlegen. Zwar meckert dann die MSI Validierung (ICE30), was aber hier eine kontrollierte Warnung ist. Viel lästiger ist natürlich, dass man in der Tat eine doppelte Konfiguration hinterlegen muss – schade und fehleranfällig.

Auch wenn man das erst einmal als notwendiges Übel akzeptiert kommt direkt der nächste Rückschlag: auch in der ServiceControl Tabelle ist die Spalte Event wieder eine Konstante und nicht dynamisch während der Installation berechenbar ist. Ich möchte aber in der VCR.NET Installation auch erlauben, dass der Windows Dienst nach der Installation nicht automatisch gestartet wird. Natürlich kann man auch hier wieder den Weg mit den bedingten Komponenten gehen, dann sind es aber schon vier Kopien von quasi identischem Code.

Ich bin daher hier einen etwas anderen Weg gegangen: in der Installationssequenz wird die StartServices Aktion nur ausgeführt, wenn der Anwender dies auch tatsächlich wünscht. So kann in den Komponenten zum Dienst immer ein Startwunsch geäußert werden, der dann aber eventuell einfach nicht umgesetzt wird. Würde die Installation mehrere Dienste beinhalten und würde man grundsätzlich nur entscheiden, ob alle Dienste entweder gestartet werden oder halt nicht, so ist diese Lösung sicher tragbar.

Allerdings ist das auch der Knackpunkt. Eine Installation mit mehreren Diensten und individuellen Auswahlmöglichkeiten beider Optionen erzeugt eine ganze Menge Copy & Paste in der Installationsbeschreibung – egal, ob mit WiX erstellt oder anderweitig. Das wäre für mich dann schon der Punkt, an dem man über eine Custom Action nachdenken sollte. Im Nachhinein frage ich mich nur, nach welchen Kriterien wirklich entschieden wurde, ob eine Spalte einer Windows Installer Tabelle statisch oder dynamisch ist. Oft ist klar, dass potentiell mehrsprachige Spalten wie Anzeigename oder Beschreibung berechenbar sind. Die ganze Infrastruktur des Windows Installers wäre aber vermutlich viel mächtiger und vielseitiger, wenn alle Spalten dynamisch verfügbar wären. Aber da ist wohl nicht mehr dran zu rütteln…

So long

Jochen

Yield und Reacquire in MSBuild Tasks

So richtig klar war mir die Verwendung dieser Methoden in einer eigenen MSBuild Task nicht – bis es dann knallte. Daher hier eine Anwendung mit meiner vermuteten Interpretation des Verhaltens, vielleicht hilft das ja dem einen oder anderen beim Verständnis und der Entwicklung eigener MSBuild Tasks.

Die Aufgabenstellung war eigentlich recht einfach und vermutlich auch nicht unüblich: eine größere Anzahl (800+) von Projekten soll mit MSBuild so schnell wie möglich auf einer eventuell potenten Maschine gebaut werden – im Test ein Dual-Xeon mit insgesamt 8 Kernen auf 2 Prozessoren mit zusätzlichem Hyper Threading, also insgesamt 16 logischen Kernen sowie einer SSD. Erst einmal sieht das aus wie eine triviale Aufgabe für die MSBuild Task und der BuildInParallel Option. Natürlich kann es nicht so einfach sein, da die Projekte untereinander abhängig sind und in einer bestimmten Reihenfolge gebaut werden müssen. Also schnell mal eine Visual Studio (2013) Solution On-The-Fly gemacht und die mit der MSBuild Task gebaut. Das geht im Prinzip auch aber leider nicht in dem konkreten Fall: neben C++ und C# Projekten gibt es besondere Projektarten, die Visual Studio und MSBuild nicht kennen. Theoretisch kann man das natürlich in die Infrastruktur von MSBuild einbringen (à la WiX et al), aber ich bin hier einen anderen Weg gegangen.

Ich verzichte auf die Details und erzähle nur das, worum es hier geht: die Projekte werden mit ihren Abhängigkeiten untereinander als MSBuild Items angelegt und einer MSBuild Dispatcher Task übergeben. Diese startet auf jedem MSBuild Knoten einen Build Vorgang mit einer speziellen Worker Task. Jede dieser Worker Tasks fordert bei dem Dispatcher ein Projekt zum Bauen an, baut dieses und teilt dem Dispatcher dann das Ergebnis mit. Der Dispatcher sorgt dafür, dass die Projekte in der korrekten Reihenfolge gebaut werden. Die Kommunikation zwischen Worker und Dispatcher ist über einen trivialen In-Process WCF Dienst und ein Named Pipe Binding geregelt.

Das funktionierte auch eine Weile und dann stand alles still – ein Deadlock. Was war passiert? Wenn ein Worker den Dispatcher nach einem neuen Projekt zur Bearbeitung fragt kann es sein, dass gerade keines zur Verfügung steht, weil abhängige Projekte noch in Berechnung sind. In diesem Fall wartet der WCF Aufruf des Workers, bis alle abhängigen Projekte abgearbeitet sind. Aus Sicht von MSBuild ist der MSBuild Node des Workers aber damit in Benutzung und kann nicht für andere Zwecke genutzt werden. In der besonderen Situation ergab es sich, dass alle Nodes bis auf einen (also hier: 15) auf eine besonders zentrale Abhängigkeit warteten, die von dem verbleibenden Node gebaut wurde. Dummerweise erforderte dieses Bearbeiten aber, dass MSBuild eine weiteren Node zur Abarbeitung anfordern musste und damit stand alles.

Hier kommt nun Yield ins Spiel: unmittelbar vor dem WCF Aufruf des Workers an den Dispatcher teilt die Dispatcher Task MSBuild via BuildEngine3.Yield() mit, dass der Node nun anderweitig beschäftigt ist. MSBuild kann dem Node nun andere Aufgaben zuordnen und der Deadlock ist weg. Wichtig ist, dass nach dem WCF Aufruf und sicher VOR dem nächsten MSBuild Zugriff des Workers ein BuildEngine3.Reacquire() erfolgt. Sollte auf dem Node zu diesem Zeitpunkt noch eine andere Aufgabe aktiv sein, so wird MSBuild den Reaquire Aufruf blockieren (das ist eine Vermutung, die aber durchaus Sinn macht), bis die andere Aufgabe abgeschlossen ist. Danach widmet sich der MSBuild Node wieder gänzlich unserer Worker Task.

Viel Spaß beim Coden

Jochen

VCR.NET goes IIS?

In dem folgenden Post werde ich kurz ein neues Rand-Feature vorstellen, das VCR.NET 4.3 anbieten wird. Es schien mir jedoch angebracht, kurz gesondert auf die Motivation dahinter einzugehen – denn eigentlich braucht man so etwas doch nicht. Aber bevor ich zum eigentlichen Thema komme eine kleine Einführung und Vorgeschichte.

Wie sicher bekannt ist VCR.NET in wesentlichen Aspekten vor allem Anderen meine persönliche Plattform um neue Technologien praktisch zu erforschen. Schon sehr früh war in VCR.NET die ASP.NET Laufzeit integriert, die es mir am Anfang sehr einfach machte SOAP Web Dienste über die ASMX Technologie anzubieten. In einer späteren Version wurde diese vorhandene Laufzeit verwendet um den damaligen SOAP (Fat-)Client abzulösen und direkt von VCR.NET einen Web Client zur Verfügung zu stellen, der auf ASP.NET Web Forms basierte. Hat man aber einmal ein virtuelles Verzeichnis in dem ASP.NET läuft, so ist es sehr einfach weitere (Teil-)Anwendungen darin unterzubringen. Eine dieser Anwendungen war eine kleine Mediendatenbank, mit der ich meine Fernsehaufzeichnungen und gekauften Filme verwaltet habe. Nichts wirklich Tolles, aber eine neue Ecke zum Lernen und dem Zweck angemessen.

Mit der Umstellung des VCR.NET Web Clients auf HMTL(5) und JavaScript kam mir allerdings die Web Forms Lösung immer wieder plump und einfach “anders” vor, so dass ich mich entschlossen habe auch diese Anwendung entsprechend umzustellen. Da aus einer ganz anderen Richtung auch die Notwendigkeit kam sich einmal näher mit dem Microsoft Entity Framework auseinander zu setzen, habe ich die Gelegenheit beim Schopfe gegriffen und die Mediendatenbank ganz neu aufgesetzt – wieder als private Minimallösung, aber doch mit einigen Sonderlocken wie zum Beispiel direkter SQL Programmierung (in sehr engem Rahmen) gegen eine SQL Server 2014 Express / LocalDb Datenbank [hier geht es um einfache Trigger und rekursive Abfragen einer Hierarchie mit Hilfe von Common Table Expressions]. So als Erfahrung in einem Satz: bis auf die Performance beim initialen Einspielen vieler Entitäten war ich doch sehr positiv überrascht vom EF Konzept, der Umsetzung von LinQ auf SQL und der Geschwindigkeit der LocalDb.

Der erste Gedanke zur Freischaltung war es, die Mediendatenbank in meinem lokalen Internet Information Service zu veröffentlichen. Allerdings habe ich nur ein Windows 7 Home Premium was mir sofort eine Überraschung bescherte: der hier integrierte IIS unterstützt die integrierte Windows Autorisierung nicht, das ist höheren Versionen vorbehalten. Tatsächlich hätte mir hier auch der anonyme Zugriff gereicht, aber “leider” habe ich das schnell als Herausforderung gesehen. Vielleicht wäre ja auch IIS-Express eine Lösung, aber warum eine Installationsvoraussetzung mehr wenn ich eigentlich immer einen Web Server zur Hand habe: den VCR.NET Recording Service!

Ab der Version 4.3 wird VCR.NET neben der Hauptanwendung weitere ASP.NET Anwendungen anbieten können – quasi wie ein kleiner IIS. Die Autorisierungsmöglichkeiten sind zwar stark eingeschränkt, aber die integrierte Windows Autorisierung und Basic sind möglich, genauso eine SSL Verschlüsselung. Allerdings ist der Web Server im VCR.NET kein echter IIS, sondern nur ein Host für die ASP.NET Laufzeitumgebung. Technisch gesehen bietet er Möglichkeiten, die irgendwo zwischen dem Classic und dem Integrated Pipeline Mode vom IIS angesiedelt sind – die Modulunterstützung entspricht fast dem mächtigeren Pipeline Modus, auch wenn natürlich die üblichen IIS Module ohne weitere Maßnahmen nicht geladen werden. Die ASP.NET Laufzeitumgebung meldet sich allerdings den Anwendung als Classic Modus – eine IIS Version ist zudem nicht verfügbar.

Abgesehen von einem echten Fehler im Hosting der ASP.NET Laufzeitumgebung, der aber leicht zu korrigieren war, sieht das bisher ganz ordentlich aus. Ich werde das in den nächsten Wochen mal genauer beobachten.

Soweit zu dieser Erfahrung.

Viel Spaß und vielleicht schon einen schönen Urlaub

Jochen