Liberating my blog :) Just Married, and a Windows Phone Rant

Vereinheitlichung von Businesslogik durch serverseitiges JavaScript

Published on Sunday, April 12, 2015 7:39:04 PM UTC in Programming

Prolog

Beim Stichwort "serverseitiges JavaScript" denkt vermutlich jeder zunächst an Node.js (oder io.js). Allerdings beschäftigt sich dieser Artikel mit der Ausführung von JavaScript in C#, also im Kontext einer .NET-Anwendung. Tatsächlich müsste es sich dabei nicht einmal unbedingt um eine Serveranwendung handeln, die Ideen sind auch problemlos auf Rich Clients übertragbar. Die Motivation entstand aber unmittelbar aus den Anforderungen einer verteilten (Web-)Anwendung heraus (s.u.).

Motivation

Das Problem ist so alt wie die Menschheit selbst (nunja, fast): schon alleine auf Grund von Erwägungen der Datensicherheit und –konsistenz müssen die Validierung von Daten und zugehörige Businesslogik einer Webanwendung, die clientseitig zur Verfügung stehen, auch nochmals serverseitig ausgeführt werden. In Zeiten dicker Internetleitungen (oder bei Enterprise-Anwendungen, die in lokalen Netzwerken zum Einsatz kommen) ist es eigentlich völlig problemlos, die Clientseite durch asynchrone Serveranfragen abzudecken, was die Problematik eventuell nötiger Doppelimplementierungen elegant löst. Was aber, wenn es sich um eine HTML5-Anwendung mit Offlinefähigkeiten handelt? Was, wenn der Client vollständig ohne Internetverbindung auskommen können muss, etwa als HTML5-Anwendung, die als Cordova- oder Windows-App entwickelt wird?

Lösungsansätze

Die triviale Lösung für das eben genannte Problem ist, einfach zwei Implementierungen der nötigen Logiken zu erstellen. Das allerdings birgt ein großes Fehlerpotenzial. Es besteht nicht nur die Gefahr, dass bei Änderungen auf der einen die andere Seite vergessen wird. Zusätzlich ist es auch ein durchaus reales Problem, dass ggfs. die clientseitige Implementierung in Details wie etwa der Verfügbarkeit von Datentypen abweicht (man denke nur an number vs. int/float/decimal) und sich hierdurch subtile Fehler einschleichen.

Am anderen Ende der Skala steht die vollständige Vereinheitlichung von Client und Server, was heutzutage einer Migration des Servers zu Node.js gleichkommt. Für viele Unternehmen ist das nicht nur aufwandstechnisch bei bestehenden Enterprise-Anwendungen völlig utopisch. Es bedeutet auch ganz neue technische Probleme, gerade bei der Integration bestehender Drittanwendungen, und den Verlust von bestehendem Know-How bzw. die Notwendigkeit zum Aufbau neuen Wissens. Außerdem ist meine persönliche Meinung noch immer, dass der nötige Reifegrade für unternehmenskritische Anwendungen bei Node.js und Co. (noch?) nicht gegeben ist. Mir sträuben sich die Nackenhaare schon bei der Vorstellung, etwa buchhalterische Systeme in JavaScript zu erstellen. Zudem ist die Werkzeugkette vorsichtig ausgedrückt verbesserungswürdig, solange "auf dieser Seite des Zaunes" noch ASP.NET mit Visual Studio und Konsorten zur Verfügung stehen.

Irgendwo dazwischen findet man Dutzende mehr oder weniger hilflose Versuche, die Situation mit Cross-Compilern zu verbessern. C# nach JavaScript, JavaScript nach C#, C# zu TypeScript, IL zu JavaScript, Schwäbisch zu Badisch – die Liste an Optionen ist lang, auch wenn die wenigstens Projekte eine enthusiastische Anfangsphase überlebt haben und tatsächlich noch aktiv gepflegt werden. Das grundsätzliche Problem dieser Ansätze ist, dass sich meist ein nicht unerheblicher, zunächst vielleicht verdeckter Aufwand anschließt: wie ist der Workflow für den Entwickler? Wie integrieren wir einen solchen Ansatz in ein automatisiertes Build-System? Wie sieht die Deployment-Story aus? Selbst bei Microsofts TypeScript kann der Weg sehr steinig werden, an dessen Ende ein funktionierendes Build-System stehen soll.

Aber auch wenn diese infrastrukturellen Fragen zufriedenstellend beantwortet werden können, sind die Probleme noch lange nicht zu Ende. Cross-Compiler können beispielsweise naturgemäß immer nur mit einem Subset der vorhandenen Features einer Sprache und deren Klassenbibliothek umgehen, was oft zumindest einschränkt, ab und zu aber auch zu hässlichen Workarounds führt. Noch schlimmer wird es, wenn der Cross-Compiler versagt und nicht das auswirft, was man erwartet hatte – es gibt kaum eine nervtötendere Arbeit, als willkürlich an den Eingabeparametern einer Black Box zu schrauben, bis irgendwann ein Ergebnis herausfällt, das einigermaßen brauchbar ist.

Wäre es nicht das Einfachste, server- wie clientseitig einfach dieselbe Implementierung nutzen zu können, ohne gleich die gesamte Infrastruktur austauschen zu müssen?

Ein Experiment

Als ich meine Suche nach einer solchen Lösung begann, erwartete ich das übliche Bild: eine Hand voll Open-Source-Projekte, von denen die eine Hälfte im Alpha-Stadium steckengeblieben ist und die andere Hälfte stundenlange Konfigurationsarbeit verlangt, bis man ein leidlich funktionierendes System zusammengestellt hat. Daher war ich einigermaßen erstaunt, als ich ClearScript fand. Ein paar Eckdaten des Projekts:

  • Open Source, erstellt und gepflegt von Microsoft
  • Erstmals veröffentlicht im Januar 2013, aktiv gepflegt (Stand Anfang 2015)
  • Sehr einfach zu nutzen und minimal invasiv – keine Annotations, speziell zu implementierenden Interfaces o.ä.
  • Unterstützt V8, JScript (und VBScript)
  • Extrem positive Reviews von Entwicklern

Natürlich ist all das weder ein Garant für Qualität noch für langfristigen Support, aber es weckte sofort mein Interesse.

Setup

Eine Einstiegshürde gilt es zunächst allerdings zu nehmen: es stehen keine Binärdateien unmittelbar zum Download zur Verfügung. Vielleicht hat das rechtliche oder politische Gründe – schließlich handelt es sich um ein Microsoft-Projekt, das u.a. Googles V8-Engine nutzt.

Wer möchte, kann auf eines der vorhandenen inoffiziellen NuGet-Pakete zurückgreifen, muss dann aber dem jeweiligen Autor vertrauen, dass er gute Arbeit geleistet und nichts am Projekt verändert hat. Vorsichtigere Naturen übersetzen das Projekt inklusive der nötigen V8-Bibliotheken selbst, was sich als ausgesprochen einfach darstellt. Im Root-Verzeichnis des ClearScript-Quellcodes liegt eine Datei "V8Update.cmd", die zunächst die Sourcen von V8 herunterlädt und dann alle nötigen DLLs erstellt – in der zu ClearScript passenden bzw. getesteten Version und in 32- sowie 64-bit-Varianten. Danach kann man einfach das ClearScript-Projekt in Visual Studio bauen und das Ergebnis aus dem Ausgabeverzeichnis fischen. Der gesamte Prozess dauert kaum weniger als ein paar Minuten.

Im eigenen Projekt sind dann nur zwei Dinge zu tun: eine Referenz auf die erzeugte ClearScript-Assembly setzen…

image

… und die nötigen nativen DLLs ins Ausgabeverzeichnis der Anwendung kopieren (lassen) bzw. in einer Web Application als Content im Root-Verzeichnis einbinden:

SNAGHTML22db76

Fertig.

Ein Beispiel

Zum Testen habe ich mich direkt an der Integration mit TypeScript versucht. Zum ist TypeScript mein bevorzugtes Instrument zum Entwickeln von komplexerem JavaScript, zum anderen war meine Erwartungshaltung, dass das völlig unkompliziert sein sollte – schließlich generiert TypeScript ja einfach nur standardkonformes JavaScript.

Hier das getestete Beispiel:

module Demo {
    export class Input {
        public value: number;
    }

    export class Result {
        public value: number;
        public message: string;
    }

    export class ImportantFeatures {
        public calculateResult(input: Input): Result {
            var result = new Result();
            result.value = 42;
            result.message = "Found the answer!";
            return result;
        }
    }
}

Wie man sieht, handelt es sich nur um einen Proof-Of-Concept mit Ein- und Ausgabe-DTO und einer Klasse, die eine Art Business-Logik, Validierung o.ä. implementieren könnte.

In nativem JavaScript könnte man etwa so damit interagieren:

(function() {
    var input = new Demo.Input();
    input.value = 123;
    var importantFeatures = new Demo.ImportantFeatures();
    var result = importantFeatures.calculateResult(input);
    document.getElementById("importantFeatureResult").innerHTML = "Client side: " + result.message + " (" + result.value + ")";
})();

So weit, so gut.

JavaScript serverseitig ausführen

Auf dem Server gestaltet sich die Nutzung desselben JavaScripts/TypeScripts ausgesprochen einfach:

using (var engine = new V8ScriptEngine())
{
    LoadScriptFiles(engine);

    engine.AddHostObject("input", new { value = 123 });
    dynamic result = engine.Evaluate(@"
        var importantFeatures = new Demo.ImportantFeatures();
        importantFeatures.calculateResult(input);");

    return "Server side: " + result.message + " (" + result.value + ")";
}

Zunächst einmal wird eine Instanz der V8-Engine erzeugt. Das "using"-Statement stellt sicher, dass die nativen Ressourcen auf jeden Fall auch wieder freigegeben werden. In einem zweiten Schritt lade ich das eigentliche Script, das mir TypeScript erzeugt hat – dazu gleich noch mehr. Ist alles vorbereitet, kann der eigentliche JavaScript-Code ausgeführt werden. Hierzu gibt es ein paar bemerkenswerte Details:

Per "AddHostObject" erzeugt man eine JavaScript-Variable (hier mit dem Namen "input"), die dann im Kontext der Engine zur Verfügung steht. Ich verwende einfach ein anonymes Objekt in C#, um mir eine separate Definition des in JavaScript definierten DTOs auf der Serverseite zu sparen.

Im zweiten Schritt wird einfach per "Evaluate" ein JavaScript-Ausdruck ausgewertet. In diesem Fall ist es das Erzeugen eines Objekts per "Demo.ImportantFeatures()" und der Aufruf der "calculateResult"-Funktion mit dem zuvor erzeugten "input"-Objekt. Der Code ist quasi identisch zu dem zuvor gezeigten händisch gebauten JavaScript. Das Ergebnis von "Evaluate" ist eigentlich vom Typ "V8ScriptItem", wobei es sich aber um ein dynamisches Objekt handelt. Per "dynamic"-Variable kann ich einfach auf die Eigenschaften des JavaScript-Objekts direkt zugreifen. Alles erstaunlich einfach und schlüssig. Wer sich die Eigenschaften des dynamischen Objekts genauer ansieht, wird zudem feststellen, dass etwa der Typ der "value"-Property schon Integer ist, ein entsprechendes Mapping bzw. Casten also schon stattgefunden hat.

Das letzte Puzzle-Teil ist, wie das von TypeScript erzeugte JavaScript den Weg in den Engine gefunden hat. Dazu nutzt man die Methode "Execute", die einfach nur einen (JavaScript-)String entgegennimmt, der direkt ausgeführt wird. Woher dieser String stammt, ist völlig unerheblich – man könnte also den zugehörigen Code aus einer Datei laden, aus einer Datenbank, oder on-the-fly im Speicher zusammenbauen.

In meinem Beispiel habe ich einen generischen Weg gewählt; da das JavaScript clientseitig über ein Bundle zur Verfügung gestellt wird, nutzte ich einfach dieses, um auch die serverseitige Engine zu befüllen – auf diese Weise müsste man auch bei Änderungen an der Zusammenstellung des Bundles keine weiteren Anpassungen vornehmen. Etwa so:

private void LoadScriptFiles(V8ScriptEngine engine)
{
    var bundle = BundleTable.Bundles.GetBundleFor("~/bundles/demo");
    foreach (var bundleFile in bundle.EnumerateFiles(new BundleContext(HttpContext, BundleTable.Bundles, bundle.Path)))
    {
        var path = Server.MapPath(bundleFile.IncludedVirtualPath);
        var content = System.IO.File.ReadAllText(path);
        engine.Execute(content);
    }
}

Ein Nachteil, der mir aufgefallen ist, betrifft den Scope der in der Engine erzeugten "Host Objects". Es scheint wohl keinen Weg zu geben, die Engine zurückzusetzen o.ä., so dass man für saubere weitere Aufrufe jeweils neue Instanzen erzeugen sollte. Untersuchungen zur Performance habe ich in diesem Zusammenhang noch nicht vorgenommen.

Fazit

Ich war überrascht, wie problemlos das mir bis jetzt völlig unbekannte ClearScript funktioniert. Daten aus C# zur Verfügung zu stellen geht ebenso einfach wie diese wieder abzugreifen. Vermutlich gibt es komplexere Szenarien, bei denen man zwangsläufig auf Probleme stoßen wird; ich weiß beispielsweise nicht, wie es sich mit Code verhält, der DOM-abhängig ist oder dazu entsprechende externe Bibliotheken nutzen möchte (jQuery und Co.). Da ich für meinen aktuellen Anwendungszweck keine Abhängigkeiten zur UI habe bzw. die Logik davon sauber abgekoppelt ist, habe ich in diese Richtung keine Tests unternommen. Alles andere sah soweit aber sehr vielversprechend aus. Diverse meiner TypeScript-Beispiele wurden anstandslos und fehlerfrei geladen und konnten problemlos verwendet werden. Ich bin gespannt auf mehr…

Tags: Architecture · Business Logic · ClearScript · Design · JavaScript · TypeScript