Alles wird async!?

Wir sind im Begriff NDO durchgängig mit asynchronen Methoden auszustatten, sodass Ihre Anwendungen mit weniger Threads und damit weniger Overhead auskommen. Die Erfahrungen aus der Umstellung anderer Projekte lassen erwarten, dass vor allem für Web-Projekte ein Performance-Boost zu erwarten ist.

Die Umstellung ist für NDO 4.5 geplant.

Um es kurz zu sagen: Je mehr Traffic Sie in ihren Web-Applikationen haben, umsomehr lohnt sich der Umstieg auf konsequente Umsetzung mit async/await-Konstrukten.

Auf den ersten Blick sieht das alles ziemlich einfach aus. Sie fragen Ihre Objekte mit einer asynchronen Methode ab:

var myObjects = await pm.Objects<Product>().Where(p=>p.Name==name).GetResultsAsync();

Wenn Sie die Objekte verändert haben, können Sie alle Änderungen in einer asynchronen Transaktion speichern:

await pm.SaveAsync();

Ersatz für DataAdapter

Wir nutzen intern die Datenstrukturen des System.Data-Namespace, also DataTable, DataRow, etc. Die Datenstrukturen haben den Vorteil, dass für jedes veränderte Objekt der ursprünglichen Status in einer DataRow gehalten werden kann. Damit lassen sich zum Beispiel Audit-Datensätze zu erzeugen, die eine Überprüfung von Änderungen ermöglichen.

Die Datenstrukturen wurden bislang von einem DataAdapter gefüllt bzw. in die Datenbank übertragen. Leider hat Microsoft es bis heute nicht geschafft, die DataAdapter-Klasse auf async/await umzustellen. Wer sich den Code ansieht, wird schnell bemerken, dass das Design der DataAdapter-Klassen einige Mängel aufweist, die eine Umstellung erschweren, wenn gleichzeitig die vorhandenen Provider noch funktionieren sollen.

Wer sich die Diskussion auf Github anschaut (vor allem gegen Ende des Threads), wird schnell feststellen, dass es wohl so schnell keine Lösung mehr geben wird – wenn überhaupt.

Wir haben daher die Fill- und Update-Methoden der DataAdapter ersetzt durch eigenen Code, der DataReader-Objekte zum Lesen und DbCommand-Objekte zum Schreiben nutzt. Dadurch wird es in NDO keinen Einsatz von DataAdaptern mehr geben.

ConfigureAwait

Um Kompatibilität mit bisherigen Anwendungen zu ermöglichen, werden die synchronen Methoden beibehalten. Sie sind letztlich nur Wrapper über die asynchronen Methoden und verwenden

GetAwaiter().GetResult()

um sicherzustellen, dass der gegenwärtige Thread die asynchrone Bearbeitung fortsetzt. Damit dies keine Deadlocks erzeugt, wurde beim Einsatz von await darauf geachtet, durchgängig mit ConfigureAwait(false) einen neuen Kontext zu erzeugen, sodass die asynchrone Bearbeitung nicht im ursprünglichen Thread fortgesetzt wird. Die durchgängige Nutzung von ConfigureAwait(false) sollte für jede Bibliothek, die asynchrone Methoden anbietet, Standard sein.

Konsequenz für Ihre Programmierung

Die Umstellung sieht nach einer Fleißaufgabe aus, bis durchgängig alle Methoden auf async/await umgestellt sind. Aber so einfach ist es nicht. NDO ist eine transparente Persistenzlösung. Hier gibt es Hollow-Objekte und nicht geladene Relationen. Ein Beispiel dafür können Sie hier nachlesen.

Das heißt: Wenn Ihr Code ein Objekt oder eine Relation "anfasst", dann werden die Daten automatisch nachgeladen. Nun könnte man glauben, dass der Code, der die Daten nachlädt, doch einfach auf async umgestellt werden könnte. Doch der Code, der dafür sorgt, wird vom NDOEnhancer in den persistenten Klassen erzeugt.

Wenn der Code jetzt auf einmal async werden sollte, müsste die gesamte Methode, in der der Zugriff auf das Objekt oder die Relation erfolgt, von synchron auf asynchron umgestellt werden. Aber nicht nur das: Die gesamte Aufrufkette müsste auf async umgestellt werden. Kurz: Diese Option kommt nicht in Frage.

Man könnte nun sagen: Wir haben eine asynchrone und eine synchrone Version der Methoden, die für das Laden der Objekte aufgerufen werden muss. Wenn sich der Code in einer synchronen Methode befindet, wird die synchrone Variante verwendet, im asynchronen Fall die asynchrone Variante.

Wer sich schon einmal den vom Compiler generierten Code für asynchrone Methoden angesehen hat, wird feststellen, dass die Methoden völlig umgestellt wurden. Es wird eine Hilfsklasse erzeugt und in dieser eine State Machine aufgebaut. Für jeden await-Aufruf entsteht ein Status in dieser State Machine. Der Enhancer müsste jetzt an der richtigen Stelle in der State Machine einen weiteren Status einbauen, um den asynchronen Aufruf der Methode zum Nachladen von Objekten und Relationen zu ermöglichen.

Wir haben uns entschieden, dies nicht zu tun. Das transparente Nachladen von Objekten und Relationen bleibt synchron.

Aber Sie können jederzeit die synchrone Version händisch einsetzen. Betrachten Sie folgenden Code:

var emp = pm.Objects<Employee>().Where(p=>p.Name == "Matytschak").Single();
foreach(var travel in emp.Travels)
...

Hier werden die Travel-Objekte nachgeladen, sobald das Property Travels auf das zugrunde liegende Feld zugreift. Sie können den Code aber folgendermaßen ändern:

var emp = pm.Objects<Employee>().Where(p=>p.Name == "Matytschak").Single();
await pm.LoadRelationAsync(emp, _=>emp.Travels); // Loads Travel objects async
foreach(var travel in emp.Travels)
...

Im synchronen Aufruf, der testet, ob die Relation schon geladen ist, wird nun festgestellt, dass die Objekte schon existieren und es ist keine synchrone Operation zum Laden der Objekte nötig. Elegant ist das nicht, aber es ist ein Handel: Sie tauschen Effizienz gegen Transparenz.