Das Ziel
Ein eigens geschriebener Code ermöglicht maximale Flexibilität und Anpassbarkeit. Um die Vorzüge davon auszureizen, liegt ein besonderer Fokus auf der Wiederverwendbarkeit des Parsers. Letztendlich wollen wir für unterschiedliche FlatFiles nicht die Implementierung des Parsers verändern, sondern ihm lediglich eine gemappte Klasse zur Verfügung stellen.
Die Werkzeuge
FlatFiles weichen untereinander enorm ab. Sie unterscheiden sich beispielsweise im Aufbau oder nutzen unterschiedliche Separatoren. Um den Parser über diese variablen Ausprägungen zu informieren, werden sie in der Mapping-Klasse als Attribute dargestellt. Die Attribute ermöglichen uns Metainformationen mit einem C# Element, wie Klassen und Properties, zu verbinden.
In der obigen Darstellung sieht man eine beispielhafte Mapping-Klasse. Die Klasse stellt das FlatFile-Dokument dar, eine Property ist ein Datenfeld und alle Properties im Verbund sind ein Datensatz. Dateiweite Metainformationen, wie das Dateiformat oder Delimiter, sind als Klassen-Attribute beigefügt. Feldweite Metainformationen, wie die Reihenfolge, sind als Property-Attribute angemerkt. Zusätzlich können wahlweise Plausi-Prüfungen deklariert werden, wie ein Mindestwert oder erlaubte Werte, beigefügt werden. Um das Setzen der Attribute benutzerfreundlicher zu gestalten, befinden sich Pflichtangaben im Konstruktor und optionale Attribut-Parameter werden darauffolgend aufgeführt. Für Wahloptionen, wie für das Attribut fileFormat, ist es sinnhaft einen Enumerator zu erstellen.
Klassenauflösung über System.Reflection
Der Parser lernt die Mapping-Klasse erst zur Laufzeit kennen. Sie wird ihm als generischen Typ bei seiner Instanziierung übergeben.
Über den Namespace System.Reflection erhält er die Fähigkeit, die Klasseninformationen der zu konvertierenden Mapping-Klasse aufzulösen. Zu diesen Informationen gehören unter anderem die einzelnen Properties, deren Set-Methoden und vor alledem dessen Attribute. Das Jonglieren mit System.Reflection bietet ein dynamisches und flexibles verhalten, jedoch sind die damit verbundenen Operationen langsam und ineffizient. Als Musterfall nutzen wir eine CSV mit 10 Feldern und 5 Millionen Einträgen. Insgesamt wird die Set-Methode also 10 mal aufgelöst, jedoch 50 Millionen mal aufgerufen.
1) Direktes Setzen der Property
Der schnellste Ansatz ist das direkte Setzen der Property. Hierfür müsste der Parser und die gemappte Klasse hart verdrahtet sein um den set-Accessor anzusteuern. Wir verlieren den dynamischen Ansatz.
2) Ansteuern der PropertyInfo
Das Auflösen der PropertyInfo über System.Reflection und setzen der Properties über die SetValue-Methode ist ein enormer Zeitaufwand. Das muss besser gehen.
3) Ansteuern der MethodInfo
Das Objekt PropertyInfo enthält die MethodInfo der Set-Methode. Die MethodInfo kann durch .Invoke aufgerufen werden. Ein besseres Ergebnis, aber nicht zufriedenstellend. Das Problem ist, dass bei Methodenaufruf der MethodInfo sowie der PropertyInfo wieder und wieder die Route der System.Reflection genutzt wird.
Ein Ansatz ist die Nutzung von Delegaten. An Delegaten können Methoden gekapselt werden. Der Delegat ähnelt dabei einem Methoden-Pointer, der auf die Ziel-Methode verweist und dabei mehrfach ausgeführt werden kann ohne System.Reflection erneut anzusteuern. Eine Set-Methode ist eine anonyme Methode ohne Rückgabewert und benötigt daher den Action Delegaten. Die Eingangsparameter sind zum einen die Klasseninstanz (T) und der geparste Feldwert als object, da der Parser den tatsächlichen Werttyp erst zur Laufzeit kennen kann. Unser theoretisch und dynamisch einsetzbarer Delegat Action<T, object> funktioniert in der Praxis jedoch nicht. Die Set-Methode benötigt einen stark-typisierten Eingabeparameter, wie einen string oder int, das genutzte object ist jedoch schwach-typisiert. Das Resultat ist eine Exception.
Des Pudels Kern wäre den Parameter object zur Laufzeit zu bestimmen. Geht das?
Lösung 1: System.Linq.Expressions oder das Nutzen des Expression Trees
Der Namespace System.Linq.Expressions bietet die benötigte Funktionalität. Durch den Methodenaufruf Expression.Convert() wird der deklarierte Parameter object durch den Typ der Ziel-Property ersetzt. Das heißt für uns, dass deklarativ unsere Action<T, object> schwach-typisiert ist, doch object im Hintergrund den Typ der Property einnimmt. Im Anschluss lässt sich der Expression Tree nun mit unserem neuen Parameter und Methodenkörper zusammenbauen und kompilieren.
Unser gemessenes Ergebnis durch diese Anpassung:
Lösung 2: schwach-typisierten Delegaten an stark-typisierten Delegaten binden
Eine weitere Lösung, ist das binden eines schwach-typisierten Delegaten an einen stark-typisierten Delegaten. Übersetzt: ich binde beispielsweise die Action<T, object> an eine stark typisierte Action<T, string>.
Um in diesem Fall eine dynamische Auflösung zu erreichen, muss die aufrufende Methode die Parameter für TTarget (Klasseninstanz der Mapping-Klasse) und TParam (Typ der Property) übergeben. Um das zu erreichen, konvertiert der Caller die Methode per .MakeGenericMethod()-Aufruf zu einer generischen Methode, denn nur so können die Ziel-Parameter dynamisch injiziert werden.
Fazit
Der endgültige Vergleich zeigt keinen nennenswerten Unterschied zwischen den beiden Lösungsansätzen, jedoch eine enorme Leistungssteigerung zu den ungekapselten Set-Aufrufen.
Das war ein Ausflug in der Performance und Nutzung des Parsers. Der kommende Teil 3 wird sich mit dem Parsen von hierarchischen FlatFile-Strukturen befassen.
Dieser Beitrag kommt von Philipp, QUIBIQ Stuttgart.