Aspektorientierte Programmierung (AOP) über den Quellcode





Aspektorientierte Programmierung ist ein sehr attraktives Konzept, um Ihre Codebasis zu vereinfachen, sauberen Code zu erstellen und Fehler beim Kopieren und Einfügen zu minimieren.



Heutzutage werden in den meisten Fällen Aspekte auf Bytecode-Ebene implementiert, d.h. Nach der Kompilierung "verwebt" ein Tool einen zusätzlichen Bytecode mit Unterstützung der erforderlichen Logik.



Unser Ansatz (wie auch der Ansatz einiger anderer Tools) besteht darin, den Quellcode zu modifizieren, um die Aspektlogik zu implementieren. Mit dem Übergang zur Roslyn-Technologie ist dies sehr einfach zu erreichen, und das Ergebnis bietet bestimmte Vorteile gegenüber der Änderung des Bytecodes selbst.



Wenn Sie an den Details interessiert sind, sehen Sie bitte Katze.



Sie können denken, dass es bei der aspektorientierten Programmierung nicht um Sie geht und Sie nicht besonders betrifft, sondern nur um eine Reihe unverständlicher Wörter. Tatsächlich ist es jedoch viel einfacher als es scheint. Hier geht es um die Probleme der realen Produktentwicklung. Wenn Sie sich mit industrieller Entwicklung beschäftigen, können Sie dies definitiv tun Nutzen Sie es.



Insbesondere bei mittelgroßen Projekten auf Unternehmensebene, bei denen die Anforderungen an die Funktionalität der Produkte formalisiert werden. Beispielsweise kann eine Anforderung bestehen: Protokollieren Sie beim Setzen des Konfigurationsflags alle Eingabeparameter für alle öffentlichen Methoden. Oder verfügen Sie für alle Projektmethoden über ein Benachrichtigungssystem, das eine Nachricht sendet, wenn ein bestimmter Schwellenwert für die Ausführungszeit dieser Methode überschritten wird.



Wie geht das ohne AOP? Entweder wird es nur für die wichtigsten Teile eingeschlagen und ausgeführt, oder beim Schreiben neuer Methoden wird ähnlicher Code aus benachbarten Methoden mit allen zugehörigen Methoden kopiert und eingefügt.



Bei Verwendung von AOP wird ein Rat geschrieben, der auf das Projekt angewendet wird und die Arbeit erledigt ist. Wenn Sie die Logik ein wenig aktualisieren müssen, aktualisieren Sie den Hinweis erneut und er wird beim nächsten Build angewendet. Ohne AOP sind das 100.500 Updates im gesamten Projektcode.



Das Plus ist, dass Ihr Code nicht mehr wie eine Person aussieht, die Pocken hatte, weil er mit solchen Funktionen übersät ist und beim Lesen des Codes wie störendes Rauschen aussieht.



Nachdem Sie AOP in Ihr Projekt eingeführt haben, beginnen Sie mit der Implementierung von Dingen, von denen Sie ohne sie nie geträumt haben, da sie wie ein relativ kleiner Vorteil zu hohen Kosten aussahen. Bei AOP ist genau das Gegenteil der Fall, relativ niedrige Kosten und große Vorteile (bei ähnlichen Kosten Ihrer Bemühungen).



Meiner Meinung nach ist die aspektorientierte Programmierung im .Net-Ökosystem im Vergleich zum Java-Ökosystem deutlich weniger beliebt. Ich denke, der Hauptgrund ist der Mangel an kostenlosen und Open-Source-Tools, die mit der Funktionalität und Qualität von Java vergleichbar sind.



PostSharp bietet ähnliche Funktionen und Komfort, aber nicht viele sind bereit, Hunderte von Dollar für die Verwendung in ihren Projekten zu zahlen, und die Community-Version ist in ihren Funktionen sehr eingeschränkt. Natürlich gibt es Alternativen, aber leider haben sie das PostSharp-Niveau nicht erreicht.



Sie können die Funktionen der Tools vergleichen (es muss berücksichtigt werden, dass der Vergleich vom Eigentümer von PostSharp durchgeführt wurde, aber es gibt ein Bild).



Unser Weg zur aspektorientierten Programmierung



Wir sind ein kleines Beratungsunternehmen (12 Personen) und das Endergebnis unserer Arbeit ist der Quellcode. Jene. Wir werden dafür bezahlt, Quellcode und Qualitätscode zu erstellen. Wir arbeiten nur in einer Branche und viele unserer Projekte haben sehr ähnliche Anforderungen. Infolgedessen ist der Quellcode auch zwischen diesen Projekten ziemlich ähnlich.



Und da unsere Ressourcen begrenzt sind, ist für uns eine der wichtigsten Aufgaben die Möglichkeit, Code wiederzuverwenden und Tools zu verwenden, die den Entwickler vor Routineaufgaben bewahren.



Um dies zu erreichen, nutzen wir unter anderem die Funktionen zur automatischen Codegenerierung und haben mehrere benutzerdefinierte Plugins und Analysatoren für Visual Studio erstellt, die für unsere Projekte und Aufgaben spezifisch sind. Dies ermöglichte es, die Produktivität der Programmierer erheblich zu steigern und gleichzeitig die hohe Qualität des Codes beizubehalten (man könnte sogar sagen, dass die Qualität höher geworden ist).



Der nächste logische Schritt war die Idee, die Verwendung von aspektorientierter Programmierung zu implementieren. Wir haben verschiedene Ansätze und Tools ausprobiert, aber das Ergebnis hat unsere Erwartungen weit übertroffen. Dies fiel zeitlich mit der Veröffentlichung der Roslyn-Technologie zusammen, und zu einem bestimmten Zeitpunkt hatten wir die Idee, die Funktionen der automatischen Codegenerierung und Roslyn zu kombinieren.



In nur wenigen Wochen wurde ein Prototyp des Instruments erstellt, und unserer Meinung nach schien dieser Ansatz vielversprechender. Nach mehreren Iterationen bei der Verwendung und Aktualisierung dieses Tools können wir sagen, dass unsere Erwartungen erfüllt wurden und sogar mehr als wir erwartet hatten. Wir haben eine Bibliothek mit nützlichen Vorlagen entwickelt und verwenden diesen Ansatz in den meisten unserer Projekte. Einige unserer Kunden verwenden ihn auch und bestellen sogar die Entwicklung von Vorlagen für ihre Anforderungen.



Leider ist unser Tool noch lange nicht ideal, daher möchte ich die Beschreibung in zwei Teile aufteilen. Der erste ist, wie ich die Implementierung dieser Funktionalität in einer idealen Welt sehe, und der zweite ist, wie es hier gemacht wird.



Bevor wir zu den Details kommen, möchte ich eine kleine Erklärung abgeben - alle Beispiele in diesem Artikel wurden so vereinfacht, dass Sie die Idee zeigen können, ohne mit irrelevanten Details überladen zu sein.



Wie es in einer perfekten Welt gemacht würde



Nachdem ich unser Tool mehrere Jahre lang verwendet habe, habe ich eine Vision davon, wie dies funktionieren soll, wenn wir in einer idealen Welt leben.



In meiner Vision einer idealen Welt ermöglichen die Sprachspezifikationen die Verwendung von Quellcode-Transformationen, und es gibt Unterstützung für Compiler und IDE.



Die Idee wurde durch die Aufnahme des Modifikators "partiell" in die C # -Sprachenspezifikation inspiriert. Dieses recht einfache Konzept (die Möglichkeit, eine Klasse, Struktur oder Schnittstelle in mehreren Dateien zu definieren) hat die Unterstützung von Tools für die automatische Quellcodegenerierung erheblich verbessert und vereinfacht. Jene. Es ist eine Art horizontale Aufteilung des Quellcodes einer Klasse zwischen mehreren Dateien. Für diejenigen, die die C # -Sprache nicht kennen, ein kleines Beispiel.



Angenommen, wir haben ein einfaches Formular, das in der Datei Example1.aspx beschrieben ist

<%@ Page Language="C#" AutoEventWireup="True" %>
// . . .
<asp:Button id="btnSubmit"
           Text="Submit"
           OnClick=" btnSubmit_Click" 
           runat="server"/>
// . . .


Und benutzerdefinierte Logik (z. B. Ändern der Schaltflächenfarbe in Rot, wenn darauf geklickt wird) in der Datei Example1.aspx.cs



public partial class ExamplePage1 : System.Web.UI.Page, IMyInterface
{
  protected void btnSubmit_Click(Object sender, EventArgs e) 
  {
    btnSubmit.Color = Color.Red;
  }
}


Das Vorhandensein der von "teilweise" bereitgestellten Funktionen in der Sprache ermöglicht es dem Toolkit, die Datei Example1.aspx zu analysieren und die Datei Example1.aspx.designer.cs automatisch zu generieren.



public partial class ExamplePage1 : System.Web.UI.Page
{
  protected global::System.Web.UI.WebControls.Button btnSubmit;
}


Jene. Wir haben die Möglichkeit, einen Teil des Codes für die ExamplePage1-Klasse in einer Datei vom aktualisierbaren Programmierer (Example1.aspx.cs) und einen Teil in der Datei Example1.aspx.designer.cs durch das automatisch generierte Toolkit zu speichern. Für den Compiler sieht es am Ende wie eine allgemeine Klasse aus



public class ExamplePage1 : System.Web.UI.Page, IMyInterface
{ 
  protected global::System.Web.UI.WebControls.Button btnSubmit;

  protected void btnSubmit_Click(Object sender, EventArgs e) 
  {
    btnSubmit.Color = Color.Red;
  }
}


Anhand des Beispiels mit der Definition der Vererbung der IMyInterface-Schnittstelle können Sie sehen, dass das Endergebnis eine Kombination von Klassendefinitionen aus verschiedenen Dateien ist.



Wenn wir keine Funktionen wie teilweise haben und der Compiler das Speichern des gesamten Klassencodes in nur einer Datei erfordert, können wir Unannehmlichkeiten und zusätzliche Gesten annehmen, die zur Unterstützung der automatischen Generierung erforderlich sind.



Dementsprechend ist es meine Idee, zwei zusätzliche Modifikatoren in die Sprachspezifikation aufzunehmen, die das Einbetten von Aspekten in den Quellcode erleichtern.



Der erste Modifikator ist original und wir fügen ihn der Klassendefinition hinzu, die transformiert werden kann.



Die zweite wird verarbeitet und symbolisiert, dass dies die endgültige Klassendefinition ist, die vom Quellentransformationstool erhalten wurde und die vom Compiler akzeptiert werden muss, um den Bytecode zu generieren.



Die Sequenz ist ungefähr so



  1. Der Benutzer arbeitet mit dem Quellcode der Klasse, die den ursprünglichen Modifikator in der CS-Datei enthält (z. B. Example1.cs).
  2. Beim Kompilieren überprüft der Compiler die Richtigkeit des Quellcodes. Wenn die Klasse erfolgreich kompiliert wurde, prüft er, ob das Original vorhanden ist
  3. Wenn das Original vorhanden ist, gibt der Compiler den Quellcode dieser Datei an den Transformationsprozess weiter (dies ist eine Blackbox für den Compiler).
  4. .processed.cs .processed.cs.map ( .cs .processed.cs, IDE)
  5. .processed.cs ( Example1.processed.cs) .
  6. ,



    a. original processed

    b. .cs .processed.cs
  7. , .processed.cs .


Jene. Durch Hinzufügen dieser beiden Modifikatoren konnten wir die Unterstützung für Quellcode-Transformationstools auf Sprachebene organisieren, ebenso wie es teilweise möglich war, die Unterstützung für die Quellcode-Generierung zu vereinfachen. Jene. Parial ist horizontale Codeaufteilung, Original / verarbeitet ist vertikal.



Aus meiner Sicht ist die Implementierung der ursprünglichen / verarbeiteten Unterstützung im Compiler eine Woche Arbeit für zwei Praktikanten bei Microsoft (ein Witz natürlich, aber nicht weit von der Wahrheit entfernt). Im Großen und Ganzen gibt es bei dieser Aufgabe keine grundsätzlichen Schwierigkeiten. Aus Sicht des Compilers handelt es sich um Dateimanipulation und Prozessaufruf.



In .NET 5 - Quellcodegeneratoren wurde eine neue Funktion hinzugefügtDamit können Sie bereits während der Kompilierung neue Quellcodedateien generieren. Dies ist eine Bewegung in die richtige Richtung. Leider können Sie damit nur neuen Quellcode generieren, den vorhandenen jedoch nicht ändern. Also warten wir immer noch.



Ein Beispiel für einen ähnlichen Prozess. Benutzer erstellt Datei Example2.cs

public original class ExamplePage2 : System.Web.UI.Page, IMyInterface
{ 
  protected global::System.Web.UI.WebControls.Button btnSubmit;

  protected void btnSubmit_Click(Object sender, EventArgs e) 
  {
    btnSubmit.Color = Color.Red;
  }	
}


Wird zur Kompilierung ausgeführt. Wenn alles in Ordnung ist und der Compiler den ursprünglichen Modifikator sieht, gibt er den Quellcode an den Transformationsprozess weiter, der die Datei Example2.processed.cs generiert (im einfachsten Fall kann es sich nur um eine exakte Kopie von Example2.cs handeln, wobei das Original durch das verarbeitete ersetzt wird.) ...



In unserem Fall gehen wir davon aus, dass der Transformationsprozess einen Protokollierungsaspekt hinzugefügt hat und das Ergebnis folgendermaßen aussieht:

public processed class ExamplePage2 : System.Web.UI.Page, IMyInterface
{ 
  protected global::System.Web.UI.WebControls.Button btnSubmit;

  protected void btnSubmit_Click(Object sender, EventArgs e) 
  {
    try
    {
      btnSubmit.Color = Color.Red;
    } 
    catch(Exception ex)
    {
      ErrorLog(ex);
      throw;
    }

    SuccessLog();
  }	

  private static processed ErrorLog(Exception ex)
  {
    // some error logic here
  }

  private static processed SuccessLog([System.Runtime.CompilerServices.CallerMemberName] string memberName = "")
  {
    // some success logic here
  }
}


Der nächste Schritt besteht darin, die Signaturen zu überprüfen. Die Hauptsignaturen sind identisch und erfüllen die Bedingung, dass die Definitionen im Original und in der Verarbeitung genau gleich sein müssen.



In diesem Beispiel habe ich speziell einen weiteren kleinen Satz hinzugefügt. Dies ist der verarbeitete Modifikator für Methoden, Eigenschaften und Felder.



Es markiert Methoden, Eigenschaften und Felder als nur für Klassen mit dem verarbeiteten Modifikator verfügbar und wird beim Vergleichen von Signaturen ignoriert. Dies dient der Bequemlichkeit von Aspektentwicklern und ermöglicht es Ihnen, die allgemeine Logik in separate Methoden zu verschieben, um keine unnötige Code-Redundanz zu erzeugen.



Der Compiler hat diesen Code kompiliert. Wenn alles in Ordnung ist, hat er den Bytecode verwendet, um den Vorgang fortzusetzen.



Es ist klar, dass es in diesem Beispiel eine gewisse Vereinfachung gibt und in Wirklichkeit die Logik komplizierter sein kann (zum Beispiel, wenn wir sowohl Original als auch Teil für dieselbe Klasse einschließen), aber dies ist keine unüberwindbare Komplexität.



Grundlegende IDE-Funktionalität in einer perfekten Welt



Die Unterstützung für die Arbeit mit dem Quellcode von .processed.cs-Dateien in der IDE besteht hauptsächlich in der korrekten Navigation zwischen den ursprünglichen / verarbeiteten Klassen und Übergängen während des schrittweisen Debuggens.



Das zweitwichtigste Merkmal der IDE (aus meiner Sicht) ist das Lesen des Codes verarbeiteter Klassen. Eine verarbeitete Klasse kann viele Codeteile enthalten, die durch verschiedene Aspekte hinzugefügt wurden. Die Implementierung einer Anzeige, die dem Konzept von Ebenen in einem Grafikeditor ähnelt, erscheint uns als die bequemste Option, um dieses Ziel zu erreichen. Unser aktuelles Plugin implementiert etwas Ähnliches und die Resonanz seiner Benutzer ist recht positiv.



Eine weitere Funktion, die dazu beitragen würde, AOP in den Alltag einzuführen, ist die Umgestaltung der Funktionalität. Ein Benutzer, der einen Teil des Codes hervorhebt, könnte "In AOP-Vorlage extrahieren" sagen, und die IDE hat die richtigen Dateien erstellt, den Anfangscode generiert und nach Analyse des Projektcodes Kandidaten für die Verwendung einer Vorlage aus anderen Klassen vorgeschlagen.



Das i-Tüpfelchen wäre die Unterstützung für das Schreiben von Aspektvorlagen, z. B. das interaktive Anwenden eines Aspekts auf eine Klasse / Methode Ihrer Wahl, damit Sie das Endergebnis im Handumdrehen ohne expliziten Kompilierungszyklus bewerten können.



Ich bin sicher, wenn die Macher des Resharper das Geschäft übernehmen, ist die Magie garantiert.



Schreiben von Aspektcode in einer perfekten Welt



Um TRIZ zu paraphrasieren: Das ideale Schreiben von Code zur Implementierung von Aspekten ist das Fehlen des Schreibens von zusätzlichem Code, der nur zur Unterstützung der Instrumentierungsprozesse vorhanden ist.



In einer idealen Welt möchten wir Code für den Aspekt selbst schreiben, ohne die Hilfslogik schreiben zu müssen, um dieses Ziel zu erreichen. Und dieser Code wäre ein wesentlicher Bestandteil des Projekts.



Der zweite Wunsch ist die Fähigkeit, interaktives Plug & Play zu haben, d.h. Nachdem wir eine Vorlage geschrieben haben, müssten wir keine zusätzlichen Schritte unternehmen, damit sie für die Transformation verwendet werden kann. Es war nicht erforderlich, das Tool neu zu kompilieren, seine Fehler abzufangen usw. Außerdem konfigurieren Sie Optionen in Projekten für die Nachkompilierung.



Nachdem ich eine Vorlage erstellt und ein paar Zeilen geschrieben habe, würde ich das Ergebnis sofort sehen. Wenn es Fehler enthält, wird deren Erkennung und Fehlerbehebung in den Prozess der Anwendung der Vorlage integriert und ist kein separater Teil, der zusätzliche Anstrengungen des Programmierers erfordert.



Nun, damit die Syntax der Vorlage der Syntax der C # -Sprache so nahe wie möglich kommt, idealerweise ein kleines Add-On sowie einige Schlüsselwörter und Platzhalter.



Unsere aktuelle Implementierung



Leider leben wir nicht in einer idealen Welt, deshalb müssen wir Fahrräder neu erfinden und fahren.



Code-Injection, Kompilierung und Debugging



Unser aktuelles Modell besteht darin, zwei Kopien des Projekts zu erstellen. Eines ist das Original, dasjenige, mit dem der Programmierer arbeitet, das zweite ist das transformierte, das zum Kompilieren und Ausführen verwendet wird.



Das Szenario ist ungefähr so



  • , , ..
  • , , , .
  • , , , WPF , ..


Zum Debuggen wird die zweite Kopie der IDE gestartet, eine vom Land erstellte Kopie des Projekts wird geöffnet und es funktioniert mit der Kopie, auf die die Transformation angewendet wurde.



Der Prozess erfordert eine bestimmte Disziplin, wird jedoch von Zeit zu Zeit zur Gewohnheit, und in bestimmten Fällen hat dieser Ansatz einige Vorteile (z. B. kann ein Build gestartet und auf einem Remote-Server bereitgestellt werden, anstatt mit einem lokalen Computer zu arbeiten). Außerdem vereinfacht die Hilfe des Plugins in VisualStudio den Vorgang.



IDE



Wir verwenden ein Plugin, das auf unsere spezifischen Aufgaben und Prozesse zugeschnitten ist, und die Unterstützung für die Implementierung von Quellcode ist ein relativ kleiner Teil seiner Funktionen.



Die Funktion zum Anzeigen von Ebenen in einem Stil, der einem grafischen Editor ähnelt, ermöglicht beispielsweise das Ausblenden / Anzeigen von Kommentarebenen nach Bereich (z. B. damit nur öffentliche Methoden sichtbar sind) und Regionen. Der eingebettete Code ist von Kommentaren eines speziellen Formats umgeben und kann auch als separate Ebene ausgeblendet werden.



Eine andere Möglichkeit besteht darin, einen Unterschied zwischen der ursprünglichen und der transformierten Datei anzuzeigen. Da die IDE den relativen Speicherort der Kopie der Datei im Projekt kennt, kann sie die Unterschiede zwischen der ursprünglichen und der vom Land generierten Datei anzeigen.



Außerdem warnt das Plugin, wenn versucht wird, Änderungen an der vom Land generierten Kopie vorzunehmen (um sie bei der anschließenden Neuumwandlung nicht zu verlieren).



Aufbau



Eine separate Aufgabe besteht darin, Transformationsregeln festzulegen, d.h. Auf welche Klassen und Methoden werden wir die Transformation anwenden.



Wir verwenden mehrere Ebenen.



Die erste Ebene ist die Konfigurationsdatei der obersten Ebene. Wir können Regeln festlegen, die vom Pfad im Dateisystem, Mustern im Namen von Dateien, Klassen oder Methoden, Bereichen von Klassen, Methoden oder Eigenschaften abhängen.



Die zweite Ebene ist ein Hinweis auf die Anwendung von Transformationsregeln auf der Ebene der Attribute von Klassen, Methoden oder Feldern.



Die dritte auf der Ebene des Codeblocks und die vierte ist eine explizite Angabe, um die Ergebnisse der Transformation der Vorlage an einer bestimmten Stelle im Quellcode aufzunehmen.



Vorlagen



In der Vergangenheit verwendeten wir zum Zwecke der automatischen Generierung Vorlagen im T4-Format. Daher war es logisch, denselben Ansatz wie Vorlagen für die Transformation zu verwenden. T4-Vorlagen bieten die Möglichkeit, beliebigen C # -Code auszuführen, haben einen minimalen Overhead und eine gute Ausdruckskraft.



Für diejenigen, die noch nie mit T4 gearbeitet haben, wäre das einfachste Analogon die Darstellung des ASPX-Formats, das anstelle von HTML Quellcode in C # generiert und nicht auf IIS ausgeführt wird, sondern als separates Dienstprogramm zur Ausgabe des Ergebnisses an die Konsole (oder in eine Datei).



Beispiele von



Um zu verstehen, wie dies in der Realität funktioniert, ist es am einfachsten, den Code vor und nach der Transformation sowie den Quellcode der Vorlagen zu demonstrieren, die während der Transformation verwendet werden. Ich werde die einfachsten Optionen demonstrieren, aber das Potenzial wird nur durch Ihre Vorstellungskraft begrenzt.



Beispielquellcode vor der Transformation
// ##aspect=AutoComment

using AOP.Common;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Aspectimum.Demo.Lib
{

    [AopTemplate("ClassLevelTemplateForMethods", NameFilter = "First")]
    [AopTemplate("StaticAnalyzer", Action = AopTemplateAction.Classes)]
    [AopTemplate("DependencyInjection", AdvicePriority = 500, Action = AopTemplateAction.PostProcessingClasses)]
    [AopTemplate("ResourceReplacer", AdvicePriority = 1000, ExtraTag = "ResourceFile=Demo.resx,ResourceClass=Demo", Action = AopTemplateAction.PostProcessingClasses)]
    public class ConsoleDemo
    {
        public virtual Person FirstDemo(string firstName, string lastName, int age)
        {
            Console.Out.WriteLine("FirstDemo: 1");

            // ##aspect="FirstDemoComment" extra data here

            return new Person()
            {
                FirstName = firstName,
                LastName = lastName,
                Age = age,
            };
        }

        private static IConfigurationRoot _configuration = inject;
        private IDataService _service { get; } = inject;
        private Person _somePerson = inject;

        [AopTemplate("LogExceptionMethod")]
        [AopTemplate("StopWatchMethod")]
        [AopTemplate("MethodFinallyDemo", AdvicePriority = 100)]
        public Customer[] SecondDemo(Person[] people)
        {
            IEnumerable<Customer> Customers;

            Console.Out.WriteLine("SecondDemo: 1");

            Console.Out.WriteLine(i18("SecondDemo: i18"));

            int configDelayMS = inject;
            string configServerName = inject;

            using (new AopTemplate("SecondDemoUsing", extraTag: "test extra"))
            {

                Customers = people.Select(s => new Customer()
                {
                    FirstName = s.FirstName,
                    LastName = s.LastName,
                    Age = s.Age,
                    Id = s.Id
                });

                _service.Init(Customers);

                foreach (var customer in Customers)
                {
                    Console.Out.WriteLine(i18($"First Name {customer.FirstName} Last Name {customer.LastName}"));
                    Console.Out.WriteLine("SecondDemo: 2 " + i18("First Name ") + customer.FirstName + i18(" Last Name   ") + customer.LastName);
                }
            }

            Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));
            Console.Out.WriteLine($"Server {configServerName} default delay {configDelayMS}");
            Console.Out.WriteLine($"Customer for ID=5 is {_service.GetCustomerName(5)}");

            return Customers.ToArray();
        }

        protected static string i18(string s) => s;
        protected static dynamic inject;

        [AopTemplate("NotifyPropertyChangedClass", Action = AopTemplateAction.Classes)]
        [AopTemplate("NotifyPropertyChanged", Action = AopTemplateAction.Properties)]
        public class Person
        {
            [AopTemplate("CacheProperty", extraTag: "{ \"CacheKey\": \"name_of_cache_key\", \"ExpiresInMinutes\": 10 }")]
            public string FullName
            {
                get
                {
                    // ##aspect="FullNameComment" extra data here
                    return $"{FirstName} {LastName}";
                }
            }

            public int Id { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
            public int Age { get; set; }
        }

        [AopTemplate("NotifyPropertyChanged", Action = AopTemplateAction.Properties)]
        public class Customer : Person
        {
            public double CreditScore { get; set; }
        }

        public interface IDataService
        {
            void Init(IEnumerable<Customer> customers);
            string GetCustomerName(int customerId);
        }

        public class DataService: IDataService
        {
            private IEnumerable<Customer> _customers;
            public void Init(IEnumerable<Customer> customers)
            {
                _customers = customers;
            }

            public string GetCustomerName(int customerId)
            {
                return _customers.FirstOrDefault(w => w.Id == customerId)?.FullName;
            }
        }

        public class MockDataService : IDataService
        {
            private IEnumerable<Customer> _customers;
            public void Init(IEnumerable<Customer> customers)
            {
                if(customers == null)
                    throw (new Exception("IDataService.Init(customers == null)"));
            }

            public string GetCustomerName(int customerId)
            {
                if (customerId < 0)
                    throw (new Exception("IDataService.GetCustomerName: customerId cannot be negative"));

                if (customerId == 0)
                    throw (new Exception("IDataService.GetCustomerName: customerId cannot be zero"));

                return $"FirstName{customerId} LastName{customerId}";
            }
        }
    }
}




Vollversion des Quellcodes nach der Transformation
//------------------------------------------------------------------------------
// <auto-generated> 
//     This code was generated from a template.
// 
//     Manual changes to this file may cause unexpected behavior in your application.
//     Manual changes to this file will be overwritten if the code is regenerated.
//
//  Generated base on file: ConsoleDemo.cs
//  ##sha256: ekmmxFSeH5ev8Epvl7QvDL+D77DHwq1gHDnCxzeBWcw
//  Created By: JohnSmith
//  Created Machine: 127.0.0.1
//  Created At: 2020-09-19T23:18:07.2061273-04:00
//
// </auto-generated>
//------------------------------------------------------------------------------
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;

namespace Aspectimum.Demo.Lib
{
    public class ConsoleDemo
    {
        public virtual Person FirstDemo(string firstName, string lastName, int age)
        {
            Console.Out.WriteLine("FirstDemo: 1");
            // FirstDemoComment replacement extra data here
            return new Person()
            {FirstName = firstName, LastName = lastName, Age = age, };
        }

        private static IConfigurationRoot _configuration = new ConfigurationBuilder()
            .SetBasePath(System.IO.Path.Combine(AppContext.BaseDirectory))
            .AddJsonFile("appsettings.json", optional: true)
            .Build();
        
        private IDataService _service { get; } = new DataService();

#error Cannot find injection rule for Person _somePerson
        private Person _somePerson = inject;

        public Customer[] SecondDemo(Person[] people)
        {
            try
            {
#error variable "Customers" doesn't match code standard rules
                IEnumerable<Customer> Customers;
                
                Console.Out.WriteLine("SecondDemo: 1");

#error Cannot find resource for a string "SecondDemo: i18", please add it to resources
                Console.Out.WriteLine(i18("SecondDemo: i18"));

                int configDelayMS = Int32.Parse(_configuration["delay_ms"]);
                string configServerName = _configuration["server_name"];
                {
                    // second demo test extra
                    {
                        Customers = people.Select(s => new Customer()
                        {FirstName = s.FirstName, LastName = s.LastName, Age = s.Age, Id = s.Id});
                        _service.Init(Customers);
                        foreach (var customer in Customers)
                        {
                            Console.Out.WriteLine(String.Format(Demo.First_Last_Names_Formatted, customer.FirstName, customer.LastName));
                            Console.Out.WriteLine("SecondDemo: 2 " + (Demo.First_Name + " ") + customer.FirstName + (" " + Demo.Last_Name + "   ") + customer.LastName);
                        }
                    }
                }

#error Argument for i18 method must be either string literal or interpolated string, but instead got Microsoft.CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax
#warning Please replace String.Format with string interpolation format.
                Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));
                Console.Out.WriteLine($"Server {configServerName} default delay {configDelayMS}");
                Console.Out.WriteLine($"Customer for ID=5 is {_service.GetCustomerName(5)}");

                return Customers.ToArray();
            }
            catch (Exception logExpn)
            {
                Console.Error.WriteLine($"Exception in SecondDemo\r\n{logExpn.Message}\r\n{logExpn.StackTrace}");
                throw;
            }
        }

        protected static string i18(string s) => s;
        protected static dynamic inject;
        public class Person : System.ComponentModel.INotifyPropertyChanged
        {
            public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
            protected void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
            {
                PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
            }

            public string FullName
            {
                get
                {
                    System.Runtime.Caching.ObjectCache cache = System.Runtime.Caching.MemoryCache.Default;
                    string cachedData = cache["name_of_cache_key"] as string;
                    if (cachedData == null)
                    {
                        cachedData = GetPropertyData();
                        if (cachedData != null)
                        {
                            cache.Set("name_of_cache_key", cachedData, System.DateTimeOffset.Now.AddMinutes(10));
                        }
                    }

                    return cachedData;
                    string GetPropertyData()
                    {
                        // FullNameComment FullName
                        return $"{FirstName} {LastName}";
                    }
                }
            }

            private int _id;
            public int Id
            {
                get
                {
                    return _id;
                }

                set
                {
                    if (_id != value)
                    {
                        _id = value;
                        NotifyPropertyChanged();
                    }
                }
            }

            private string _firstName;
            public string FirstName
            {
                get
                {
                    return _firstName;
                }

                set
                {
                    if (_firstName != value)
                    {
                        _firstName = value;
                        NotifyPropertyChanged();
                    }
                }
            }

            private string _lastName;
            public string LastName
            {
                get
                {
                    return _lastName;
                }

                set
                {
                    if (_lastName != value)
                    {
                        _lastName = value;
                        NotifyPropertyChanged();
                    }
                }
            }

            private int _age;
            public int Age
            {
                get
                {
                    return _age;
                }

                set
                {
                    if (_age != value)
                    {
                        _age = value;
                        NotifyPropertyChanged();
                    }
                }
            }
        }

        public class Customer : Person
        {
            private double _creditScore;
            public double CreditScore
            {
                get
                {
                    return _creditScore;
                }

                set
                {
                    if (_creditScore != value)
                    {
                        _creditScore = value;
                        NotifyPropertyChanged();
                    }
                }
            }
        }

        public interface IDataService
        {
            void Init(IEnumerable<Customer> customers);
            string GetCustomerName(int customerId);
        }

        public class DataService : IDataService
        {
            private IEnumerable<Customer> _customers;
            public void Init(IEnumerable<Customer> customers)
            {
                _customers = customers;
            }

            public string GetCustomerName(int customerId)
            {
                return _customers.FirstOrDefault(w => w.Id == customerId)?.FullName;
            }
        }

        public class MockDataService : IDataService
        {
            private IEnumerable<Customer> _customers;
            public void Init(IEnumerable<Customer> customers)
            {
                if (customers == null)
                    throw (new Exception("IDataService.Init(customers == null)"));
            }

            public string GetCustomerName(int customerId)
            {
                if (customerId < 0)
                    throw (new Exception("IDataService.GetCustomerName: customerId cannot be negative"));
                if (customerId == 0)
                    throw (new Exception("IDataService.GetCustomerName: customerId cannot be zero"));
                return $"FirstName{customerId} LastName{customerId}";
            }
        }
    }
}

// ##template=AutoComment sha256=Qz6vshTZl2/u+NgtcV4u5W5RZMb9JPkJ2Zj0yvQBH9w
// ##template=AopCsharp.ttinclude sha256=2QR7LE4yvfWYNl+JVKQzvEBwcWvReeupVpslWTSWQ0c
// ##template=FirstDemoComment sha256=eIleHCim5r9F/33Mv9B7pcNQ/dlfEhDVXJVhA7+3OgY
// ##template=FullNameComment sha256=2/Ipn8fk2y+o/FVQHAWnrOlhqS5ka204YctZkwl/CUs
// ##template=NotifyPropertyChangedClass sha256=sxRrSjUSrynQSPjo85tmQywQ7K4fXFR7nN2mX87fCnk
// ##template=StaticAnalyzer sha256=zmJsj/FWmjqDDnpZXhoAxQB61nYujd41ILaQ4whcHyY
// ##template=LogExceptionMethod sha256=+zTre3r3LR9dm+bLPEEXg6u2OtjFg+/V6aCnJKijfcg
// ##template=NotifyPropertyChanged sha256=PMgorLSwEChpIPnEWXfEuUzUm4GO/6pMmoJdF7qcgn8
// ##template=CacheProperty sha256=oktDGTfC2hHoqpbKkeNABQaPdq6SrVLRFEQdNMoY4zE
// ##template=DependencyInjection sha256=nPq/ZxVBpgrDzyH+uLtJvD1aKbajKinX/DUBQ4BGG9g
// ##template=ResourceReplacer sha256=ZyUljjKKj0jLlM2nUIr1oJc1L7otYUI8WqWN7um6NxI







Erklärungen und Vorlagencodes



AutoComment-Vorlage



// ##aspect=AutoComment


Wenn wir im Quellcode auf einen Kommentar in einem speziellen Format stoßen, führen wir die angegebene Vorlage aus (in diesem Fall AutoComment) und fügen anstelle dieses Kommentars das Transformationsergebnis ein. In diesem Beispiel ist es sinnvoll, automatisch einen speziellen Haftungsausschluss einzufügen, der den Programmierer warnt, dass der Code in dieser Datei das Ergebnis einer Transformation ist, und es ist nicht sinnvoll, diese Datei direkt zu ändern.



Vorlagencode AutoComment.t4



<#@ include file="AopCsharp.ttinclude" #>

//------------------------------------------------------------------------------
// <auto-generated> 
//     This code was generated from a template.
// 
//     Manual changes to this file may cause unexpected behavior in your application.
//     Manual changes to this file will be overwritten if the code is regenerated.
//
//  Generated base on file: <#= FileName #>
//  ##sha256: <#= FileSha256 #>
//  Created By: <#= User #>
//  Created Machine: <#= MachineName #>
//  Created At: <#= Now #>
//
// </auto-generated>
//------------------------------------------------------------------------------


Die Variablen FileName, FileSha256, User, MachineName und Now werden aus dem Transformationsprozess in die Vorlage exportiert.



Transformationsergebnis



//------------------------------------------------------------------------------
// <auto-generated> 
//     This code was generated from a template.
// 
//     Manual changes to this file may cause unexpected behavior in your application.
//     Manual changes to this file will be overwritten if the code is regenerated.
//
//  Generated base on file: ConsoleDemo.cs
//  ##sha256: PV3lHNDftTzVYnzNCZbKvtHCbscT0uIcHGRR/NJFx20
//  Created By: EuGenie
//  Created Machine: 192.168.0.1
//  Created At: 2017-12-09T14:49:26.7173975-05:00
//
// </auto-generated>
//------------------------------------------------------------------------------


Die folgende Umwandlung wird als Attribut der Klasse



[AopTemplate ("ClassLevelTemplateForMethods", NameFilter = "First")] angegeben.



Dieses Attribut signalisiert, dass die Vorlage auf alle Klassenmethoden angewendet werden soll, die das Wort "First" enthalten. Der NameFilter-Parameter ist ein Muster für reguläre Ausdrücke, mit dem bestimmt wird, welche Methoden in die Transformation einbezogen werden sollen.



Vorlagencode ClassLevelTemplateForMethods.t4



<#@ include file="AopCsharp.ttinclude" #>

// class level template
<#= MethodStart() #><#= MethodBody() #><#= MethodEnd() #>


Dies ist das einfachste Beispiel, das // class level templatevor dem



Transformationsergebnis des Methodencodes einen Kommentar hinzufügt



// class level template
public virtual Person FirstDemo(string firstName, string lastName, int age)
{
  Console.Out.WriteLine("FirstDemo: 1");

  // ##aspect="FirstDemoComment" extra data here

  return new Person()
      {
        FirstName = firstName,
        LastName = lastName,
        Age = age,
      };
}


Die folgenden Transformationen werden als Methodenattribute angegeben, um mehrere Transformationen zu demonstrieren, die auf dieselbe Methode angewendet werden. LogExceptionMethod.t4-Vorlage



[AopTemplate("LogExceptionMethod")]

[AopTemplate("StopWatchMethod")]

[AopTemplate("MethodFinallyDemo", AdvicePriority = 100)]






<#@ include file="AopCsharp.ttinclude" #>
<# EnsureUsing("System"); #>
<#= MethodStart() #>
try
{
<#= MethodBody() #>
} 
catch(Exception logExpn)
{
	Console.Error.WriteLine($"Exception in <#= MethodName #>\r\n{logExpn.Message}\r\n{logExpn.StackTrace}");
	throw;
}

<#= MethodEnd() #>


StopWatchMethod.t4-Vorlage

<#@ include file="AopCsharp.ttinclude" #>
<# EnsureUsing("System.Diagnostics"); #>
<#= MethodStart() #>

var stopwatch = Stopwatch.StartNew(); 

try
{
<#= MethodBody() #>
} 
finally
{
	stopwatch.Stop();
	Console.Out.WriteLine($"Method <#= MethodName #>: {stopwatch.ElapsedMilliseconds}");

}

<#= MethodEnd() #>


MethodFinallyDemo.t4-Vorlage

<#@ include file="AopCsharp.ttinclude" #>

<#= MethodStart() #>
try
{
<#= MethodBody() #>
} 
finally 
{
	// whatever logic you need to include for a method
}

<#= MethodEnd() #>


Ergebnis von Transformationen

public Customer[] SecondDemo(Person[] people)
{
    try
    {
        var stopwatch = Stopwatch.StartNew();
        try
        {
            try
            {
                IEnumerable<Customer> customers;
                Console.Out.WriteLine("SecondDemo: 1");
                {
                    // second demo test extra
                    {
                        customers = people.Select(s => new Customer()
                        {FirstName = s.FirstName, LastName = s.LastName, Age = s.Age, });
                        foreach (var customer in customers)
                        {
                            Console.Out.WriteLine($"SecondDemo: 2 {customer.FirstName} {customer.LastName}");
                        }
                    }
                }

                Console.Out.WriteLine("SecondDemo: 3");
                return customers.ToArray();
            }
            catch (Exception logExpn)
            {
                Console.Error.WriteLine($"Exception in SecondDemo\r\n{logExpn.Message}\r\n{logExpn.StackTrace}");
                throw;
            }
        }
        finally
        {
            stopwatch.Stop();
            Console.Out.WriteLine($"Method SecondDemo: {stopwatch.ElapsedMilliseconds}");
        }
    }
    finally
    {
    // whatever logic you need to include for a method
    }
}


Die folgende Transformation wird für einen Block angegeben, der auf ein using-Konstrukt beschränkt ist



using (new AopTemplate("SecondDemoUsing", extraTag: "test extra"))
{
    customers = people.Select(s => new Customer()
    {
        FirstName = s.FirstName,
        LastName = s.LastName,
        Age = s.Age,
    });

    foreach (var customer in customers)
    {
        Console.Out.WriteLine($"SecondDemo: 2 {customer.FirstName} {customer.LastName}");
    }
}


SecondDemoUsing.t4-Vorlage

<#@ include file="AopCsharp.ttinclude" #>

// second demo <#= ExtraTag #>

<#= StatementBody() #>


ExtraTag ist eine Zeichenfolge, die als Parameter übergeben wird. Dies kann für Generika nützlich sein, die je nach Eingabeparameter ein leicht unterschiedliches Verhalten aufweisen können.



Transformationsergebnis



{
  // second demo test extra
  {
      customers = people.Select(s => new Customer()
      {FirstName = s.FirstName, LastName = s.LastName, Age = s.Age, });
      foreach (var customer in customers)
      {
          Console.Out.WriteLine($"SecondDemo: 2 {customer.FirstName} {customer.LastName}");
      }
  }
}


Die folgende Umwandlung wird durch die Attribute der NotifyPropertyChanged- Klasse angegeben. Dies ist ein klassisches Beispiel, das zusammen mit dem Protokollierungsbeispiel in den meisten Beispielen für aspektorientierte Programmierung angegeben wird.



[AopTemplate("NotifyPropertyChangedClass", Action = AopTemplaceAction.Classes)]

[AopTemplate("NotifyPropertyChanged", Action = AopTemplaceAction.Properties)]








Die Vorlage NotifyPropertyChangedClass.t4 wird auf den Klassencode angewendet
<#@ include file="AopCsharp.ttinclude" #>
<#
	// the class already implements INotifyPropertyChanged, nothing to do here
	if(ImplementsBaseType(ClassNode, "INotifyPropertyChanged", "System.ComponentModel.INotifyPropertyChanged"))
		return null;

	var classNode = AddBaseTypes<ClassDeclarationSyntax>(ClassNode, "System.ComponentModel.INotifyPropertyChanged"); 
#>

<#= ClassStart(classNode) #>
            public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

            protected void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
            {
                PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
            }

<#= ClassBody(classNode) #>
<#= ClassEnd(classNode) #>


.



Fogy
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;

public partial class ModuleWeaver
{
    public void InjectINotifyPropertyChangedInterface(TypeDefinition targetType)
    {
        targetType.Interfaces.Add(new InterfaceImplementation(PropChangedInterfaceReference));
        WeaveEvent(targetType);
    }

    void WeaveEvent(TypeDefinition type)
    {
        var propertyChangedFieldDef = new FieldDefinition("PropertyChanged", FieldAttributes.Private | FieldAttributes.NotSerialized, PropChangedHandlerReference);
        type.Fields.Add(propertyChangedFieldDef);
        var propertyChangedField = propertyChangedFieldDef.GetGeneric();

        var eventDefinition = new EventDefinition("PropertyChanged", EventAttributes.None, PropChangedHandlerReference)
            {
                AddMethod = CreateEventMethod("add_PropertyChanged", DelegateCombineMethodRef, propertyChangedField),
                RemoveMethod = CreateEventMethod("remove_PropertyChanged", DelegateRemoveMethodRef, propertyChangedField)
            };

        type.Methods.Add(eventDefinition.AddMethod);
        type.Methods.Add(eventDefinition.RemoveMethod);
        type.Events.Add(eventDefinition);
    }

    MethodDefinition CreateEventMethod(string methodName, MethodReference delegateMethodReference, FieldReference propertyChangedField)
    {
        const MethodAttributes Attributes = MethodAttributes.Public |
                                            MethodAttributes.HideBySig |
                                            MethodAttributes.Final |
                                            MethodAttributes.SpecialName |
                                            MethodAttributes.NewSlot |
                                            MethodAttributes.Virtual;

        var method = new MethodDefinition(methodName, Attributes, TypeSystem.VoidReference);

        method.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.None, PropChangedHandlerReference));
        var handlerVariable0 = new VariableDefinition(PropChangedHandlerReference);
        method.Body.Variables.Add(handlerVariable0);
        var handlerVariable1 = new VariableDefinition(PropChangedHandlerReference);
        method.Body.Variables.Add(handlerVariable1);
        var handlerVariable2 = new VariableDefinition(PropChangedHandlerReference);
        method.Body.Variables.Add(handlerVariable2);

        var loopBegin = Instruction.Create(OpCodes.Ldloc, handlerVariable0);
        method.Body.Instructions.Append(
            Instruction.Create(OpCodes.Ldarg_0),
            Instruction.Create(OpCodes.Ldfld, propertyChangedField),
            Instruction.Create(OpCodes.Stloc, handlerVariable0),
            loopBegin,
            Instruction.Create(OpCodes.Stloc, handlerVariable1),
            Instruction.Create(OpCodes.Ldloc, handlerVariable1),
            Instruction.Create(OpCodes.Ldarg_1),
            Instruction.Create(OpCodes.Call, delegateMethodReference),
            Instruction.Create(OpCodes.Castclass, PropChangedHandlerReference),
            Instruction.Create(OpCodes.Stloc, handlerVariable2),
            Instruction.Create(OpCodes.Ldarg_0),
            Instruction.Create(OpCodes.Ldflda, propertyChangedField),
            Instruction.Create(OpCodes.Ldloc, handlerVariable2),
            Instruction.Create(OpCodes.Ldloc, handlerVariable1),
            Instruction.Create(OpCodes.Call, InterlockedCompareExchangeForPropChangedHandler),
            Instruction.Create(OpCodes.Stloc, handlerVariable0),
            Instruction.Create(OpCodes.Ldloc, handlerVariable0),
            Instruction.Create(OpCodes.Ldloc, handlerVariable1),
            Instruction.Create(OpCodes.Bne_Un_S, loopBegin), // go to begin of loop
            Instruction.Create(OpCodes.Ret));
        method.Body.InitLocals = true;
        method.Body.OptimizeMacros();

        return method;
    }
}


, AOP .Net


NotifyPropertyChanged.t4-Vorlage, die auf Klasseneigenschaften angewendet wird
<#@ include file="AopCsharp.ttinclude" #>
<#
 	if(!(PropertyHasEmptyGetBlock() && PropertyHasEmptySetBlock()))
		return null;

	string privateUnqiueName = GetUniquePrivatePropertyName(ClassNode, PropertyNode.Identifier.ToString());
#>

	private <#= PropertyNode.Type.ToFullString() #> <#= privateUnqiueName #><#= PropertyNode.Initializer != null ? " = " + PropertyNode.Initializer.ToFullString() : "" #>;

<#= PropertyNode.AttributeLists.ToFullString() + PropertyNode.Modifiers.ToFullString() + PropertyNode.Type.ToFullString() + PropertyNode.Identifier.ToFullString() #>
	{
		get { return <#= privateUnqiueName #>; }
		set 
		{
			if(<#= privateUnqiueName #> != value)
			{
				<#= privateUnqiueName #> = value;
				NotifyPropertyChanged();
			}
		}
	}


Ursprünglicher Code der Klasse und Eigenschaften

public class Person
{
    public int Id { get; set; }

// ...
}


Transformationsergebnis

public class Person : System.ComponentModel.INotifyPropertyChanged
{
    public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
    protected void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
    }

    private int _id;
    public int Id
    {
        get
        {
            return _id;
        }

        set
        {
            if (_id != value)
            {
                _id = value;
                NotifyPropertyChanged();
            }
        }
    }

// ...
}


Ein Beispiel für eine Vorlage zum Zwischenspeichern von Eigenschaftsergebnissen, die durch die



[AopTemplate("CacheProperty", extraTag: "{ \"CacheKey\": \"name_of_cache_key\", \"ExpiresInMinutes\": 10 }")]



Vorlagenparameter angegeben wird, wird als JSON- Attribut angegeben . Wenn keine expliziten Parameter vorhanden sind, werden die Standardparameter verwendet.



CacheProperty.t4-Vorlage
<#@ include file="AopCsharp.ttinclude" #>
<#
	// The template accepts a configuration value from extraTag in two ways
	// 1. as a number of minutes to use for expiration (example: 8)
	// 2. as a string in JSON in format { CacheKey: "name_of_cache_key", CacheKeyVariable: "name_of_variable", ExpiresInMinutes: 10, ExpiresVariable: "name_of_variable" }
	//
	//    CacheKey (optional) name of the cache key, the name will be used as a literal string (example: my_key)
	//    CacheKeyVariable (optional) name of variable that holds the cache key (example: GlobalConsts.MyKeyName)
	//
	//    ExpiresInMinutes (optional) number minutes that the cache value will expires (example: 12)
	//    ExpiresVariable (optional) name of a variable that the expiration value will be get from (example: AppConfig.EXPIRE_CACHE)
	//
	// if any of expiration values are not specified, 5 minutes default expiration will be used

	if(!PropertyHasAnyGetBlock())
		return null;

	const int DEFAULT_EXPIRES_IN_MINUTES = 5;

	string propertyName = PropertyNode.Identifier.ToFullString().Trim();
	string propertyType = PropertyNode.Type.ToFullString().Trim();
	string expiresInMinutes = DEFAULT_EXPIRES_IN_MINUTES.ToString();
	string cacheKey = "\"" + ClassNode.Identifier.ToFullString() + ":" + propertyName + "\"";

	if(!String.IsNullOrEmpty(ExtraTag))
	{
		if(Int32.TryParse(ExtraTag, out int exp))
		{
			expiresInMinutes = exp.ToString();
		}
		else
		{
			JsonDocument json = ExtraTagAsJson();
			if(json != null && json.RootElement.ValueKind  == JsonValueKind.Object)
			{
				if(json.RootElement.TryGetProperty("CacheKey", out JsonElement cacheKeyElement))
				{
					string s = cacheKeyElement.GetString();
					if(!String.IsNullOrEmpty(s))
						cacheKey = "\"" + s + "\"";
				}
				else if(json.RootElement.TryGetProperty("CacheKeyVariable", out JsonElement cacheVariableElement))
				{
					string s = cacheVariableElement.GetString();
					if(!String.IsNullOrEmpty(s))
						cacheKey = s;
				}

				if(json.RootElement.TryGetProperty("ExpiresInMinutes", out JsonElement expiresInMinutesElement))
				{
					if(expiresInMinutesElement.TryGetInt32(out int v) && v > 0)
						expiresInMinutes = "" + v;
				} 
				else if(json.RootElement.TryGetProperty("ExpiresVariable", out JsonElement expiresVariableElement))
				{				
					string s = expiresVariableElement.GetString();
					if(!String.IsNullOrEmpty(s))
						expiresInMinutes = s;
				}
			}
		}
	}

#>


<#= PropertyDefinition() #>
	{
		get 
		{ 
			System.Runtime.Caching.ObjectCache cache = System.Runtime.Caching.MemoryCache.Default;			

			<#= propertyType #> cachedData = cache[<#= cacheKey #>] as <#= propertyType #>;
			if(cachedData == null)
			{
				cachedData = GetPropertyData();
				if(cachedData != null)
				{					
					cache.Set(<#= cacheKey #>, cachedData, System.DateTimeOffset.Now.AddMinutes(<#= expiresInMinutes #>)); 
				}
			}

			return cachedData;

			<#= propertyType #> GetPropertyData()
			{
				<# if(PropertyNode.ExpressionBody != null ) { #>
				return (<#= PropertyNode.ExpressionBody.Expression.ToFullString() #>);
				<# } else if(PropertyNode?.AccessorList?.Accessors.FirstOrDefault(w => w.ExpressionBody != null && w.Keyword.ToString() == "get") != null) { #>
				return (<#= PropertyNode?.AccessorList?.Accessors.FirstOrDefault(w => w.ExpressionBody != null && w.Keyword.ToString() == "get").ExpressionBody.Expression.ToFullString() #>);
				<# } else { #>
				<#= PropertyGetBlock() #>
				<# } #>
			}
       }

		<#
		
		if(PropertyHasAnySetBlock()) { #>
		set 
		{
			System.Runtime.Caching.ObjectCache cache = System.Runtime.Caching.MemoryCache.Default;  

			cache.Remove(<#= cacheKey #>); // invalidate cache for the property		
			
			<#= PropertySetBlock() #>			
		}
		<# } #>

	}


Quelle

[AopTemplate("CacheProperty", extraTag: "{ \"CacheKey\": \"name_of_cache_key\", \"ExpiresInMinutes\": 10 }")]
public string FullName
{
    get
    {
        return $"{FirstName} {LastName}";
    }
}


Transformationsergebnis für CacheProperty.t4

public string FullName
{
    get
    {
        System.Runtime.Caching.ObjectCache cache = System.Runtime.Caching.MemoryCache.Default;
        string cachedData = cache["name_of_cache_key"] as string;
        if (cachedData == null)
        {
            cachedData = GetPropertyData();
            if (cachedData != null)
            {
                cache.Set("name_of_cache_key", cachedData, System.DateTimeOffset.Now.AddMinutes(10));
            }
        }

        return cachedData;
        string GetPropertyData()
        {
            // FullNameComment FullName
            return $"{FirstName} {LastName}";
        }
    }
}


Der nächste Aufruf der Vorlage erneut aus dem Kommentar

// ##aspect="FullNameComment" extra data here


FullNameComment.t4 Vorlage

<#@ include file="AopCsharp.ttinclude" #>

// FullNameComment <#= PropertyNode.Identifier #>


Sehr ähnlich der Vorlage AutoComment.t4, aber hier demonstrieren wir die Verwendung von PropertyNode. Die Daten "Zusätzliche Daten hier" stehen der Vorlage "FullNameComment.t4" über den Parameter "ExtraTag" zur Verfügung (in diesem Beispiel werden sie jedoch nicht verwendet, sodass sie einfach ignoriert werden).



Transformationsergebnis

// FullNameComment FullName


Die folgende Umwandlung in der Datei, die durch das



[AopTemplate("NotifyPropertyChanged", Action = AopTemplaceAction.Properties)]



AND- Attribut angegeben wird, ist identisch mit der der Person-Klasse. Der Quellcode für die Vorlage NotifyPropertyChanged.t4 wurde bereits oben aufgenommen.



Transformationsergebnis

public class Customer : Person
{
    private double _creditScore;
    public double CreditScore
    {
        get
        {
            return _creditScore;
        }

        set
        {
            if (_creditScore != value)
            {
                _creditScore = value;
                NotifyPropertyChanged();
            }
        }
    }
}


Letzter Teil



Obwohl sich dieser Artikel auf die aspektorientierte Programmierung konzentriert, ist die Quellcode-Transformationstechnik universell und kann im Prinzip für Aufgaben verwendet werden, die nicht mit AOP zusammenhängen.



Zum Beispiel kann es zur Abhängigkeitsinjektion verwendet werden, d.h. Wir ändern den Code zur Ressourcenerstellung in Abhängigkeit von den Build-Parametern.



DependencyInjection.t4-Vorlage
<#@ include file="AopCsharp.ttinclude" #>
<#
	var syntaxNode = FieldsInjection(SyntaxNode);
	syntaxNode = VariablesInjection(syntaxNode);
	syntaxNode = PropertiesInjection(syntaxNode);	

	if(syntaxNode == SyntaxNode)
		return null;
#>

<#= syntaxNode.ToFullString() #>

<#+
	private SyntaxNode VariablesInjection(SyntaxNode syntaxNode)
	{
		return RewriteNodes<LocalDeclarationStatementSyntax >(syntaxNode, OnLocalVariablesInjection);	
	
		SyntaxNode OnLocalVariablesInjection(LocalDeclarationStatementSyntax node)
		{
			var errorMsgs = new System.Text.StringBuilder();

			SyntaxNode syntaxNode = RewriteNodes<VariableDeclaratorSyntax>(node, (n) => OnVariableDeclaratorVisit(n, node.Declaration.Type, errorMsgs));

			if(errorMsgs.Length > 0)
				return AddErrorMessageTrivia(syntaxNode, errorMsgs.ToString());

			return syntaxNode;
		}
	}

	private SyntaxNode PropertiesInjection(SyntaxNode syntaxNode)
	{
		return RewriteNodes<PropertyDeclarationSyntax>(syntaxNode, OnPropertyInjection);	
	
		SyntaxNode OnPropertyInjection(PropertyDeclarationSyntax node)
		{
			if(node.Initializer?.Value?.ToString() != "inject")
				return node;

			var errorMsgs = new System.Text.StringBuilder();

			SyntaxNode syntaxNode = DoInjection(node, node.Identifier.ToString().Trim(), node.Initializer.Value, node.Type, errorMsgs);

			if(errorMsgs.Length > 0)
				return AddErrorMessageTrivia(syntaxNode, errorMsgs.ToString());

			return syntaxNode;
		}
	}

	private SyntaxNode FieldsInjection(SyntaxNode syntaxNode)
	{
		return RewriteNodes<BaseFieldDeclarationSyntax>(syntaxNode, OnFieldsInjection);	
	
		SyntaxNode OnFieldsInjection(BaseFieldDeclarationSyntax node)
		{
			var errorMsgs = new System.Text.StringBuilder();

			SyntaxNode syntaxNode = RewriteNodes<VariableDeclaratorSyntax>(node, (n) => OnVariableDeclaratorVisit(n, node.Declaration.Type, errorMsgs));

			if(errorMsgs.Length > 0)
				return AddErrorMessageTrivia(syntaxNode, errorMsgs.ToString());

			return syntaxNode;
		}
	}

	private SyntaxNode OnVariableDeclaratorVisit(VariableDeclaratorSyntax node, TypeSyntax typeSyntax, System.Text.StringBuilder errorMsgs)
	{
		if(node.Initializer?.Value?.ToString() != "inject")
			return node;

		return DoInjection(node, node.Identifier.ToString().Trim(), node.Initializer.Value, typeSyntax, errorMsgs);
	}

	private SyntaxNode DoInjection(SyntaxNode node, string varName, ExpressionSyntax initializerNode, TypeSyntax typeSyntax, System.Text.StringBuilder errorMsgs)
	{		
		string varType = typeSyntax.ToString().Trim();

		Log($"{varName} {varType} {initializerNode.ToString()}");

		if(varName.StartsWith("config"))
		{
			string configName = Regex.Replace(Regex.Replace(varName, "^config", ""), "([a-z])([A-Z])", (m) => m.Groups[1].Value + "_" + m.Groups[2].Value).ToLower();
			ExpressionSyntax configNode = CreateElementAccess("_configuration", CreateStringLiteral(configName));

			if(varType == "int")
			{
				configNode = CreateMemberAccessInvocation("Int32", "Parse", configNode);
			}

			return node.ReplaceNode(initializerNode, configNode);
		}

		switch(varType)
		{
			case "Microsoft.Extensions.Configuration.IConfigurationRoot":
			case "IConfigurationRoot":
				EnsureUsing("Microsoft.Extensions.Configuration");

				ExpressionSyntax pathCombineArg = CreateMemberAccessInvocation("System.IO.Path", "Combine", CreateMemberAccess("AppContext", "BaseDirectory"));

				ExpressionSyntax builderNode = CreateNewType("ConfigurationBuilder").WithTrailingTrivia(SyntaxFactory.EndOfLine("\r\n"));
				builderNode  = CreateMemberAccessInvocation(builderNode, "SetBasePath", pathCombineArg).WithTrailingTrivia(SyntaxFactory.EndOfLine("\r\n"));

				ExpressionSyntax addJsonFileArg = CreateMemberAccessInvocation("System.IO.Path", "Combine", CreateMemberAccess("AppContext", "BaseDirectory"));

				builderNode  = CreateMemberAccessInvocationNamedArgs(builderNode, "AddJsonFile", 
																		(null, CreateStringLiteral("appsettings.json")), 
																		("optional",  SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression))).WithTrailingTrivia(SyntaxFactory.EndOfLine("\r\n"));

				if(GetGlobalSetting("env")?.ToLower() == "test")
				{
					builderNode  = CreateMemberAccessInvocationNamedArgs(builderNode, "AddJsonFile", 
																			(null, CreateStringLiteral("appsettings.test.json")), 
																			("optional",  SyntaxFactory.LiteralExpression(SyntaxKind.FalseLiteralExpression)));
				}

				builderNode  = CreateMemberAccessInvocation(builderNode, "Build");

				return node.ReplaceNode(initializerNode, builderNode);
				
			case "IDataService":
			{
				string className = (GetGlobalSetting("env")?.ToLower() == "test" ? "MockDataService" : "DataService");

				return node.ReplaceNode(initializerNode, CreateNewType(className));
			}
		}

		errorMsgs.AppendLine($"Cannot find injection rule for {varType} {varName}");

		return node;
	}

#>




Im Quellcode (hier wird die dynamische Variablenfunktion verwendet, mit der sie beliebigen Typen zugewiesen werden können), d.h. Aus Gründen der Ausdruckskraft haben wir uns ein neues Schlüsselwort ausgedacht.

private static IConfigurationRoot _configuration = inject;
private IDataService _service { get; } = inject;
// ...
public Customer[] SecondDemo(Person[] people)
{
     int configDelayMS = inject; // we are going to inject dependency to local variables
     string configServerName = inject;
}
// ...
protected static dynamic inject;


Bei der Transformation wird der Vergleich GetGlobalSetting ("env") == "test" verwendet. Abhängig von dieser Bedingung wird entweder neuer DataService () oder neuer MockDataService () injiziert.



Transformationsergebnis


private static IConfigurationRoot _configuration = new ConfigurationBuilder()
    .SetBasePath(System.IO.Path.Combine(AppContext.BaseDirectory))
    .AddJsonFile("appsettings.json", optional: true)
    .Build();

private IDataService _service { get; } = new DataService();
// ...
public Customer[] SecondDemo(Person[] people)
{
       int configDelayMS = Int32.Parse(_configuration["delay_ms"]);
       string configServerName = _configuration["server_name"];
}
// ...


Oder Sie können dieses Tool als statische Analyse für "arme Leute" verwenden (aber es ist viel, viel korrekter, Analysatoren mit der nativen Roslyn-Funktionalität zu implementieren). Wir analysieren den Code für unsere Regeln und fügen ihn in den Quellcode ein.



#error our error message here



Dies führt zu einem Fehler bei der Kompilierung.



#warning our warning message here



Dies dient als Warnung in der IDE oder beim Kompilieren.



StaticAnalyzer.t4-Vorlage
<#@ include file="AopCsharp.ttinclude" #>
<#
	var syntaxNode = AnalyzeLocalVariables(SyntaxNode);
	syntaxNode = AnalyzeStringFormat(syntaxNode);	

	if(syntaxNode == SyntaxNode)
		return null;
#>

<#= syntaxNode.ToFullString() #>

<#+

	private SyntaxNode AnalyzeLocalVariables(SyntaxNode syntaxNode)
	{
		return RewriteNodes<LocalDeclarationStatementSyntax>(syntaxNode, OnAnalyzeLocalVariablesNodeVisit);	
	
		SyntaxNode OnAnalyzeLocalVariablesNodeVisit(LocalDeclarationStatementSyntax node)
		{
			var errorMsgs = new System.Text.StringBuilder();
			
			string d = "";
			foreach(VariableDeclaratorSyntax variableNode in node.DescendantNodes().OfType<VariableDeclaratorSyntax>().Where(w => Regex.IsMatch(w.Identifier.ToString(), "^[A-Z]")))
			{
				LogDebug($"variable: {variableNode.Identifier.ToString()}");

				errorMsgs.Append(d + $"variable \"{variableNode.Identifier.ToString()}\" doesn't match code standard rules");
				d = ", ";
			}

			if(errorMsgs.Length > 0)
				return AddErrorMessageTrivia(node, errorMsgs.ToString());

			return node;
		}
	}


	private SyntaxNode AnalyzeStringFormat(SyntaxNode syntaxNode)
	{
		return RewriteLeafStatementNodes(syntaxNode, OnAnalyzeStringFormat);	
	
		SyntaxNode OnAnalyzeStringFormat(StatementSyntax node)
		{
			bool hasStringFormat = false;

			foreach(MemberAccessExpressionSyntax memberAccessNode in node.DescendantNodes().OfType<MemberAccessExpressionSyntax>())
			{
				if(memberAccessNode.Name.ToString().Trim() != "Format")
					continue;

				string expr = memberAccessNode.Expression.ToString().Trim().ToLower();
				if(expr != "string" && expr != "system.string")
					continue;

				hasStringFormat = true;
				break;
			}

			if(hasStringFormat)
				return AddWarningMessageTrivia(node, "Please replace String.Format with string interpolation format.");

			return node;
		}
	}
#>




Transformationsergebnis

#error variable "Customers" doesn't match code standard rules
IEnumerable<Customer> Customers;
// ...
#warning Please replace String.Format with string interpolation format.
Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));


Oder als automatisches Werkzeug zum Lokalisieren einer Anwendung, d.h. Suchen Sie alle Zeichenfolgen in den Klassen und ersetzen Sie sie durch die Verwendung der entsprechenden Ressourcen.



ResourceReplacer.t4-Vorlage
<#@ include file="AopCsharp.ttinclude" #>
<#

	Dictionary<string, string> options = ExtraTagAsDictionary();
	_resources = LoadResources(options["ResourceFile"]);
	_resourceClass = options["ResourceClass"];

	var syntaxNode = RewriteLeafStatementNodes(SyntaxNode, OnStatementNodeVisit);	
#>

<#= syntaxNode.ToFullString() #>

<#+ 
	private SyntaxNode OnStatementNodeVisit(StatementSyntax node)
	{
		if(!node.DescendantNodes().OfType<InvocationExpressionSyntax>().Any(w => (w.Expression is IdentifierNameSyntax) && ((IdentifierNameSyntax)w.Expression).Identifier.ToString() == "i18"  ))
			return node;

		var errorMsgs = new System.Text.StringBuilder();

		SyntaxNode syntaxNode = RewriteNodes<InvocationExpressionSyntax>(node, (n) => OnInvocationExpressionVisit(n, errorMsgs));

		if(errorMsgs.Length > 0)
			return AddErrorMessageTrivia(syntaxNode, errorMsgs.ToString());

		return syntaxNode;
	}

    private SyntaxNode OnInvocationExpressionVisit(InvocationExpressionSyntax node, System.Text.StringBuilder errorMsgs)
	{
		if(!(node.Expression is IdentifierNameSyntax && ((IdentifierNameSyntax)node.Expression).Identifier.ToString() == "i18"  ))
			return node;

		ArgumentSyntax arg = node.ArgumentList.Arguments.Single(); // We know that i18 method accepts only one argument. Keep in mind that it is just a demo and in real life you could be more inventive
		
		var expr = arg.Expression;
		if(!(expr is LiteralExpressionSyntax || expr is InterpolatedStringExpressionSyntax))
		{
			errorMsgs.AppendLine($"Argument for i18 method must be either string literal or interpolated string, but instead got {arg.Expression.GetType().ToString()}");

			return node;
		}
		
		string s = expr.ToString();
		if(s.StartsWith("$"))
		{
			(string format, List<ExpressionSyntax> expressions) = ConvertInterpolatedStringToFormat((InterpolatedStringExpressionSyntax)expr);

			ExpressionSyntax stringNode = ReplaceStringWithResource("\"" + format + "\"", errorMsgs);
			if(stringNode != null)
			{
				var memberAccess = CreateMemberAccess("String", "Format");
			
				var arguments = new List<ArgumentSyntax>();
	
				arguments.Add(SyntaxFactory.Argument(stringNode));
				expressions.ForEach(item => arguments.Add(SyntaxFactory.Argument(item)));

				var argumentList = SyntaxFactory.SeparatedList(arguments);

				return SyntaxFactory.InvocationExpression(memberAccess, SyntaxFactory.ArgumentList(argumentList));
			}
		}
		else
		{
			SyntaxNode stringNode = ReplaceStringWithResource(s, errorMsgs);
			if(stringNode != null)
				return stringNode;
		}

		return node;
	}

	private ExpressionSyntax ReplaceStringWithResource(string s, System.Text.StringBuilder errorMsgs)
	{
		Match m = System.Text.RegularExpressions.Regex.Match(s, "^\"(\\s*)(.*?)(\\s*)\"$");
		if(!m.Success)
		{
			errorMsgs.AppendLine($"String doesn't match search criteria");

			return null;
		}

		if(!_resources.TryGetValue(m.Groups[2].Value, out string resourceName))
		{

			errorMsgs.AppendLine($"Cannot find resource for a string {s}, please add it to resources");
			return null;
		}

		string csharpName = Regex.Replace(resourceName, "[^A-Za-z0-9]", "_");

		ExpressionSyntax stringNode = CreateMemberAccess(_resourceClass, csharpName);

		if(!String.IsNullOrEmpty(m.Groups[1].Value) || !String.IsNullOrEmpty(m.Groups[3].Value))
		{
			if(!String.IsNullOrEmpty(m.Groups[1].Value))
			{
				stringNode = SyntaxFactory.BinaryExpression(SyntaxKind.AddExpression, 
																CreateStringLiteral(m.Groups[1].Value), 
																stringNode);
			}

			if(!String.IsNullOrEmpty(m.Groups[3].Value))
			{
				stringNode = SyntaxFactory.BinaryExpression(SyntaxKind.AddExpression, 
															stringNode, 
															CreateStringLiteral(m.Groups[3].Value));
			}

			stringNode = SyntaxFactory.ParenthesizedExpression(stringNode);
		}

		return stringNode;
	}	

	private string _resourceClass;
	private Dictionary<string,string> _resources;
#>




Quelle


Console.Out.WriteLine(i18("SecondDemo: i18"));
// ...
Console.Out.WriteLine(i18($"First Name {customer.FirstName} Last Name {customer.LastName}"));

Console.Out.WriteLine("SecondDemo: 2 " + i18("First Name ") + customer.FirstName + i18(" Last Name   ") + customer.LastName);
// ...
 Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));
// ...
protected static string i18(string s) => s;


In der Ressourcendatei Demo.resx haben wir beispielsweise die folgenden Zeilen erstellt

<data name="First Last Names Formatted" xml:space="preserve">
  <value>First Name {0} Last Name {1}</value>
</data>
<data name="First Name" xml:space="preserve">
    <value>First Name</value>
</data>
<data name="Last Name" xml:space="preserve">
  <value>Last Name</value>
</data>


und den automatisch generierten Code der Datei Demo.Designer.cs
public class Demo 
{
// ...

    public static string First_Last_Names_Formatted
    {
        get
        {
            return ResourceManager.GetString("First Last Names Formatted", resourceCulture);
        }
    }

    public static string First_Name
    {
        get
        {
            return ResourceManager.GetString("First Name", resourceCulture);
        }
    }

    public static string Last_Name
    {
        get
        {
            return ResourceManager.GetString("Last Name", resourceCulture);
        }
    }
}


Transformationsergebnis (Beachten Sie, dass die interpolierte Zeichenfolge durch String.Format ersetzt wurde und die Ressource "Vorname {0} Nachname {1}" verwendet wurde). Für Zeilen, die nicht in der Ressourcendatei vorhanden sind oder nicht unserem Format entsprechen, wird eine Fehlermeldung hinzugefügt

//#error Cannot find resource for a string "SecondDemo: i18", please add it to resources
Console.Out.WriteLine(i18("SecondDemo: i18"));
// ...
Console.Out.WriteLine(String.Format(Demo.First_Last_Names_Formatted, customer.FirstName, customer.LastName));

Console.Out.WriteLine("SecondDemo: 2 " + (Demo.First_Name + " ") + customer.FirstName + (" " + Demo.Last_Name + "   ") + customer.LastName);
// ...
//#error Argument for i18 method must be either string literal or interpolated string, but instead got Microsoft.CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax
Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));


Darüber hinaus können Sie mit dem Transformationstool nicht nur mit C # -Dateien, sondern auch mit jedem Dateityp arbeiten (natürlich mit bestimmten Einschränkungen). Wenn Sie einen Parser haben, der einen AST für Ihre Sprache erstellen kann, können Sie Roslyn durch diesen Parser ersetzen, die Implementierung des Code-Handlers optimieren und es wird funktionieren. Leider gibt es eine sehr begrenzte Anzahl von Bibliotheken mit Funktionen in der Nähe von Roslyn, und ihre Verwendung erfordert viel mehr Aufwand. Zusätzlich zu C # verwenden wir Transformationen für JavaScript- und TypeScript-Projekte, aber sicherlich nicht so umfassend wie für C #.



Ich wiederhole noch einmal, dass der Beispielcode und die Vorlagen zur Veranschaulichung der Möglichkeiten eines solchen Ansatzes dienen und, wie sie sagen, der Himmel die Grenze ist.



Vielen Dank für Ihre Zeit.



Der Hauptteil dieses Artikels wurde vor ein paar Jahren geschrieben, aber leider war es aus irgendeinem Grund möglich, ihn erst jetzt zu veröffentlichen.



Unser ursprüngliches Tool wurde auf dem .Net Framework entwickelt, aber wir haben mit der Arbeit an einer vereinfachten Open Source-Version unter der MIT-Lizenz für .Net Core begonnen. Im Moment ist das Ergebnis voll funktionsfähig und zu 90% fertig, es gibt geringfügige Verbesserungen, die Frisur des Codes, die Erstellung von Dokumentationen und Beispielen, aber ohne all dies wird es schwierig sein, in das Projekt einzusteigen, die Idee selbst wird kompromittiert und DX wird negativ sein.



Die Person, die an der Erstellung des Projekts gearbeitet hat, konnte es nicht beenden, bevor sie in ein anderes Unternehmen gewechselt ist. Bevor wir also Ressourcen zuweisen, um die Arbeit fortzusetzen, möchten wir die Reaktion der Community untersuchen, da wir verstehen, dass das, was in unserem Fall geeignet ist, nicht unbedingt gefragt und durchaus möglich ist. dass diese Nische durch ein alternatives Werkzeug oder einen alternativen Entwicklungsansatz gefüllt wird.



Die Idee des Tools ist sehr einfach und der Entwickler hat insgesamt etwa einen Monat für die Implementierung einer funktionsfähigen Version aufgewendet. Ich denke, dass ein Programmierer mit guten Qualifikationen und Erfahrungen mit Roslyn in wenigen Tagen seine eigene spezifische Version erstellen kann. Derzeit beträgt die Größe des Projektquellcodes einschließlich Beispielen und Vorlagen nur etwa 150 KB.



Ich würde mich über konstruktive Kritik freuen (nicht konstruktive Kritik wird mich auch nicht verärgern, also zögern Sie nicht).



Vielen Dank an Phil Rangin (fillpackart) zur Motivation beim Schreiben des Artikels. Channel "We Are Doomed" Regeln!



All Articles