
Heute werden wir darüber sprechen, wie Sie Ihren
AutoMapper schreiben. Ja, ich würde Ihnen gerne davon erzählen, aber ich kann nicht. Tatsache ist, dass solche Lösungen sehr umfangreich sind, eine lange Geschichte von Versuch und Irrtum haben und auch einen langen Weg zur Anwendung zurückgelegt haben. Ich kann nur ein Verständnis dafür geben, wie dies funktioniert, und einen Ausgangspunkt für diejenigen geben, die den Arbeitsmechanismus der „Mapper“ verstehen möchten. Man könnte sogar sagen, dass wir unser Fahrrad schreiben werden.
Haftungsausschluss
Ich erinnere Sie noch einmal daran: Wir werden einen primitiven Mapper schreiben. Wenn Sie sich plötzlich dazu entschließen, es zu ändern und im Produkt zu verwenden, tun Sie dies nicht. Nehmen Sie eine vorgefertigte Lösung, die den Stapel von Problemen in diesem Themenbereich
kennt und
bereits weiß, wie man sie löst. Es gibt mehrere mehr oder weniger wichtige Gründe, Ihren Bike Mapper zu schreiben und zu verwenden:
- Benötigen Sie eine spezielle Anpassung.
- Sie benötigen maximale Leistung unter Ihren Bedingungen und sind bereit, Kegel zu füllen.
- Sie möchten verstehen, wie Mapper funktioniert.
- Du radelst einfach gern.
Wie heißt das Wort "Mapper"?
Dies ist das Subsystem, das dafür verantwortlich ist, ein Objekt zu nehmen und es in ein anderes zu konvertieren (zu kopieren). Eine typische Aufgabe besteht darin, ein DTO in ein Business-Layer-Objekt zu konvertieren. Der primitivste Mapper "durchläuft" die Eigenschaften der Datenquelle und vergleicht sie mit den Eigenschaften des Datentyps, der ausgegeben wird. Nach dem Abgleich werden die Werte aus der Quelle extrahiert und in das Objekt geschrieben, was das Ergebnis der Konvertierung ist. Irgendwo auf dem Weg wird es höchstwahrscheinlich noch notwendig sein, genau dieses „Ergebnis“ zu erzielen.
Für den Verbraucher ist Mapper ein Dienst, der die folgende Schnittstelle bietet:
public interface IMapper<out TOut> { TOut Map(object source); }
Ich betone: Dies ist die primitivste Schnittstelle, die aus meiner Sicht zur Erklärung geeignet ist. In der Realität handelt es sich höchstwahrscheinlich um einen spezifischeren Mapper (IMapper <TIn, TOut>) oder um eine allgemeinere Fassade (IMapper), die selbst einen bestimmten Mapper für die angegebenen Arten von Eingabe-Ausgabe-Objekten auswählt.
Naive Umsetzung
Hinweis: Selbst die naive Implementierung von Mapper erfordert Grundkenntnisse in
Reflection und
ExpressionTrees . Wenn Sie den Links nicht gefolgt sind oder nichts über diese Technologien gehört haben, lesen Sie sie. Ich verspreche, dass die Welt niemals dieselbe sein wird.
Wir schreiben jedoch Ihren eigenen Mapper. Lassen Sie uns zunächst alle Eigenschaften (
PropertyInfo ) des Datentyps abrufen, der
ausgegeben werden soll (ich werde ihn
später TOut nennen). Dies ist ganz einfach: Wir kennen den Typ, da wir die Implementierung einer generischen Klasse schreiben, die mit dem TOut-Typ parametrisiert ist. Als Nächstes erhalten wir mithilfe einer Instanz der Type-Klasse alle ihre Eigenschaften.
Type outType = typeof(TOut); PropertyInfo[] outProperties = outType.GetProperties();
Beim Abrufen von Eigenschaften lasse ich die Funktionen weg. Zum Beispiel können einige von ihnen ohne Setter-Funktion sein, einige können als vom Attribut ignoriert markiert sein, einige können mit speziellem Zugriff sein. Wir erwägen die einfachste Option.
Wir gehen weiter. Es wäre schön, eine Instanz vom Typ TOut erstellen zu können, dh genau das Objekt, in das wir das eingehende Objekt "abbilden". In C # gibt es verschiedene Möglichkeiten, dies zu tun. Zum Beispiel können wir dies tun: System.Activator.CreateInstance (). Oder auch nur neues TOut (), aber dafür müssen Sie eine Einschränkung für TOut erstellen, die Sie in der verallgemeinerten Oberfläche nicht tun möchten. Wir wissen beide etwas über ExpressionTrees, was bedeutet, dass wir es so machen können:
ConstructorInfo outConstructor = outType.GetConstructor(Array.Empty<Type>()); Func<TOut> activator = outConstructor == null ? throw new Exception($"Default constructor for {outType.Name} not found") : Expression.Lambda<Func<TOut>>(Expression.New(outConstructor)).Compile();
Warum so? Da wir wissen, dass eine Instanz der Type-Klasse Informationen darüber liefern kann, über welche Konstruktoren sie verfügt, ist dies sehr praktisch für Fälle, in denen wir unseren Mapper so entwickeln, dass alle Daten an den Konstruktor übergeben werden. Außerdem haben wir etwas mehr über ExpressionTrees gelernt, nämlich, dass Plaque Code erstellen und kompilieren kann, der dann wiederverwendet werden kann. In diesem Fall handelt es sich um eine Funktion, die tatsächlich wie () => new TOut () aussieht.
Jetzt müssen Sie die Haupt-Mapper-Methode schreiben, die die Werte kopiert. Wir gehen den einfachsten Weg: Wir gehen die Eigenschaften des Objekts durch, das am Eingang zu uns gekommen ist, und suchen unter den Eigenschaften des ausgehenden Objekts nach den gleichnamigen Eigenschaften. Wenn gefunden - kopieren, wenn nicht - weitermachen.
TOut outInstance = _activator(); PropertyInfo[] sourceProperties = source.GetType().GetProperties(); for (var i = 0; i < sourceProperties.Length; i++) { PropertyInfo sourceProperty = sourceProperties[i]; string propertyName = sourceProperty.Name; if (_outProperties.TryGetValue(propertyName, out PropertyInfo outProperty)) { object sourceValue = sourceProperty.GetValue(source); outProperty.SetValue(outInstance, sourceValue); } } return outInstance;
Damit haben wir die
BasicMapper- Klasse vollständig gebildet. Hier können Sie sich mit seinen Tests vertraut machen. Bitte beachten Sie, dass die Quelle entweder ein Objekt eines bestimmten Typs oder ein anonymes Objekt sein kann.
Leistung und Boxen
Die Reflexion ist großartig, aber langsam. Darüber hinaus erhöht seine häufige Verwendung den Speicherverkehr, was bedeutet, dass der GC geladen wird, was bedeutet, dass die Anwendung noch mehr verlangsamt wird. Zum Beispiel haben wir nur die Methoden
PropertyInfo.SetValue und
PropertyInfo.GetValue verwendet. Die GetValue-Methode gibt ein Objekt zurück, in das ein bestimmter Wert eingeschlossen ist (Boxing). Dies bedeutet, dass wir eine Zuteilung von Grund auf erhalten haben.
Mapper befinden sich normalerweise dort, wo Sie ein Objekt in ein anderes verwandeln müssen ... Nein, nicht eines, sondern viele Objekte. Zum Beispiel, wenn wir etwas aus der Datenbank nehmen. An dieser Stelle möchte ich eine normale Leistung sehen und bei einer elementaren Operation nicht das Gedächtnis verlieren.
Was können wir tun?
ExpressionTrees wird uns wieder helfen. Tatsache ist, dass Sie mit .NET Code "on the fly" erstellen und kompilieren können: Wir beschreiben ihn in der Objektdarstellung, sagen, was und wo wir ihn verwenden werden ... und kompilieren ihn. Fast keine Magie.
Kompilierter Mapper
Tatsächlich ist alles relativ einfach: Wir haben Expression.New (ConstructorInfo) bereits neu gemacht. Sie haben wahrscheinlich bemerkt, dass die statische New-Methode genauso aufgerufen wird wie der Operator. Tatsache ist, dass fast die gesamte C # -Syntax in Form statischer Methoden der Expression-Klasse wiedergegeben wird. Wenn etwas fehlt, bedeutet dies, dass Sie nach dem sogenannten suchen "Syntaktischer Zucker."
Hier sind einige Operationen, die wir in unserem Mapper verwenden werden:
- Variablendeklaration - Expression.Variable (Typ, Zeichenfolge). Das Type-Argument gibt an, welcher Variablentyp erstellt wird, und string ist der Name der Variablen.
- Zuweisung - Expression.Assign (Ausdruck, Ausdruck). Das erste Argument ist das, was wir zuweisen, und das zweite Argument ist das, was wir zuweisen.
- Der Zugriff auf die Eigenschaft eines Objekts erfolgt über Expression.Property (Expression, PropertyInfo). Expression ist der Eigentümer der Eigenschaft, und PropertyInfo ist die Objektdarstellung der durch Reflection erhaltenen Eigenschaft.
Mit diesem Wissen können wir Variablen erstellen, auf Eigenschaften von Objekten zugreifen und Eigenschaften von Objekten Werte zuweisen. Höchstwahrscheinlich verstehen wir auch, dass ExpressionTree in einen Delegaten der Form
Func <Objekt, TOut> kompiliert werden muss . Der Plan lautet wie folgt: Wir erhalten eine Variable, die die Eingabedaten enthält, erstellen eine Instanz vom Typ TOut und erstellen Ausdrücke, die eine Eigenschaft einer anderen zuweisen.
Leider ist der Code nicht sehr kompakt, daher schlage ich vor, dass Sie sich sofort die Implementierung von
CompiledMapper ansehen. Ich habe hier nur wichtige Punkte gebracht.
Zunächst erstellen wir eine Objektdarstellung des Parameters unserer Funktion. Da ein Objekt als Eingabe verwendet wird, ist das Objekt ein Parameter.
var parameter = Expression.Parameter(typeof(object), "source");
Als Nächstes erstellen wir zwei Variablen und eine Ausdrucksliste, in die wir nacheinander Zuweisungsausdrücke einfügen. Die Reihenfolge ist wichtig, da die Befehle so ausgeführt werden, wenn wir die kompilierte Methode aufrufen. Beispielsweise können wir einer Variablen, die noch nicht deklariert wurde, keinen Wert zuweisen.
Ebenso wie bei einer naiven Implementierung gehen wir die Liste der Typeneigenschaften durch und versuchen, sie nach Namen abzugleichen. Anstatt jedoch sofort Werte zuzuweisen, erstellen wir Ausdrücke zum Extrahieren von Werten und zum Zuweisen von Werten für jede zugeordnete Eigenschaft.
Expression sourceValue = Expression.Property(sourceInstance, sourceProperty); Expression outValue = Expression.Property(outInstance, outProperty); expressions.Add(Expression.Assign(outValue, sourceValue));
Ein wichtiger Punkt: Nachdem wir alle Zuweisungsoperationen erstellt haben, müssen wir das Ergebnis von der Funktion zurückgeben. Zu diesem Zweck sollte der letzte Ausdruck in der Liste Ausdruck sein, der die Instanz der von uns erstellten Klasse enthält. Ich habe einen Kommentar neben dieser Zeile hinterlassen. Warum sieht das Verhalten, das dem Schlüsselwort return in ExpressionTree entspricht, so aus? Ich fürchte, dies ist ein separates Thema. Jetzt schlage ich vor, es ist leicht zu merken.
Nun, ganz am Ende müssen wir alle Ausdrücke kompilieren, die wir erstellt haben. Was interessiert uns hier? Die Body-Variable enthält den "Body" der Funktion. "Normale Funktionen" haben einen Körper, oder? Nun, die wir in geschweiften Klammern einschließen. Expression.Block ist genau das. Da geschweifte Klammern ebenfalls ein Bereich sind, müssen wir dort die Variablen übergeben, die dort verwendet werden - in unserem Fall sourceInstance und outInstance.
var body = Expression.Block(new[] {sourceInstance, outInstance}, expressions); return Expression.Lambda<Func<object, TOut>>(body, parameter).Compile();
Am Ausgang erhalten wir Func <Objekt, TOut>, d.h. Eine Funktion, die Daten von einem Objekt in ein anderes konvertieren kann. Warum solche Schwierigkeiten, fragst du? Ich erinnere Sie daran, dass wir erstens beim Kopieren von ValueType-Werten das Boxen vermeiden wollten und zweitens die Methoden PropertyInfo.GetValue und PropertyInfo.SetValue aufgeben wollten, da sie etwas langsam sind.
Warum nicht boxen? Da der kompilierte ExpressionTree eine echte IL ist und zur Laufzeit wie (fast) wie Ihr Code aussieht. Warum ist der "kompilierte Mapper" schneller? Nochmals: weil es einfach nur IL ist. Übrigens können wir die Geschwindigkeit mithilfe der
BenchmarkDotNet- Bibliothek leicht bestätigen, und der Benchmark selbst kann hier angezeigt
werden .
In der Ratio-Spalte zeigte "CompiledMapper" (CompiledMapper) ein sehr gutes Ergebnis, selbst im Vergleich zu AutoMapper (es ist die Basislinie, d. H. 1). Freuen wir uns jedoch nicht: AutoMapper verfügt im Vergleich zu unserem Fahrrad über deutlich größere Funktionen. Mit dieser Platte wollte ich nur zeigen, dass ExpressionTrees viel schneller ist als der „klassische Reflection-Ansatz“.
Zusammenfassung
Ich hoffe, ich konnte zeigen, dass das Schreiben Ihres Mappers recht einfach ist. Reflection und ExpressionTrees sind sehr leistungsstarke Tools, mit denen Entwickler viele verschiedene Aufgaben lösen können. Abhängigkeitsinjektion, Serialisierung / Deserialisierung, CRUD-Repositorys, Erstellen von SQL-Abfragen, Verwenden anderer Sprachen als Skripts für .NET-Anwendungen - all dies erfolgt mithilfe von Reflection, Reflection.Emit und ExpressionTrees.
Was ist mit Mapper? Mapper ist ein großartiges Beispiel, wo Sie all dies lernen können.
PS: Wenn Sie weitere ExpressionTrees möchten, empfehlen wir Ihnen zu lesen, wie Sie
Ihren JSON-Konverter mithilfe dieser Technologie erstellen können.