Interne DSL- und Ausdrucksbäume - Dynamische Erstellung von Funktionen zum Serialisieren, Kopieren, Klonen und Gleichstellen (Teil I)


Dieser Artikel konzentriert sich auf die doppelte Verwendung der Expression Trees- API - zum Parsen von Ausdrücken und zum Generieren von Code. Die Analyse von Ausdrücken hilft beim Aufbau von Präsentationsstrukturen (sie sind auch Präsentationsstrukturen der problemorientierten Sprache Internal DSL ), und die Codegenerierung ermöglicht es Ihnen, effektiv effektive Funktionen zu erstellen - Befehlssätze, die durch Präsentationsstrukturen festgelegt werden.


Ich werde die dynamische Erstellung von Eigenschaftsiteratoren demonstrieren : serialisieren, kopieren, klonen, gleich . Am Beispiel der Serialisierung zeige ich Ihnen, wie Sie die Serialisierung (im Vergleich zu Stream-Serialisierern) in der klassischen Situation optimieren können, in der "vorläufiges" Wissen zur Verbesserung der Leistung verwendet wird. Die Idee ist, dass beim Aufrufen des Streaming-Serializers immer die Funktion "Nicht-Streaming" verloren geht und genau bekannt ist, welche Baumknoten umgangen werden müssen. Gleichzeitig wird ein solcher Serializer "nicht von Hand", sondern dynamisch, sondern nach vordefinierten Bypass-Regeln erstellt. Das vorgeschlagene Inernal DSL löst das Problem einer kompakten Beschreibung der Regeln zum Durchlaufen von Baumstrukturen von Objekten anhand ihrer Eigenschaften / Eigenschaften (und im allgemeinen Fall: Durchlaufen des Berechnungsbaums mit dem Namen von Knoten) . Der Serializer-Benchmark ist bescheiden, aber es ist wichtig, dass er dem Ansatz, der auf der Verwendung eines bestimmten internen DSL-Includes (einem Dialekt von Include / ThenInclude von EF Core ) und der Verwendung von internem DSL als Ganzes basiert, die notwendige Überzeugungskraft verleiht.


Einführung


Vergleichen Sie:


var p = new Point(){X=-1,Y=1}; // which has better performance ? var json1 = JsonConvert.SerializeObject(p); var json2 = $"{{\"X\":{pX}, \"Y\":{pY}}}"; 

Die zweite Methode ist offensichtlich schneller (die Knoten sind bekannt und „in Code eingepfercht“), während die Methode natürlich komplizierter ist. Wenn Sie diesen Code jedoch als Funktion erhalten (dynamisch generiert und kompiliert), wird die Komplexität ausgeblendet (selbst was unklar wird, wird ausgeblendet
Wo ist die Reflexion und wo ist die Laufzeit der Codegenerierung?


 var p = new Point(){X=-1,Y=1}; // which has better performance ? var json1 = JsonConvert.SerializeObject(p); var formatter = JsonManager.ComposeFormatter<Point>(); var json2 = formatter(p); 

Hier ist JsonManager.ComposeFormatter das eigentliche Werkzeug . Die Regel, nach der die Strukturumgehung während der Serialisierung generiert wird, ist nicht offensichtlich, klingt jedoch so: "Mit den Standardparametern werden für benutzerdefinierte Werttypen alle Felder der ersten Ebene umgangen." Wenn Sie es explizit festlegen:


 //    var formatter2 = JsonManager.ComposeFormatter<Point>( chain=>chain .Include(e=>eX) .Include(e=>eY) // DSL Includes ) 

Dies ist die Beschreibung von Metadaten über DSL Includes. DSL hat die Analyse der Vor- und Nachteile der Beschreibung von Metadaten aufgeklärt. Da ich jedoch die Form der Aufzeichnung von Metadaten ignoriere, betone ich, dass C # die Möglichkeit bietet, den "idealen Serializer" mithilfe von Ausdrucksbäumen zu kompilieren und zu kompilieren.


Wie er es macht - viel Code und Anleitung zur Codegenerierung von Expression Trees ...

Übergang vom formatter zum serilizer (bisher ohne Ausdrucksbäume):


  Func<StringBuilder, Point, bool> serializer = ... // later string formatter(Point p) { var stringBuilder = new StringBuilder(); serializer(stringBuilder, p); return stringBuilder.ToString(); } 

Der serializer ist wiederum wie folgt aufgebaut (wenn er mit statischem Code festgelegt ist):


 Expression<Func<StringBuilder, Point, bool>> serializerExpression = SerializeAssociativeArray(sb, p, (sb1, t1) => SerializeValueProperty(sb1, t1, "X", o => oX, SerializeValueToString), (sb4, t4) => SerializeValueProperty(sb1, t1, "Y", o => oY, SerializeValueToString) ); Func<StringBuilder, Point, bool> serializer = serializerExpression.Compile(); 

Warum ist es so "funktional", warum können Sie nicht zwei Felder durch ein Semikolon serialisieren? Kurz gesagt: weil dieser Ausdruck einer Variablen vom Typ Expression<Func<StringBuilder, Box, bool>> zugewiesen werden kann, ein Semikolon jedoch nicht zulässig ist.
Warum konnte ich Func<StringBuilder, Point, bool> serializer = (sb,p)=>SerializeAssociativeArray(sb,p,... nicht direkt schreiben? Es ist möglich, aber ich demonstriere nicht das Erstellen eines Delegaten, sondern einer Assembly (in diesem Fall statischer Code). Ausdrucksbaum, mit einer Kompilierung für den Delegaten in der Zukunft, in der Praxis wird serializerExpression auf eine völlig andere Weise festgelegt - dynamisch (unten).


Was jedoch in der Lösung selbst wichtig ist: SerializeAssociativeArray akzeptiert ein Array von params Func<..> propertySerializers entsprechend der Anzahl der zu params Func<..> propertySerializers Knoten. Das Umgehen einiger von ihnen kann durch SerializeValueProperty-Serialisierer von Blättern (Akzeptieren des SerializeValueToString Formatierers) und andere wiederum durch SerializeAssociativeArray ( SerializeAssociativeArray Zweige) festgelegt werden, und somit wird ein Iterator (Baum) des Durchlaufs erstellt.


Wenn Point die NextPoint-Eigenschaft enthielt:


 var @delegate = SerializeAssociativeArray(sb, p, (sb1, t1) => SerializeValueProperty(sb1, t1, "X", o => oX, SerializeValueToString), (sb4, t4) => SerializeValueProperty(sb1, t1, "Y", o => oY, SerializeValueToString), (sb4, t4) => SerializeValueProperty(sb1, t1, "NextPoint", o => o.NextPoint, (sb4, t4) =>SerializeAssociativeArray(sb1, p1, (sb1, t1) => SerializeValueProperty(sb2, t2, "X", o => oX, SerializeValueToString), (sb4, t4) => SerializeValueProperty(sb2, t2, "Y", o => oY, SerializeValueToString) ) ) ); 

Das Gerät der drei Funktionen SerializeAssociativeArray , SerializeValueProperty , SerializeValueToString nicht kompliziert:


Serialisieren ...
 public static bool SerializeAssociativeArray<T>(StringBuilder stringBuilder, T t, params Func<StringBuilder, T, bool>[] propertySerializers) { var @value = false; stringBuilder.Append('{'); foreach (var propertySerializer in propertySerializers) { var notEmpty = propertySerializer(stringBuilder, t); if (notEmpty) { if (!@value) @value = true; stringBuilder.Append(','); } }; stringBuilder.Length--; if (@value) stringBuilder.Append('}'); return @value; } public static bool SerializeValueProperty<T, TProp>(StringBuilder stringBuilder, T t, string propertyName, Func<T, TProp> getter, Func<StringBuilder, TProp, bool> serializer) where TProp : struct { stringBuilder.Append('"').Append(propertyName).Append('"').Append(':'); var value = getter(t); var notEmpty = serializer(stringBuilder, value); if (!notEmpty) stringBuilder.Length -= (propertyName.Length + 3); return notEmpty; } public static bool SerializeValueToString<T>(StringBuilder stringBuilder, T t) where T : struct { stringBuilder.Append(t); return true; } 

Viele Details werden hier nicht angegeben (Listenunterstützung, Referenztyp und nullbar). Und doch ist es klar, dass ich wirklich json in der Ausgabe bekomme und alles andere noch mehr von den Standardfunktionen SerializeArray , SerializeNullable , SerializeRef .


Es war ein statischer Ausdrucksbaum, nicht dynamisch, nicht in C # ausgewertet .


Sie können sehen, wie der Ausdrucksbaum in zwei Schritten dynamisch erstellt wird:


Schritt 1 - Der Dekompiler überprüft den durch Expression<T> zugewiesenen Code



Dies wird Sie natürlich beim ersten Mal überraschen. Nichts ist klar, aber Sie können sehen, wie die ersten vier Zeilen so etwas zusammensetzen:


 ("sb","t") .. SerializeAssociativeArray.. 

Dann wird die Verbindung mit dem Quellcode erfasst. Und es sollte klar werden, dass Sie, wenn Sie einen solchen Datensatz beherrschen (indem Sie 'Expression.Const', 'Expression.Parameter', 'Expression.Call', 'Expression.Lambda' usw. kombinieren), wirklich dynamisch verknüpfen können - jede Umgehung von Knoten (basierend auf Metadaten). Dies ist in C # eval .


Schritt 2 - folgen Sie diesem Link ,


Der gleiche Dekompiler-Code, aber vom Menschen kompiliert.


Nur der Autor des Dolmetschers muss sich mit dieser Perlenstickerei befassen. Alle diese Künste verbleiben in der Serialisierungsbibliothek . Es ist wichtig zu lernen, dass Sie Bibliotheken bereitstellen können, die dynamisch kompilierte effiziente Funktionen in C # (und .NET Standard) generieren.


Ein Streaming-Serializer überholt jedoch eine dynamisch generierte Funktion, wenn Sie die Kompilierung jedes Mal vor der Serialisierung aufrufen (das Kompilieren im ComposeFormatter ist eine kostspielige Operation). Sie können den Link jedoch speichern und wiederverwenden:


 static Func<Point, string> formatter = JsonManager.ComposeFormatter<Point>(); public string Get(Point p){ // which has better performance ? var json1 = JsonConvert.SerializeObject(p); var json2 = formatter(p); return json2; } 

Wenn Sie einen Serializer vom anonymen Typ zur Wiederverwendung erstellen und speichern müssen, benötigen Sie zusätzliche Infrastruktur:


 static CachedFormatter cachedFormatter = new CachedFormatter(); public string Get(List<Point> list){ // there json formatter will be build only for first call // and assigned to cachedFormatter.Formatter // in all next calls cachedFormatter.Formatter will be used. // since building of formatter is determenistic it is lock free var json3 = list.Select(e=> {X:eX, Sum:e.X+EY}) .ToJson(cachedFormatter, e=>e.Sum); return json3; } 

Danach berücksichtigen wir zuversichtlich die erste Mikrooptimierung für uns selbst und akkumulieren, akkumulieren, akkumulieren ... Wer ist der Witz, wer nicht, aber bevor ich zu der Frage übergehe, dass der neue Serializer neu ist, behebe ich den offensichtlichen Vorteil - er wird schneller sein.


Was dafür?


Das DSL enthält Interpreter in Serilize (und auf die gleiche Weise, wie es in Iteratoren gleich ist, kopieren, klonen - und das wird auch ungefähr sein), erforderte die folgenden Kosten:


1 - Kosten der Infrastruktur zum Speichern von Links zu kompiliertem Code.


Diese Kosten sind im Allgemeinen nicht erforderlich, ebenso wie die Verwendung von Ausdrucksbäumen bei der Kompilierung. Der Interpreter kann einen Serialisierer für Reflexe erstellen und ihn sogar so weit lecken, dass er sich der Geschwindigkeit in Bezug auf Stream-Serialisierer (übrigens Kopieren, Klonen und) nähert Gleichheit wird weder durch Ausdrucksbäume gesammelt noch geleckt, es gibt keine solche Aufgabe, im Gegensatz zum Überholen von ServiceStack und Json.NET im Rahmen der wohlverstandenen Aufgabe der Optimierung der Serialisierung in json - eine notwendige Voraussetzung für die Präsentation einer neuen Lösung).


2 - Sie müssen Abstraktionslecks sowie ein ähnliches Problem berücksichtigen: Änderungen in der Semantik im Vergleich zu vorhandenen Lösungen.


Zum Beispiel benötigen Point und IEnumerable zwei verschiedene Serializer zum Serialisieren.

 var formatter1 = JsonManager.ComposeFormatter<Point>(); var formatter2 = JsonManager.ComposeEnumerableFormatter<Point>(); // but not // var formatter2 = JsonManager.ComposeEnumerableFormatter<List<Point>>(); 

Oder: "Funktioniert die Schließung?". Es funktioniert, nur der Knoten muss einen Namen (eindeutig) festlegen:


 string DATEFORMAT= "YYYY"; var formatter3 = JsonManager.ComposeFormatter<Record>( chain => chain .Include(i => i.RecordId) .Include(i => i.CreatedAt.ToString(DATEFORMAT) , "CreatedAt"); ); 

Dieses Verhalten wird ComposeFormatter vom internen Gerät des ComposeFormatter Interpreters ComposeFormatter .


Kosten dieser Art sind unvermeidlich böse. Darüber hinaus wird festgestellt, dass durch die Erweiterung der Funktionalität und den Umfang des internen DSL auch Abstraktionslecks zunehmen. Es wird sicherlich den Entwickler von internem DSL unterdrücken, hier müssen Sie sich mit einer philosophischen Stimmung eindecken.


Für den Benutzer werden Abstraktionslecks durch die Kenntnis der technischen Details von internem DSL ( was ist zu erwarten? ) Und des Funktionsumfangs eines bestimmten DSL und seiner Interpreter ( was im Gegenzug? ) Überwunden . Daher die Antwort auf die Frage: "Lohnt es sich, internes DSL zu erstellen und zu verwenden?" Es kann nur eine Geschichte über die Funktionalität eines bestimmten DSL geben - über all seine Details und Annehmlichkeiten und seine möglichen Anwendungen (Dolmetscher), d. H. eine Geschichte über die Überwindung von Kosten.


In diesem Sinne kehre ich zur Effektivität eines bestimmten DSL-Includes zurück.


Eine signifikant höhere Effizienz wird erreicht, wenn das Ziel darin besteht, das Tripel (DTO, Transformation in DTO, Serialisierung von DTO) durch eine lokal detaillierte und generierte Serialisierungsfunktion zu ersetzen. Am Ende des Dualismus-Funktionsobjekts können Sie "DTO ist eine solche Funktion" sagen und ein Ziel festlegen: lernen, wie eine DTO-Funktion festgelegt wird.


Die Serialisierung muss konfiguriert sein:


  1. Bypass-Baum (um die Knoten zu beschreiben, über die die Serialisierung stattfinden wird, um das Problem der kreisförmigen Verknüpfungen zu lösen), weisen Sie im Fall von Blättern einen Formatierer (nach Typ) zu.
  2. Die Regel für das Einschließen von Blättern (falls nicht angegeben) - Eigenschaft gegen Felder? schreibgeschützt?
  3. Um sowohl einen Zweig (einen Knoten mit Navigation) als auch ein Blatt angeben zu können, muss nicht nur MemberExpression ( e=>e.Name ), sondern im Allgemeinen eine beliebige Funktion (`e => e.Name.ToUpper ()," MyMemberName ") den Formatierer auf einen bestimmten Wert festlegen Knoten.

Weitere Möglichkeiten zur Erhöhung der Flexibilität:


  1. Serialisieren Sie ein Blatt, das eine JSON-Zeichenfolge "as is" enthält (spezieller Zeichenfolgenformatierer).
  2. setze Formatierer auf Gruppen, d.h. ganze Zweige, in diesem Zweig wie diesem - auf eine andere Weise anders (zum Beispiel hier mit der Zeit und in diesem ohne Zeit).

Überall können Konstruktionen wie Bypass-Baum, Zweig, Blatt und all dies mit DSL Includes geschrieben werden.


DSL beinhaltet


Da jeder mit EF Core vertraut ist, sollte die Bedeutung der folgenden Ausdrücke sofort erfasst werden (dies ist eine Teilmenge von xpath).


  // DSL Includes Include<User> include1 = chain=> chain .IncludeAll(e => e.Groups) .IncludeAll(e => e.Roles) .ThenIncludeAll(e => e.Privileges) // EF Core syntax // https://docs.microsoft.com/en-us/ef/core/querying/related-data var users = context.Users .Include(blog => blog.Groups) .Include(blog => blog.Roles) .ThenInclude(blog => blog.Privileges); 

Hier sind die Knoten "mit Navigation" - "Zweige".
Die Antwort auf die Frage, welche Knoten "Blätter" (Felder / Eigenschaften) in dem so definierten Baum enthalten sind - keine. Um Blätter einzuschließen, müssen Sie sie entweder explizit auflisten:


 Include<User> include2 = chain=> chain .Include(e => e.UserName) // leaf member .IncludeAll(e => e.Groups) .ThenInclude(e => e.GroupName) // leaf member .IncludeAll(e => e.Roles) .ThenInclude(e => e.RoleName) // leaf member .IncludeAll(e => e.Roles) .ThenIncludeAll(e => e.Privileges) .ThenInclude(e => e.PrivilegeName) // leaf member 

Oder fügen Sie dynamisch nach der Regel über einen spezialisierten Interpreter hinzu:


 // Func<ChainNode, MemberInfo> rule = ... var include2 = IncludeExtensions.AppendLeafs(include1, rule); 

Hier ist die Regel eine Regel, die von ChainNode.Type ausgewählt werden kann, d.h. nach Art des vom Knoten zurückgegebenen Ausdrucks (ChainNode - interne Darstellung von DSL Includes, auf die später noch eingegangen wird) Eigenschaften (MemberInfo) für die Teilnahme an der Serialisierung, z. Nur eine Eigenschaft oder nur eine Lese- / Schreibeigenschaft oder nur diejenigen, für die es einen Formatierer gibt, können Sie aus einer Liste von Typen auswählen, und sogar der Include-Ausdruck selbst kann eine Regel festlegen (wenn er Blattknoten auflistet - d. h. die Form der Baumverknüpfung). .


Oder ... überlassen Sie es dem Benutzerinterpreter, der entscheidet, was mit den Knoten geschehen soll. DSL Includes ist nur ein Metadatensatz. Wie dieser Datensatz interpretiert wird, hängt vom Interpreter ab. Er kann die Metadaten so interpretieren, wie er möchte, bis er sie ignoriert. Einige Interpreter führen die Aktion selbst aus, während andere eine Funktion erstellen, die bereit ist, sie auszuführen (über Expression Tree oder sogar Reflection.Emit). Ein gutes internes DSL ist für den universellen Gebrauch und die Existenz vieler Dolmetscher konzipiert, von denen jeder seine eigenen Besonderheiten und Abstraktionslecks aufweist.
Code, der internes DSL verwendet, kann sich stark von dem unterscheiden, was er zuvor war.


Out of the Box


Integration mit EF Core.
Die laufende Aufgabe besteht darin, "zyklische Links abzubrechen", um nur das zu starten, was im Include-Ausdruck angegeben ist:


 static CachedFormatter cachedFormatter1 = new CachedFormatter(); string GetJson() { using (var dbContext = GetEfCoreContext()) { string json = EfCoreExtensions.ToJsonEf<User>(cachedFormatter1, dbContext, chain=>chain .IncludeAll(e => e.Roles) .ThenIncludeAll(e => e.Privileges)); } } 

ToJsonEf akzeptiert die Navigationssequenz, verwendet sie beim Serialisieren (wählt Blätter nach der Regel "Standard für EF Core" aus, dh öffentliche Lese- / Schreibeigenschaft), interessiert sich für das Modell - wobei string / json Feldformatierer verwendet, um sie unverändert einzufügen Standardmäßig (Byte [] pro Zeichenfolge, Datum / Uhrzeit in ISO usw.). Daher muss er IQuaryable unter sich ausführen.


In dem Fall, in dem das Ergebnis transformiert wird, ändern sich die Regeln - es ist nicht erforderlich, DSL Includes zu verwenden, um die Navigation anzugeben (wenn die Regel nicht wiederverwendet wird), ein anderer Interpreter wird verwendet und die Konfiguration erfolgt lokal:


 static CachedFormatter cachedFormatter1 = new CachedFormatter(); string GetJson() { using (var dbContext = GetEfCoreContext()) { var json = dbContext.ParentRecords // back to EF core includes // but .Include(include1) also possible .IncludeAll(e => e.Roles) .ThenIncludeAll(e => e.Privileges) .Select(e => new { FieldA: e.FieldA, FieldJson:"[1,2,3]", Role: e.Roles().First() }) .ToJson(cachedFormatter1, chain => chain.Include(e => e.Role), LeafRuleManager.DefaultEfCore, config: rules => rules .AddRule<string[]>(GetStringArrayFormatter) .SubTree( chain => chain.Include(e => e.FieldJson), stringAsJsonLiteral: true) // json as is .SubTree( chain => chain.Include(e => e.Role), subRules => subRules .AddRule<DateTime>( dateTimeFormat: "YYYMMDD", floatingPointFormat: "N2" ) ), ), useToString: false, // no default ToString for unknown leaf type (throw exception) dateTimeFormat: "YYMMDD", floatingPointFormat: "N2" } } 

Es ist klar, dass all diese Details, all dies ist „standardmäßig“, nur dann berücksichtigt werden können, wenn Sie sie wirklich brauchen und / oder wenn dies Ihr eigener Dolmetscher ist. Auf der anderen Seite kehren wir noch einmal zu den Pluspunkten zurück: DTO wird nicht durch Code verschmiert, wird durch eine bestimmte Funktion spezifiziert, Interpreter sind universell. Der Code wird kleiner - das ist gut.


Es ist zu warnen : Obwohl es den Anschein hat, dass vorläufiges Wissen in ASP immer verfügbar ist und ein Streaming-Serializer in der Welt des Webs, in der sogar Datenbanken Daten in JSON übertragen, nicht unbedingt erforderlich ist, ist die Verwendung von DSL Includes in ASP MVC nicht die einfachste Geschichte . Wie man funktionale Programmierung mit ASP MVC kombiniert, verdient eine separate Studie.


In diesem Artikel beschränke ich mich auf die Feinheiten von DSL Includes. Ich werde sowohl neue Funktionen als auch das Durchsickern von Abstraktionen zeigen, um zu zeigen, dass das Problem der Analyse von "Kosten und Akquisitionen" tatsächlich erschöpfbar ist.


Mehr DSL beinhaltet


 Include<Point> include = chain => chain.Include(e=>eX).Include(e=>eY); 

Dies unterscheidet sich von EF Core Includes, die auf statischen Funktionen basieren, die nicht Variablen zugewiesen und als Parameter übergeben werden können. DSL Includes selbst entstand aus der Notwendigkeit heraus, "include" in meine Implementierung der Repository-Vorlage zu übergeben, ohne die Typinformationen zu verschlechtern, die bei der Standardisierung in Zeichenfolgen aufgetreten wären.


Der dramatischste Unterschied besteht immer noch in der Ernennung. EF Core Includes - Einbeziehung von Navigationseigenschaften (Knoten von Zweigen), DSL Includes - Aufzeichnung der Durchquerung eines Berechnungsbaums, wobei dem Ergebnis jeder Berechnung ein Name (Pfad) zugewiesen wird.


Die interne Darstellung von EF Core Includes ist eine Liste von Zeichenfolgen, die von MemberExpression.Member empfangen wurden (Der durch e=>User.Name angegebene e=>User.Name kann nur [MemberExpression] sein ( https://msdn.microsoft.com/en-us/library/system.linq.expressions). Mitgliedsausdruck (v = vs. 110) .aspx und in internen Ansichten wird nur die Namenszeile gespeichert).


In DSL Includes besteht die interne Darstellung aus den Klassen ChainNode und ChainMemberNode , die den gesamten Ausdruck (z. B. e=>User.Name ) e=>User.Name , der so wie er ist in den Ausdrucksbaum integriert ist. Genau aus diesem Grund unterstützt DSL Includes sowohl Felder als auch Benutzerwerttypen und Funktionsaufrufe:


Ausführung von Funktionen:


 Include<User> include = chain => chain .Include(i => i.UserName) .Include(i => i.Email.ToUpper(),"EAddress"); 

Was damit zu tun ist, hängt vom Dolmetscher ab. CreateFormatter- gibt {"UserName": "John", "EAddress": "JOHN@MAIL.COM"} zurück


Die Ausführung kann auch nützlich sein, um das Durchlaufen von nullbaren Strukturen festzulegen.


 Include<StrangePointF> include = chain => chain .Include(e => e.NextPoint) // NextPoint is nullable struct .ThenIncluding(e => e.Value.X) .ThenInclude(e => e.Value.Y); // but not this way (abstraction leak) // Include<StrangePointF> include // = chain => chain // now this can throw an exception // .Include(e => e.NextPoint.Value) // .ThenIncluding(e => eX) // .ThenInclude(e => eY); 

DSL Includes enthält auch einen kurzen Eintrag für die mehrstufige ThenIncluding-Problemumgehung.


 Include<User> include = chain => chain .Include(i => i.UserName) .IncludeAll(i => i.Groups) // ING-form - doesn't change current node .ThenIncluding(e => e.GroupName) // leaf .ThenIncluding(e => e.GroupDescription) // leaf .ThenInclude(e => e.AdGroup); // leaf 

vergleiche mit


 Include<User> include = chain => chain .Include(i => i.UserName) .IncludeAll(i => i.Groups) .ThenInclude(e => e.GroupName) .IncludeAll(i => i.Groups) .ThenInclude(e => e.GroupDescription) .IncludeAll(i => i.Groups) .ThenInclude(e => e.AdGroup); 

Und auch hier gibt es ein Abstraktionsleck. Wenn ich die Navigation in dieser Form notiert habe, sollte ich wissen, wie ein Interpreter funktioniert, der QuaryableExtensions aufruft. Und er übersetzt die Aufrufe in Include und ThenInclude in Include "string". Was kann wichtig sein (Sie müssen bedenken).


Algebra Include-Ausdrücke .


Einschlussausdrücke können sein:


Vergleichen
 var b1 = InlcudeExtensions.IsEqualTo(include1, include2); var b2 = InlcudeExtensions.IsSubTreeOf(include1, include2); var b3 = InlcudeExtensions.IsSuperTreeOf(include1, include2); 

Klon
 var include2 = InlcudeExtensions.Clone(include1); 

Zusammenführen
 var include3 = InlcudeExtensions.Merge(include1, include2); 

In XPath-Listen konvertieren - Alle Pfade zu Blättern
 IReadOnlyCollection<string> paths1 = InlcudeExtensions.ListLeafXPaths(include); // as xpaths IReadOnlyCollection<string[]> paths2 = InlcudeExtensions.ListLeafKeyPaths(include); // as string[] 

usw.


Die gute Nachricht ist: Es gibt keine Abstraktionslecks, hier wird die Ebene der reinen Abstraktion erreicht. Es gibt Metadaten und arbeiten mit Metadaten.


Dialektik


Mit DSL Includes können Sie eine neue Abstraktionsebene erreichen. Zum Zeitpunkt des Erreichens besteht jedoch die Notwendigkeit, zur nächsten Ebene zu gelangen: Include-Ausdrücke selbst generieren.


In diesem Fall ist es nicht erforderlich, DSL als fließende Kette zu generieren. Sie müssen lediglich interne Repräsentationsstrukturen erstellen.


 var root = new ChainNode(typeof(Point)); var child = new ChainPropertyNode( typeof(int), expression: typeof(Point).CreatePropertyLambda("X"), memberName:"X", isEnumerable:false, parent:root ); root.Children.Add("X", child); // or there is number of extension methods eg: var child = root.AddChild("X"); Include<Point> include = ChainNodeExtensions.ComposeInclude<Point>(root); 

Sie können Präsentationsstrukturen auch an Dolmetscher übergeben. Warum enthält die fließende DSL-Aufzeichnung dann überhaupt? Dies ist eine rein spekulative Frage, deren Antwort darin besteht, dass es in der Praxis nur möglich ist, die interne Repräsentation (und auch die Entwicklung) zusammen mit der Entwicklung von DSL zu entwickeln (d. H. Eine kurze Ausdrucksaufzeichnung, die für statischen Code geeignet ist). Dies wird noch einmal näher an der Schlussfolgerung gesagt.


Kopieren, Klonen, Gleich


All dies gilt für Interpreter-Ausdrucksinterpreter, die Copy , Clone und Iteratoren implementieren.


Gleich

Vergleich nur auf Blättern aus dem Include-Ausdruck.
Verstecktes semantisches Problem: Auswerten oder nicht in der Liste sortieren


 Include<User> include = chain=>chain.Include(e=>e.UserId).IncludeAll(e=>e.Groups).ThenInclude(e=>e.GroupId) bool b1 = ObjectExtensions.Equals(user1, user2, include); bool b2 = ObjectExtensions.EqualsAll(userList1, userList2, include); 

Klon

Übergeben Sie Ausdrucksknoten. Eigenschaften, die der Regel entsprechen, werden kopiert.


 Include<User> include = chain=>chain.Include(e=>e.UserId).IncludeAll(e=>e.Groups).ThenInclude(e=>e.GroupId) var newUser = ObjectExtensions.Clone(user1, include, leafRule1); var newUserList = ObjectExtensions.CloneAll(userList1, leafRule1); 

Möglicherweise gibt es einen Dolmetscher, der Blatt aus Includes auswählt. Warum wird es gemacht - durch eine separate Regel? Was war ähnlich der Semantik von ObjectExtensions.Copy


Kopieren

Durch Knoten gehen - ein Ausdruckszweig und die Identifizierung durch Blattknoten. Eigenschaften, die der Regel entsprechen, werden kopiert (ähnlich wie beim Klonen).


 Include<User> include = chain=>chain.IncludeAll(e=>e.Groups); ObjectExtensions.Copy(user1, user2, include, supportedLeafsRule); ObjectExtensions.CopyAll(userList1, userList2, include, supportedLeafsRule); 

Möglicherweise gibt es einen Interpreter, der Blatt aus Includes auswählt. Warum wird es gemacht - durch eine separate Regel? ObjectExtensions.Copy ( — include , supportedLeafsRule — ).


copy / clone :


  1. readonly , Tuple<,> Anonymous Type. , .
  2. (. IEnumerable ) — public .
  3. expression include-, — .
  4. " " .

DSL , .. . , Tuple<,> , .. c readonly , ValueTuple<,> c writabale ( ).


, ( Expression Trees) Includes — . Include DSL .


Detach, FindDifferences ..


run-time, .cs ?


.cs , , run-time :


  1. ( , , source control).
  2. , , , — .
  3. .
  4. " ". dev time , : "" "" , "" , , "" .

Roslyn', . Typescript ( DTO , .. ) — DSL Includes Roslyn' ( ) — typescript ( ). " " " " .cs ( Expression Trees).


: run time — , . ( Expression Trees).


Expression Trees


Internal DSL Expression Tree :


  1. LambdaExpression.Compile Lambda . , . , "" expression tree, CallExpression — LambdaExpression, (. LambdaExpression) ConstantExpression. , " /" — , Expression Trees.


  2. ssmbly , ( 10 ) ( assembly , — ). , , , — .



, ( ), , . : . — — .cs .



— 600 15 . JSON.NET, ServiceStack reflection' GetProperties().


dslComposeFormatter — ComposeFormatter , .


BenchmarkDotNet =v0.10.14, OS=Windows 10.0.17134
Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=2.1.300


MethodMeanFehlerStdDevMinMaxMedianAllocated
dslComposeFormatter2.208 ms0.0093 ms0.0078 ms2.193 ms2.220 ms2.211 ms849.47 KB
JsonNet_Default2.902 ms0.0160 ms0.0150 ms2.883 ms2.934 ms2.899 ms658.63 KB
JsonNet_NullIgnore2.944 ms0.0089 ms0.0079 ms2.932 ms2.960 ms2.942 ms564.97 KB
JsonNet_DateFormatFF3.480 ms0.0121 ms0.0113 ms3.458 ms3.497 ms3.479 ms757.41 KB
JsonNet_DateFormatSS3.880 ms0.0139 ms0.0130 ms3.854 ms3.899 ms3.877 ms785.53 KB
ServiceStack_SerializeToString4.225 ms0.0120 ms0.0106 ms4.201 ms4.243 ms4.226 ms805.13 KB
fake_expressionManuallyConstruted54.396 ms0.1758 ms0.1644 ms54.104 ms54.629 ms54.383 ms7401.58 KB

fake_expressionManuallyConstruted — expression ( ).



DSL : DSL ; Internal DSL run-time .


Expression Tree .NET Standard .


Expression Trees Internal DSL Fluent API. # .


fluent ( Expression Trees), Internal DSL # fluent, "" Expression Trees.


Expression Trees DSL Includes ( , ), / run-time — (run-time ).


Internal DSL : - serialize , copy , clone , equals "" . , " ", . : includes ( ) , ( , ).


Fazit


DSL Includes DTO — ( json). , , , " ", . = .


Internal DSL , DSL, Internal DSL ( Expression) ( Expression Tree).


DSL Includes json ComposeFormatter DashboardCodes.Routines nuget GitHub.

Source: https://habr.com/ru/post/de419759/


All Articles