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


    Der Artikel befasst sich mit der doppelten Verwendung der Expression Trees- API - zum Analysieren von Ausdrücken und zum Generieren von Code. Das Analysieren von Ausdrücken hilft beim Aufbau von Repräsentationsstrukturen (sie sind auch die Repräsentationsstrukturen der problemorientierten Sprache " Internes DSL" ). Mit der Codegenerierung können Sie effiziente Funktionen dynamisch erstellen - Befehlssätze, die durch Repräsentationsstrukturen definiert werden.


    Ich werde die dynamische Erstellung von Property-Iteratoren demonstrieren : serialisieren, kopieren, klonen, gleich . Am Beispiel der Serialisierung zeige ich, wie Sie die Serialisierung (im Vergleich zu Stream-Serialisierern) in einer klassischen Situation optimieren können, in der Vorkenntnisse zur Verbesserung der Leistung verwendet werden. Die Idee ist, dass der Aufruf eines Streaming-Serialisierers immer die "Nicht-Streaming" -Funktion verliert, die genau weiß, welche Knoten in der Baumstruktur umgangen werden müssen. Gleichzeitig wird ein solcher Serializer nicht "von Hand" erstellt, sondern dynamisch, sondern nach vorgegebenen Bypass-Regeln. Das vorgeschlagene Inernal DSL löst das Problem einer kompakten Beschreibung der Regeln für das Durchlaufen von Baumstrukturen von Objekten anhand ihrer Eigenschaften / Eigenschaften (und im Allgemeinen durch Umgehen des Berechnungsbaums mit Namensknoten).. Der Benchmark des Serialisierers ist bescheiden, er ist jedoch insofern wichtig, als er einen Ansatz hinzufügt, der auf der Verwendung bestimmter Internal DSL Includes (dem Dialekt des Include / ThenInclude von EF Core ) und der Anwendung von Internal DSL als Ganzes basiert , die notwendige Überzeugungskraft.


    Einleitung


    Vergleichen Sie:


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

    Die zweite Methode ist offensichtlich schneller (die Knoten sind bekannt und "im Code verstopft"), während die Methode natürlich komplizierter ist. Wenn Sie diesen Code jedoch als Funktion erhalten (dynamisch generiert und kompiliert), ist die Komplexität verborgen (sie verdeckt sogar das, was unklar ist,
    wo sich die Reflektion befindet und wo die Laufzeitcode-Generierung erfolgt).


    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 JsonManager.ComposeFormatterist ein echtes Werkzeug . Die Regel, nach der die Strukturdurchquerung während der Serialisierung generiert wird, ist nicht offensichtlich, klingt jedoch wie folgt: "Mit den Standardparametern gehen Sie alle Felder der ersten Ebene für den benutzerdefinierten Werttyp durch". Wenn es explizit gesetzt ist:


    // обход задан явноvar formatter2 = JsonManager.ComposeFormatter<Point>(
       chain=>chain   
          .Include(e=>e.X)
          .Include(e=>e.Y)  // DSL Includes
    )

    Dies ist die Metadatenbeschreibung von DSL Includes. Die Analyse der Vor- und Nachteile von Metadatenbeschreibungen durch DSL ist aufgeklärt, aber jetzt, da ich das Metadateneingabeformular ignoriere, betone ich, dass C # die Möglichkeit bietet, den „perfekten Serialisierer“ mithilfe von Ausdrucksbäumen zu kompilieren und zu kompilieren.


    Wie er das macht - viel Code und eine Anleitung zur Code-Generierung von Expression Trees ...

    Übergang von formatternach serilizer(vorerst ohne Ausdrucksbäume):


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

    В свою очередь serializer строится такой (если задавать статическим кодом):


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

    Зачем так "функционально", почему нельзя задать сериализацию двух полей через "точку с запятой"? Коротко: потому что вот это выражение можно присвоить переменной типа Expression<Func<StringBuilder, Box, bool>>, а "точку с запятой" нельзя.
    Почему нельзя было прямо написать Func<StringBuilder, Point, bool> serializer = (sb,p)=>SerializeAssociativeArray(sb,p,...? Можно, но я демонстрирую не создание делегата, а сборку (в данном случае статическим кодом) expression tree, с полседующей компиляцией в делегат, в практическом использовании serializerExpression будут задаваться уже совсем по другому — динамически (ниже).


    Но что важно в самом решении: SerializeAssociativeArray принимает массив params Func<..> propertySerializers по числу узлов которые надо обойти. Обход одних из них может быть задан сериалайзерами "листьев" SerializeValueProperty(принимающим форматер SerializeValueToString), а других опять SerializeAssociativeArray (т.е. веток) и таким образом строится итератор (дерево) обхода.


    Если бы Point содержал свойство NextPoint:


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

    Устройство трех функций SerializeAssociativeArray, SerializeValueProperty, SerializeValueToString не сложное:


    Serialize...
    publicstaticbool 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;
            }
            publicstaticbool 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(':');
                varvalue = getter(t);
                var notEmpty = serializer(stringBuilder, value);
                if (!notEmpty)
                    stringBuilder.Length -= (propertyName.Length + 3);
                return notEmpty;
            }
            publicstaticbool SerializeValueToString<T>(StringBuilder stringBuilder, T t) where T : struct
            {
                stringBuilder.Append(t);
                returntrue;
            }

    Многие детали тут не приведены (поддержка списков, ссылочного типа и nullable). И все же видно, что я действительно получу json на выходе, а все остальное это еще больше типовых функций SerializeArray, SerializeNullable, SerializeRef.


    Это было статическое Expression Tree, не динамиеческое, не eval в C#.


    Увидеть как Expression Tree строится динамически можно в два шага:


    Шаг 1 — decompiler'ом посмотреть на код присвоенный Expression<T>



    Это конечно удивит по первому разу. Ничего не понятно но можно заметить как четырьмя первыми строчками скомпоновано что-то вроде:


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

    Тогда связь с исходным кодом улавливается. И должно стать понятно что если освоить такую запись (комбинируя 'Expression.Const', 'Expression.Parameter', 'Expression.Call', 'Expression.Lambda' etc ...) можно действительно компоновать динамически — любой обход узлов (исходя из метаданных). Это и есть eval в С#.


    Шаг 2 — сходить по этой ссылке,


    Тот же код декомпилера, но составленный человеком.


    Втягиваться в это вышивание бисером обязательно только автору интерпретатора. Все эти художества остаются внутри библиотеки сериализации. Важно усвоить идею, что можно предоставлять библиотеки динамически генерирующие скомпилированные эффективные функции в С# (и .NET Standard).


    Der Stream-Serialisierer überholt jedoch die dynamisch generierte Funktion, wenn die Kompilierung jedes Mal vor der Serialisierung aufgerufen wird (die Kompilierung ComposeFormatterist ein kostspieliger Vorgang). Sie können die Verknüpfung jedoch speichern und erneut verwenden:


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

    Wenn Sie jedoch einen Serializer mit anonymen Typen für die Wiederverwendung erstellen und speichern müssen, ist eine zusätzliche Infrastruktur erforderlich:


    static CachedFormatter cachedFormatter  = new CachedFormatter();
    publicstringGet(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:e.X, Sum:e.X+E.Y})
                             .ToJson(cachedFormatter, e=>e.Sum); 
       return json3;
    } 

    Danach zählen wir zuversichtlich die erste Mikrooptimierung für uns und sammeln, akkumulieren, akkumulieren ... Wem gehört der Witz, der es nicht tut, aber bevor wir uns der Frage zuwenden, dass der neue Serializer etwas Neues kann - den offensichtlichen Vorteil beheben - es wird schneller sein.


    Was dafür?


    Der DSL Includes-Interpreter in serilize (und auf gleiche Weise ist es bei Iteratoren gleich, Kopieren, Klonen - und dies wird auch der Fall sein) - erforderte folgende Kosten:


    1 - Kosten der Infrastruktur für das Speichern von Verweisen auf den kompilierten Code.


    Diese Kosten sind im Allgemeinen nicht erforderlich, ebenso wie die Verwendung von Expression Trees bei der Kompilierung. Der Interpreter kann einen Serializer für Reflexe erstellen und ihn sogar so sehr lecken, dass er sich dem Streaming von Serialisierern annähert (übrigens am Ende der Artikelkopie, des Klons und des Clones) entspricht und sammelt sich nicht durch Ausdrucksbäume, und sie leckten nicht, es gibt keine solche Aufgabe, im Gegensatz zu "Überholen" von ServiceStack und Json.NET im Rahmen des universell verstandenen Json-Optimierungsproblems (eine notwendige Voraussetzung für die Vorstellung einer neuen Lösung).


    2 - Sie müssen Abstraktionen in Ihrem Kopf undicht halten und ein ähnliches Problem haben: Änderungen in der Semantik im Vergleich zu vorhandenen Lösungen.


    Beispielsweise benötigen Point und IEnumerable zwei verschiedene Serialisierer, um die Serialisierung durchzuführen.

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

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


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

    Dieses Verhalten wird vom internen Gerät speziell für den Interpreter vorgegeben ComposeFormatter.


    Die Kosten dieser Art sind unvermeidlich übel. Darüber hinaus wird festgestellt, dass durch die Erhöhung der Funktionalität und die Erweiterung des Umfangs von Internal DSL Abstraktionslecks zunehmen. Der Entwickler des Internal DSL wird natürlich unterdrückt, hier muss man sich eine philosophische Stimmung zulegen.


    Für einen Benutzer werden Abstraktionslecks überwunden, indem die technischen Details des internen DSL ( was zu erwarten ist ) und die Funktionsvielfalt eines bestimmten DSL und seiner Interpreter ( was im Gegenzug? ) Bekannt ist. Daher die Antwort auf die Frage: "Soll ich Internes DSL erstellen und verwenden?". Kann nur eine Geschichte über die Funktionalität eines bestimmten DSL sein - über all seine kleinen Dinge und Annehmlichkeiten und Anwendungsmöglichkeiten (Interpreter), d. H. Geschichte über die Überwindung von Kosten.


    Vor diesem Hintergrund komme ich zur Wirksamkeit eines bestimmten DSL Includes zurück.


    Eine wesentlich höhere Effizienz wird erreicht, wenn der Ersatz eines Triple (DTO, Umwandlung in DTO, Serialisierung von DTO) an Stelle einer detaillierten angewiesenen und generierten Serialisierungsfunktion erfolgt. Am Ende des Objekts können Sie durch den Dualismus des Objektobjekts "DTO ist eine solche Funktion" angeben und ein Ziel festlegen: Lernen Sie, wie Sie die DTO-Funktion definieren.


    Die Serialisierung muss konfiguriert sein:


    1. Bypass-Baum (Beschreiben Sie die Knoten, an denen die Serialisierung durchgeführt wird, indem Sie das Problem der zirkularen Verknüpfungen lösen). Im Falle von Blättern - weisen Sie das Formatierungsprogramm (nach Typ) zu.
    2. Blatteinschlussregel (wenn nicht angegeben) - Eigenschaft vs. Felder? Readonly?
    3. Um sowohl einen Zweig (einen Knoten mit Navigation) als auch ein Blatt festlegen zu können, nicht nur MemberExpression ( e=>e.Name), sondern generell durch eine beliebige Funktion (`e => e.Name.ToUpper ()," MyMemberName ") - setzen Sie das Formatierungsprogramm auf einen bestimmten Knoten.

    Andere Funktionen, die die Flexibilität erhöhen:


    1. das Blatt mit der Zeile json "as is" (spezieller Formatierer von Strings) serialisieren;
    2. Formatierer auf Gruppen einstellen, d.h. ganze Zweige, in diesem Zweig also - in einem anderen anders (zum Beispiel gibt es Datumsangaben mit der Zeit und in diesem ohne Zeitangabe).

    Überall, wo solche Konstruktionen beteiligt sind: Baum, Zweig, Blatt und alles kann mit DSL Includes geschrieben werden.


    DSL beinhaltet


    Da jeder mit EF Core vertraut ist, sollte die Bedeutung nachfolgender Ausdrücke sofort erfasst werden (dies ist eine Xpath-Teilmenge).


    // 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-datavar users = context.Users
       .Include(blog => blog.Groups)
       .Include(blog => blog.Roles)
          .ThenInclude(blog => blog.Privileges);

    Hier sind die Knoten "mit Navigation" - "Äste".
    Die Antwort auf die Frage, welche Knoten "Blätter" (Felder / Eigenschaften) in dem so angegebenen Baum enthalten, ist keine. Um die 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 gemäß der Regel durch einen spezialisierten Interpreter hinzu:


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

    Hier ist die Regel eine Regel, die mit ChainNode ausgewählt werden kann durch den Typ des vom Knoten zurückgegebenen Ausdrucks (ChainNode ist eine interne Darstellung von DSL Includes, was sonst noch gesagt wird) Eigenschaften (MemberInfo) für die Teilnahme an der Serialisierung, zum Beispiel. Nur die Eigenschaft, oder nur die Lese- / Schreibeigenschaft, oder nur die, für die ein Formatierer vorhanden ist, kann über die Liste der Typen ausgewählt werden, und sogar der Include-Ausdruck selbst kann eine Regel angeben (wenn Blattknoten aufgelistet werden, dh die Form der Baumzuordnung). .


    Oder ... es liegt im Ermessen des Benutzerinterpreters, der entscheidet, was mit den Knoten zu tun ist. Bei DSL Includes handelt es sich lediglich um einen Metadateneintrag. Wie dieser Eintrag zu interpretieren ist, hängt vom Interpreter ab. Er kann die Metadaten beliebig interpretieren, bis er ignoriert wird. Einige Interpreter führen die Aktion selbst aus, andere bauen eine Funktion auf, die zur Ausführung bereit ist (über den Ausdrucksbaum oder sogar über Reflection.Emit). Ein gutes internes DSL ist für den universellen Einsatz und die Existenz vieler Dolmetscher konzipiert, von denen jeder seine eigenen Besonderheiten hat, seine eigenen Abstraktionslecks.
    Code, der Internes DSL verwendet, kann sich stark von dem unterscheiden, was er zuvor war.


    Aus der Box


    Integration mit EF Core.
    Die laufende Aufgabe besteht darin, Zirkelverweise abzuschneiden, damit nur das, was im Include-Ausdruck angegeben ist, serialisiert werden kann:


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

    Er ToJsonEfakzeptiert die Navigationssequenz für den Interpreter , verwendet sie für die Serialisierung (wählt Blätter mit der Regel "Standard für EF Core" aus, dh die öffentliche Lese- / Schreibeigenschaft), ist an dem Modell interessiert - wobei String / Json, so wie er eingefügt wird, Feldformatierer verwendet Standard (Byte [] in Zeichenfolge, Datum / Uhrzeit in ISO usw.). Daher muss er IQuaryable unter ihm durchführen.


    Wenn das Ergebnis transformiert wird, ändern sich die Regeln. Es ist nicht erforderlich, die DSL-Includes zum Einstellen der Navigation zu verwenden (wenn die Regel nicht erneut verwendet wird), wird ein anderer Interpreter verwendet und die Konfiguration erfolgt lokal:


    static CachedFormatter cachedFormatter1 = new CachedFormatter();
    stringGetJson()
    {
               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, "alles standardmäßig", nur dann gespeichert werden können, wenn dies sehr notwendig ist und / oder wenn es sich um Ihren eigenen Dolmetscher handelt. Auf der anderen Seite kehren wir noch einmal zu den Pluspunkten zurück: DTO wird nicht über den Code verschmiert, der durch eine bestimmte Funktion gegeben wird. Interpreter sind universell. Der Code wird weniger - es ist schon gut.


    Es muss gewarnt werden : Obwohl es in ASP so aussieht und Vorkenntnisse immer verfügbar sind, ist Streaming-Serializer in der Welt des Webs nicht unbedingt erforderlich, wo sogar Datenbanken Daten an Json senden, aber DSL Includes in ASP. MVC ist 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 DS Includes. Ich werde sowohl neue Funktionen als auch das Durchsickern von Abstraktionen zeigen, um zu zeigen, dass das Problem der Analyse von "Kosten und Übernahmen" tatsächlich ausgeschöpft ist.


    Weitere DSL-Angebote


    Include<Point> include = chain => chain.Include(e=>e.X).Include(e=>e.Y);

    Dies unterscheidet sich von EF Core Includes, die auf statischen Funktionen basieren, die Variablen nicht zugewiesen und nicht als Parameter übergeben werden können. DSL Includes selbst ist aus der Notwendigkeit entstanden, das "Include" in meine Implementierung der Repository-Vorlage zu übergeben, ohne die Informationen über die Typen zu beeinträchtigen, die erscheinen würden, wenn sie in Strings übersetzt würden.


    Der wesentlichste Unterschied besteht immer noch in der Ernennung. EF Core Includes - Ermöglicht Navigationseigenschaften (Zweigknoten), DSL Includes - einen Datensatz für den Durchlauf des Berechnungsbaums, wobei dem Ergebnis jeder Berechnung ein Name (Pfad) zugewiesen wird.


    Interne Ansicht von EF Core Includes - Die Liste der von MemberExpression.Member empfangenen Zeichenfolgen (Ausdruckssatz e=>User.Namekann nur [MemberExpression] sein) ( https://msdn.microsoft.com/de-de/library/system.linq.expressions.memberexpression(v=vs). 110) .aspx und nur die Zeile wird in internen Ansichten gespeichert Name.


    In DSL Enthält eine interne Darstellung - die ChainNode- und ChainMemberNode- Klassen, in denen ein vollständiger Ausdruck (z. B. e=>User.Name) gespeichert wird , der in den Ausdrucksbaum eingebettet werden kann. Daraus folgt, dass DSL Includes sowohl Felder als auch benutzerdefinierte Werttypen und Funktionsaufrufe unterstützt:


    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- zeigt {"UserName": "John", "EAddress": "JOHN@MAIL.COM"} an.


    Die Ausführung kann auch nützlich sein, um eine Durchquerung nullfähiger Strukturen anzugeben.


    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 => e.X) //                        .ThenInclude(e => e.Y);

    In DSL Includes gibt es auch eine kurze Aufzeichnung eines ThenIncluding auf mehreren Ebenen.


    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 eine ähnliche Form der Navigation aufgezeichnet habe, muss ich wissen, wie der Interceptor, der QuaryableExtensions genannt wird, funktioniert. Und es übersetzt Aufrufe von Include und ThenInclude, um "String" einzuschließen. Was wichtig ist (muss berücksichtigt werden).


    Algebra enthält Ausdrücke .


    Include-Ausdrücke können sein:


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

    Zu klonen
    var include2 = InlcudeExtensions.Clone(include1);

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

    Konvertieren Sie in XPath-Listen - bis zu den Blättern
    IReadOnlyCollection<string> paths1 = InlcudeExtensions.ListLeafXPaths(include); // as xpaths
    IReadOnlyCollection<string[]> paths2 = InlcudeExtensions.ListLeafKeyPaths(include); //  as string[]

    usw.


    Die gute Nachricht ist, dass es keine Abstraktionslecks gibt, eine Ebene der reinen Abstraktion wurde erreicht. Es gibt Metadaten und arbeiten mit Metadaten.


    Dialektik


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


    In diesem Fall ist es nicht notwendig, DSLs als fließende Ketten zu generieren. Sie müssen lediglich Strukturen für die interne Darstellung 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 e.g.: var child = root.AddChild("X");
    Include<Point> include = ChainNodeExtensions.ComposeInclude<Point>(root);

    Die Dolmetscher können auch an die Dolmetscher übergeben werden. Warum enthält der fließende DSL-Datensatz überhaupt? Dies ist eine rein spekulative Frage, deren Antwort lautet: Denn in der Praxis wird die Entwicklung einer internen Repräsentation (und entwickelt sich auch) nur mit der Entwicklung von DSL (dh einer kurzen, für statischen Code geeigneten Aufzeichnung) erzielt. Dies wird noch einmal näher an der Schlussfolgerung diskutiert.


    Kopieren, Klonen, Gleiches


    All das oben Gesagte gilt auch für Interpreter von Include-Ausdrücken, die copy , clone und gleich Iteratoren implementieren .


    Gleich

    Сравнение только по листьям из Include-выражения.
    Скрытая семантическая проблема: оценивать или нет порядок в списке


    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

    Проход по узлам выражения. Копируются свойства подходящие под правило.


    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);

    Может существовать интрепретатор который будет отбирать leaf из includes. Почему сделано — через отдельное правило? Что было схоже с семантикой ObjectExtensions.Copy


    Kopieren

    Проход по узлам-ветка выражения и идентификация по узлам-листьям. Копируются свойства подходящие под правило (схоже с Clone).


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

    Может существовать интерпретатор который будет отбирать leaf из includes. Почему сделано — через отдельное правило? Что было схоже с объявление ObjectExtensions.Copy (там разделение вынуждено — в include то как идентифицируем, в supportedLeafsRule — то что копируем).


    Beim Kopieren / Klonen sollten Sie Folgendes beachten:


    1. Die Unfähigkeit, Readonly-Eigenschaften zu kopieren, sind die gängigen Typen Tuple <,> und Anonymous Type. Ein ähnliches Problem beim Klonen, jedoch aus einem anderen Blickwinkel.
    2. Ein abstrakter Typ (z. B. IEnumerable wird von einem privaten Typ implementiert) - mit welchem ​​öffentlichen Typ er ersetzt werden soll.
    3. Alle Ausdrücke aus Include-Ausdrücken, die keine Eigenschaften und Felder ausdrücken, werden verworfen.
    4. "Kopieren in ein Array" ist nicht klar, was es ist.

    Der DSL-Autor muss sich auf die Tatsache verlassen, dass solche unsicheren Situationen, die sich aus dem Konflikt der Semantik und der Art und Weise der Metadatenschreibung ergeben, der Benutzer voraussehen kann, d.h. wird davon ausgehen, dass sie zu einem undefinierten Ergebnis führen und nicht auf vorhandene Dolmetscher angewiesen sind. Übrigens die Serialisierung anonymer Eigenschaften und Tuple<,>d. H. C-Typen sind Readonly-Eigenschaften, oder das Kopieren von ValueTuple<,>C Writabale-Feldern ist nicht ungewiss (und wird wie erwartet implementiert).


    Die gute Nachricht ist, dass es ziemlich einfach ist, einen eigenen Interpreter zu schreiben (ohne zu beanspruchen, Ausdrucksbäume zu kompilieren). Alle Algebra-Arbeiten mit Include DSL wurden bereits implementiert.


    Es ist möglich, Interpreter wie Detach, FindDifferences usw. zu erstellen.


    Warum Laufzeit, nicht .cs vor der Kompilierung?


    Die Möglichkeit, .cs zu generieren, ist besser als nicht in der Lage zu sein, aber die Laufzeit hat ihre Vorteile:


    1. Vermeiden Sie kostspielige Umstände mit generierten Quellcodes (Einstellungen von Verzeichnissen, Dateinamen, Quellcodeverwaltung).
    2. Wir vermeiden die Bindung an die Programmierumgebung, Plugins, das Abfangen von Ereignissen, Skriptsprachen - alles, was die Eintrittsschwelle erhöht.
    3. Das garantierte Fehlen von Änderungen am generierten Code erleichtert die Aktualisierung.
    4. Vermeiden Sie das Problem von "Eiern und Hühnern". Die Generierung des dev-Zeitcodes erfordert eine Planung, andernfalls können Sie sich in die Situation begeben: "A" kann nicht kompiliert werden, da "B" noch nicht generiert wurde, und "B" kann nicht generiert werden, da "A" noch nicht kompiliert ist.

    Letzteres wird von Roslyn gelöst, aber diese Lösung bringt Einschränkungen mit und erhöht die Eintrittsschwelle. Wenn jedoch TypScript-Bindigs erforderlich sind (ich habe DTO mit einer Funktion geschrieben, d. H., Das ist jetzt ein Problem), müssen Sie die DSL-Includes-Roslyn-Ausdrücke (den harten Teil) herausziehen und ihren Interpreter in Typoskript (den einfachen Teil) schreiben. „Für Unternehmen“ können Sie den „idealen Serializer“ auch in .cs (und nicht in Expression Trees) schreiben.


    Um es zusammenzufassen: die gleiche Kodogeneration-Laufzeit - fast rein kodogeneratsiya, ein Minimum an Infrastruktur. Sie müssen nur daran denken, dass Sie das wiederholte Erstellen von Funktionen vermeiden sollten, die wiederverwendet werden können (und dem exponentiellen Datensatz von Expression Trees zustimmen).


    Probleme mit der Effizienz der kompilierten Funktionen von Expression Trees


    Bei der Programmierung des internen DSL mit dem Ausdrucksbaum sollten Sie Folgendes berücksichtigen:


    1. LambdaExpression.Compilekompiliert nur den oberen Lambda . Zur gleichen Zeit arbeitet der Ausdruck, aber langsam. Es ist notwendig, jedes Lambda im Verlauf des "Verklebens" des Ausdrucksbaums zu kompilieren und als Parameter an CallExpression zu übergeben - nicht LambdaExpression, sondern einen in einer ConstantExpression-Konstante eingeschlossenen Delegaten (d. H. Eine kompilierte LambdaExpression). Dies erhöht den Code erheblich, da "Weitergabe der Lambda / Funktion" die beliebteste Operation in der Funktionsprogrammierung ist, die für Expression Trees erforderlich ist.


    2. Die Kompilierung erfolgt in einer dynamisch generierten anonymen Assembly , und der Methodenaufruf durchläuft (in meinen Tests 10 Nanosekunden) eine Sicherheitsüberprüfung (meine Assembly ist nicht signiert, möglicherweise ist sie länger signiert). Es ist natürlich nicht viel, aber wenn der Code stark gespalten ist, kann er sich ansammeln.



    Sie können versuchen, eine Optimierungsstrategie zu formulieren, die diese und andere Aspekte der Codegenerierung berücksichtigt (in einer anonymen Assembly), was ich noch nicht tun kann, da ich nicht alle Einzelheiten vollständig verstanden habe. Aber es gibt einen praktischen Ausweg: Ich habe bei Benchmarks angehalten, die für mich ausreichend sind. Übrigens - ja - die Generation in .cs würde alle aufgelisteten Probleme beseitigen.


    Serialisierungs-Benchmark


    Daten - Ein Objekt, das ein Array von 600 Einträgen in 15 Feldern einfacher Typen enthält. Für das Streaming von JSON.NET, ServiceStack, sind zwei Aufrufe erforderlich, um ein GetProperties () zu überlegen.


    dslComposeFormatter - ComposeFormatter an erster Stelle, den Rest der Details hier .


    BenchmarkDotNet = v0.10.14, Betriebssystem = Windows 10.0.17134
    Intel Core i5-2500K, CPU 3,30 GHz (Sandy Bridge), 1 CPU, 4 logische und 4 physische Kerne.
    NET Core SDK = 2.1.300


    MethodeMittelwertFehlerStddevMinMaxMedianZugewiesen
    dslComposeFormatter2,208 ms0,0093 ms0,0078 ms2,193 ms2,220 ms2,211 ms849,47 KB
    JsonNet_Default2,902 ms0,0160 ms0,0150 ms2,893 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,0131 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 - Ausdruck, bei dem nur das obere Lambda kompiliert wird (der Preis des Fehlers).


    Formalisierung


    Codegenerierung und DSL sind wie folgt verbunden: Um eine effektive DSL zu erstellen, ist die Codegenerierung in der Laufzeitsprache erforderlich. Um ein effizientes internes DSL zu erstellen, ist eine Laufzeitcode-Generierung erforderlich .


    Wir verwenden Expression Tree, da dies die einzige Möglichkeit ist, die Codegenerierung im .NET Standard-Framework zu haben.


    Andererseits ist die Verwendung von Ausdrucksbäumen zum Analysieren von Ausdrücken kein Attribut, das Internal DSL von der gesamten Fluent-API-Klasse unterscheidet. Ein solches Zeichen ist die Verwendung der C # -Grammatik, um Beziehungen im Problembereich auszudrücken.


    Das Erstellen von Präsentationsstrukturen kann durch einfaches Ausführen fließender Code-Ausdrücke (unterschiedslos nach Ausdrucksbäumen) durchgeführt werden, während das charakteristische Merkmal von Internal DSL in С # die Ausführung von fließenden Ketten ist, von denen jede ein wenig durch Ausdrucksbäume analysiert wird.


    Expression Trees in DSL Includes spielen eine sehr kleine Rolle (Knotennamen abrufen, wenn sie nicht manuell angegeben werden) und umgekehrt, um einen effektiven Interpreter / Serializer einer kompilierten Laufzeit zu erstellen - entscheidend (Laufzeitkompilierung).


    Erstellen Interne DSL ist wichtiger für die Zukunft des kreativen Prozesses: von Bibliotheksfunktionen Eigenschaften iterator s serialize , Kopier- , den Klon , das Gleichheits abgeleitet relativ eine Methode gefunden , um den Prozess der Iteration zu erfassen und effektiv das Schreiben von „Bypass“ zu vereinfachen . Wenn man den Eindruck hat, dass es sich lohnt, sich darauf zu beschränken, nur „Repräsentationsstrukturen“ zu erfinden, verläuft der kreative Prozess nicht auf diese Weise. Bequemes symbolisches Schreiben ist notwendig: Includes Algebra ist viel ausdrucksstärker (und unterstützt daher das Denken) als dieselben Operationen, die mit Strukturen aufgezeichnet wurden (obwohl die direkte Verwendung von Strukturen auch gerechtfertigt sein kann, da sie effektiv ist).


    Fazit


    Mit Hilfe von DSL Includes wurde es möglich, DTO zu schreiben, und schließlich handelt es sich in einer erheblichen Anzahl von Fällen um die Serialisierungsfunktion (in Json). Es war möglich, ein neues Abstraktionsniveau zu erreichen, ohne dabei zu verlieren, aber sowohl hinsichtlich Rechengeschwindigkeit als auch "weniger Code" an Leistung zu gewinnen, aber immer noch aufgrund einer Zunahme der angewandten Komplexität. Wachstum der Abstraktion = Wachstum der Abstraktionslecks.


    Der Entwickler von Internal DSL reagiert auf dieses Problem, indem er den Benutzer auf die Semantik der von DSL-Interpreten implementierten Operationen aufmerksam macht, auf die Notwendigkeit der Kenntnis der Strukturen der Internen DSL-Ansicht (in welcher Form Ausdruck gespeichert ist) und auf die Wichtigkeit, die interne Struktur des Interpreters zu kennen (Verwendung des Compression Tree oder nicht) ).


    Sowohl DSL Includes als auch json Serializer ComposeFormatter befinden sich in der DashboardCodes.Routines- Bibliothek, die über Nuget und GitHub zugänglich ist.


    Jetzt auch beliebt: