Kleine Falle ASP.NET Hosting: HttpListener und Threads

Die einfachste Methode, ASP.NET zu hosten ist es, den HttpListener als HTTP Protokollschicht einzusetzen (wenn man nicht gerade eine eigene Implementierung schreiben will). Allerdings gibt es hier einen kleinen Haken: der HttpListener scheint an den Thread gebunden zu sein, der ihn gestartet hat (Start oder BeginGetContext).

Startet man den Listener auf dem Haupthread der Anwendung, funktioniert alles wie gewünscht und der Listener erkennt automatisch das Ende der Anwendung. So weit so gut. Wenn man allerdings einen Thread verwendet, der explizit oder implizit beendet wird, so terminiert auch der Listener. In meinem Fall knallte es beim expliziten Beenden: ich hatte einen Thread, der einfach nur den Listener konfigurierte und startet und sich dann beendet – damit die Initialisierung der Anwendung (in diesem Fall der VCR.NET Windows Dienst) im Hauptstrang so schnell wie möglich durchläuft.

Mit ThreadPool.QueueUserWorkItem geht es dann, aber mit einer im Hintergrund drohenden Gefahr: der ThreadPool beendet die Aufgabe und verwendet den Thread dann später für andere Aufgaben weiter. Der Listener bleibt an diesen Thread gebunden, der überhaupt nichts mehr mit ihm zu tun hat. Und wenn der ThreadPool irgendwann mal einen Thread wegwirft? Den Fehler findet man vermutlich nie!

Ich habe mich daher entschieden, mich nicht auf die Willkür zu verlassen und halte den Thread, der den Listener hochfährt, bis zum Ende der Anwendung oben (über ein Manual/AutoResetEvent). Nur so habe ich ein gutes Gefühl, dass nicht doch plötzlich die Kommunikation des Clients zum ASP.NET zusammenbricht.

Zumindest sollte man das im Hinterkopf haben!

Jochen

Serverarchitektur bei ASP.NET Hosting

Vielleicht einfach ein paar kurze Gedanken zu den letzten Arbeiten an VCR.NET 3.9, etwas abstrahiert. Im Endeffekt geht es darum, eine Serverfunktionalität mit präziser Lebenszeitkontrolle mit einer Web Oberfläche anzubieten. Die Kontrolle der Lebenszeit läßt sich ideal durch das Hosten des Servers in einem Windows Dienst erreichen, für die Web Oberfläche wäre natürlich der Microsoft Internet Information Server die bevorzugte Lösung. Mit dem ASP.NET Hosting läßt sich beides weitgehend kombinieren (es ist allerdings kaum möglich, alle Autorisierungsoptionen des IIS nachzubilden, aber das braucht man auch nicht immer).

Zu beachten ist allerdings, dass ein virtuelles Verzeichnis immer in einer eigenen .NET AppDomain läuft. Unser Ansatz besteht daher immer aus mindestens zwei AppDomains, was einige besondere Aspekte mit sich bringt. Ich stelle hier in Stichworten einmal die beim VCR.NET implementierte Lösung vor.

1. Der .NET Dienst wird natürlich in einer ersten AppDomain gestartet. Hier soll auch der Server leben.
2. Der Server wird als .NET Instanz einer MarshalByRefObject Klasse (Server) erzeugt.
3. Für das ASP.NET Hosting wird eine weitere MarshalByRefObject Klasse (Host) benötigt, die in dem virtuellen Verzeichnis erstellt wird.
4. Nach dem Starten des virtuellen Verzeichnisses erhält Host die Server Referenz aus der ersten AppDomain.
5. Alle Zugriffe aus dem virtuellen Verzeichnis erfolgen über dese Referenz und werden in der ersten AppDomain ausgeführt.
6. Damit die beiden (Server und Host) Inter-AppDomain Referenzen nicht durch die Remoting Lebenszeitkontrolle spontan gelöst werden, melden deren InitializeLifetimeService Methoden null.
7. Alle Klassen, die zwischen den AppDomains ausgetauscht werden sollen, müssen MarshalByRefObject oder [Serializable] sein. VCR.NET setzt auf serialisierbare Klassen, so dass zwar für die Serialisierung und Deserialisierung bezahlt werden muss, die Objekte dann aber ohne Kommunikationsverluste weiter verwendet werden können. Man beachte, dass hier die binäre Serialisierung zum Einsatz kommt – man siehe dazu einen früheren Eintrag in diesem BLog.

Zur Verwaltung der Konfiguration wird ein einziges Konfigurationsobjekt eingesetzt, über das alle Zugriff (auch schreibend) erfolgen. Dieses wird in der ersten AppDomain angelegt. Damit es nicht in alle Klassen durchgereicht werden muss (hier gibt es etwa Methoden wie Log(string message)), erhählt die Konfigurationsklasse (Config) ein statisches Feld Current, dass bei der Intialisierung des Windows Dienstes auf der ersten AppDomain belegt wird (Config.Current = new Config()). Zusätzlich bietet die Server Klasse eine Eigenschaft zum Zugriff auf Config.Current an. In Schritt 4. oben wird in der AppDomain des virtuellen Verzeichnisses eine Referenz des eindeutigen Konfigurationsobjektes in das statische Feld als Referenz übernommen (Config.Current = Server.CurrentConfiguration) – ein anderes statisches Feld, da statische Felder nur pro AppDomain eindeutig sind. So arbeiten beide AppDomains immer mit der selben Konfiguration – Config ist natürlich ein MarshalByRefObject, dessen InitializeLifetimeService null liefert.

Nun, für einige ist das vermutlich trivial, aber vielleicht hilft die Information hier dem einen oder anderen, einige Fallstricke zu vermeiden und etwas Zeit zu sparen.

Viel Glück

Jochen

Kleine Fallstricke der Serialisierung

Eine Klasse soll als Ergebnis eines guten alten ASMX basierten SOAP Web Service von einem Server an einen Client gesendet werden. Die Klasse hat neben den Eigenschaften, die den Client etwas angehen, auch noch interne Strukturen, die für die Verarbeitung genutzt werden. Diese werden geeignet markiert, etwa so:

[Serializable, XmlType("Test")] public class Item
{
[XmlAttribute("id")] public string UniqueName { get; set; }
[XmlIgnore] public Context State { get; set; }
}

Hier werden automatische Felder (auto-implemented properties) benutzt, i.e. zu den deklarierten .NET Eigenschaften UniqueName und State gibt es für den Entwickler unsichtbar zugehörige .NET Felder der Klasse. Was aber, wenn die Serveranwendung tatsächlich aus mehreren AppDomains besteht, zwischen denen Instanzen der Klasse ebenso ausgetauscht werden sollen – typisch ist ein (ASP.NET) Hosting Szenario, wobei die eigentlichen Serveralgorithmen in einer AppDomain mit kontrolliertem Lebenszyklus (etwa einem Windows Dienst) implementiert sind und eine oder mehrere andere AppDomains die Kommunikation mit dem Client regeln und etwa dynamisch bei Veränderungen nachgeladen werden (etwa durch die ASP.NET Dateiüberwachung).

Die hier verwendete SOAP Serialisierung zum Client verwendet die XML Serialisierung. Diese berücksichtigt ausschließlich öffentliche (public) .NET Eigenschaften, das Ausschließen einzelner Eigenschaften geschieht wie im Beispiel durch das XmlIgnoreAttribute. Zwischen AppDomains wird allerdings die binäre Serialisierung verwenden (Kann man das wählen? Ich habe nichts dazu gefunden.), die sich an .NET Feldern und dem NonSerializedAttribute orientiert. Konkret heißt das im Beispiel, dass zwischen AppDomains beide Eigenschaften ausgetauscht werden.

Das kann natürlich so gewollt sein, keine Frage. Aber dann wäre im Beispiel der Context auch entweder serialisierbar oder ein MarshalByRefObject. Passiert es ungewollt, so kann das je nach Situation einen empfindlichen Einfluss auf die Laufzeiten haben, da mit kleinen Instanzen eventuell sehr große angehängte Objekte stillschweigend mit serialisiert werden oder unerwartete AppDomain kreuzende Aufrufe vorgenommen werden. Leider hat Microsoft aber wohl den einfachen Weg nicht implementiert, der für event Felder prima funktioniert:

[XmlIgnore][field:NonSerialized] public Context State { get; set; }

Daher ist es notwendig, auf die automatischen Eigenschaften zu verzichten und selbst ein Feld anzulegen. Nicht tragisch, aber auch nicht elegant.

[NonSerialized] private Context m_State;
[XmlIgnore] public Context State { get { return m_State; } set { m_State = value; } }

DVB.NET hat in der Klasse SourceSelection ein weiteres Problem: hier sollen Eigenschaften, die auf Objekte verweisen, als Zeichenkette serialisiert werden, wobei diese durch eindeutige Namen der Objekte zusammengesetzt werden. Dabei kommt man leider um ISerializable und Konsorten nicht herum, wenn man die binäre Serialisierung genauso nutzen möchte wie die XML Serialisierung – egal ob explizit oder wie beschrieben implizit durch AppDomain Grenzen bedingt.

Happy Coding

Jochen

Spaß mit Extensionmethoden – hier: zyklische Referenzen von Assemblies

Man stelle sich eine Anwendung vor, die aus zwei Assemblies besteht. Die eine enthält ausschließlich Klassen, die zur Konfiguration dienen und etwa in die XML Repräsentation serialisiert werden. Als Beispiel eine Klasse StationInformation, die zu einem Fernsehsender alle Informationen für einen DVB Empfang beinhaltet (PIDs, um es konkret zu machen). Diese Assembly ist von nichts ausser .NET abhängig. Eine andere DLL implementiert einen DVB Zugriff über BDA und ist reichlich von diversen Bibliotheken abhängig. Natürlich auch von unserer ersten Assembly. In der zweiten Assembly gibt es etwa eine Klasse DVBHardware mit einer Methode UpdateStation, die eine StationInformation als Parameter erhält und die Empfangsdaten auf den aktuellen Stand bringt (etwa gibt es nur zeitweise einen Videotext wie bei KiKa).

Die Abhängigkeit der Assemblies zwingt nun erst einmal einen Anwender der Bibliotheken, die DVBHardware Instanzen ins Zentrum seines Denkens zu stellen: nur hier kann es Methoden geben, die beide Assemblies nutzen. Mit einer Extensionmethode läßt sich aber nun einfach eine Flexibilität erreichen, die eine natürlicher Sicht auf die Dinge erlaubt (eigentlich interessiert mich der Empfang eines Senders, die verwendete Hardware ist eigentlich zweitrangig): mit

public static void Update
(
this StationInformation station,
DVBHardware hardware
)
{
hardware.Update(station);
}

kann nun alternativ auch StationInformation.Update(hardware) aufgerufen werden. Aus Sicht des Anwenders, der beide Assemblies gleichzeitig verwendet und beim IntelliSense nicht so genau hinschaut sieht es fast aus, als würden sich die beiden Assemblies hier gegenseitig (zyklisch) referenzieren.

Wie dem auch sei und unabhängig davon, ob das Beispiel trägt: Extensionmethoden machen es beim Design von Bibliotheken einfacher, sich nicht schon früh auf eine Sichtweise festlegen zu müssen. Eine vollständig symmetrische Sicht ist zwar nicht immer notwendig und man kann es dabei auch leicht übertreiben, aber in Einzelfällen halte ich das für eine durchaus interessante Option – ausserhalb der trivialen Bedeutung, scheinbar abgeschlossene Klassen mit neuen Instanzmethoden erweitern zu können.

Happy Coding

Jochen

.NET 3.5 SP1 und ASP.NET Run-Time Hosting

VCR.NET ist ein Windows Dienst, der sich als ASP.NET Web Server präsentiert. Kern dazu ist eine Implementierung der HttpWorkerRequest Basisklasse. Die ursprüngliche Realisierung beschränkte sich auf das Notwendigste, i.e. die Methoden, die im Betrieb auch tatsächlich aufgerufen wurden. Seit SP1 von .NET 3.5 wird nun leider auch die Methode void SendResponseFromFile( string filename, long offset, long length ) für statische Inhalte verwendet (Bilder, HTML, CSS, …), was im konkreten Fall von VCR.NET dazu führte, dass die Anwendung zwar noch lief, aber sehr komisch aussah.

Glücklicherweise ist zumindest eine dumme Implementierung der Methode recht trivial, aber man muss es halt erst einmal tun. Wer also auch ASP.NET Run-Time Hosting betreibt und sich auf eine Minimalimplementierung stützt: lieber mal reinschauen, bevor es eine optisch unschöne Überraschung gibt.

Viel Spaß

Jochen