Dezember 2016

Band 31, Nummer 13

Roslyn: Generieren von JavaScript mit Roslyn und T4-Vorlagen

Von Nick Harrison | Dezember 2016

Neulich hat mir meine Tochter etwas Witziges über eine Unterhaltung zwischen einem Smartphone und einem Feature Phone erzählt. In etwa so: Was hat das Smartphone zum Feature Phone gesagt? „Ich komme aus der Zukunft. Kannst Du mich verstehen?” Manchmal fühlt es sich genau so an, wenn man etwas wirklich Neues lernt. Roslyn kommt auch aus der Zukunft und kann auf den ersten Blick schwer zu verstehen sein.

In diesem Artikel beschäftige ich mich mit Roslyn unter einem Aspekt, der vielleicht bisher nicht die verdiente Aufmerksamkeit erregt hat. Ich schaue mir Roslyn als Quelle von Metadaten zum Generieren von JavaScript mit T4 an. Dabei werden die Workspace-API, ein Teil der Syntax-API, die Symbol-API und eine Runtimevorlage aus dem T4-Modul verwendet. Der tatsächlich generierte JavaScript-Code spielt beim Verstehen der zum Erfassen der Metadaten verwendeten Prozesse nur eine Nebenrolle.

Da Roslyn auch einige nette Optionen zum Generieren von Code bereitstellt, denken Sie möglicherweise, dass die beiden Technologien einen Konflikt verursachen und nicht gut zusammen funktionieren. Technologien verursachen häufig einen Konflikt, wenn sich ihre Sandkästen überschneiden. Das Zusammenspiel dieser beiden Technologien ist jedoch recht gut.

Moment mal, was ist T4?

Wenn T4 neu für Sie ist, finden Sie alle benötigten Hintergrundinformationen in einem E-Buch der Syncfusion Succinctly-Serie aus dem Jahr 2015 mit dem Titel „T4 Succinctly“ (bit.ly/2cOtWuN).

Im Moment reicht es zu wissen, dass es sich bei T4 um das vorlagenbasierte Texttransformations-Toolkit von Microsoft handelt. Sie geben Metadaten in die Vorlage ein, und der Text wird zum gewünschten Code. Es ist sogar so, dass keine Einschränkung auf Code vorliegt. Sie können jeden beliebigen Texttyp generieren. Quellcode ist aber der am häufigsten verwendete Ausgabetyp. Sie können HTML, SQL, Textdokumentation, Visual Basic .NET, C# oder jede andere textbasierte Ausgabe generieren.

Schauen Sie sich Abbildung 1 an. Sie zeigt ein einfaches Konsolenanwendungsprogramm. In Visual Studio habe ich eine neue Runtimetextvorlage namens „AngularResourceService.tt“ hinzugefügt. Der Vorlagencode generiert automatisch C#-Code, der die Vorlage zur Laufzeit implementiert. Dies wird im Konsolenfenster angezeigt.

Verwenden von T4 für die Codegenerierung zur Entwurfszeit
Abbildung 1: Verwenden von T4 für die Codegenerierung zur Entwurfszeit

In diesem Artikel zeige ich, wie Roslyn zum Erfassen von Metadaten aus einem Web-API-Projekt verwendet wird, die dann als Eingabe für T4 dienen, um eine JavaScript-Klasse zu generieren. Anschließend wird Roslyn zum Hinzufügen dieses JavaScript-Codes zur Projektmappe verwendet.

Abbildung 2 zeigt das Konzept dieses Prozessflusses.

T4-Prozessfluss
Abbildung 2: T4-Prozessfluss

Roslyn als Datenquelle für T4

Für das Generieren von Code sind zahlreiche Metadaten erforderlich. Sie benötigen Metadaten zum Beschreiben des Codes, der generiert werden soll. Reflektion, das Codemodell und das Datenwörterbuch sind häufig verwendete Quellen für bereits verfügbare Metadaten. Roslyn kann alle Metadaten bereitstellen, die von Reflektion oder dem Codemodell empfangen würden, jedoch ohne einige der Probleme, die mit diesen Ansätzen entstehen.

In diesem Artikel verwende ich Roslyn zum Ermitteln von Klassen, die von „ApiController“ abgeleitet sind. Ich verwende die T4-Vorlage zum Erstellen einer JavaScript-Klasse für jeden Controller und stelle eine Methode für jede Aktion und eine Eigenschaft für jede Eigenschaft im ViewModel bereit, das dem Controller zugeordnet ist. Das Ergebnis ähnelt dem Code in Abbildung 3.

Abbildung 3: Ergebnis der Codeausführung

var app = angular.module("challenge", [ "ngResource"]);
  app.factory(ActivitiesResource , function ($resource) {
    return $resource(
      'http://localhost:53595//Activities',{Activities : '@Activities'},{
    Id : "",
    ActivityCode : "",
    ProjectId : "",
    StartDate : "",
    EndDate : "",
  , get: {
      method: "GET"
    }
  , put: {
      method: "PUT"
    }
  , post: {
      method: "POST"
    }
  , delete: {
      method: "DELETE"
    }
  });
});

Erfassen der Metadaten

Ich beginne mit dem Erfassen der Metadaten, indem ich ein neues Konsolenanwendungsprojekt in Visual Studio 2015 erstelle. In diesem Projekt erfasst eine Klasse Metadaten mit Roslyn. Eine T4-Vorlage ist ebenfalls vorhanden. Dabei handelt es sich um eine Runtimevorlage, die JavaScript-Code basierend auf den erfassten Metadaten generiert.

Nachdem das Projekt erstellt wurde, werden die folgenden Befehle über die Paket-Manager-Konsole ausgegeben:

Install-Package Microsoft.CodeAnalysis.CSharp.Workspaces

Auf diese Weise wird sichergestellt, dass der aktuelle Roslyn-Code für den CSharp-Compiler und verwandte Dienste verwendet wird.

Ich positioniere den Code für die verschiedenen Methoden in einer neuen Klasse namens „RoslynDataProvider“. Ich werde mich in diesem Artikel immer wieder auf diese Klasse beziehen. Sie dient als praktische Referenz, wann immer ich Metadaten mit Roslyn erfassen möchte.

Ich verwende „MSBuildWorkspace“ zum Abrufen eines Arbeitsbereichs, der den gesamten für die Kompilierung benötigten Kontext bereitstellt. Sobald die Projektmappe bereit ist, kann ich auf einfache Weise die Projekte durchlaufen und nach dem WebApi-Projekt suchen:

private Project GetWebApiProject()
{
  var work = MSBuildWorkspace.Create();
  var solution = work.OpenSolutionAsync(PathToSolution).Result;
  var project = solution.Projects.FirstOrDefault(p =>
    p.Name.ToUpper().EndsWith("WEBAPI"));
  if (project == null)
    throw new ApplicationException(
      "WebApi project not found in solution " + PathToSolution);
  return project;
}

Wenn Sie eine andere Benennungskonvention verwenden, können Sie diese problemlos in „GetWebApiProject“ integrieren, um nach dem Projekt zu suchen, an dem Sie interessiert sind.

Da ich jetzt weiß, mit welchem Projekt ich arbeiten möchte, muss ich die Kompilierung für dieses Projekt sowie einen Verweis auf den Typ abrufen, den ich zum Identifizieren der jeweiligen Controller verwende. Ich benötige die Kompilierung, weil ich das „SemanticModel“ zum Ermitteln verwende, ob eine Klasse von „System.Web.Http.ApiController“ abgeleitet ist. Aus dem Projekt kann ich die im Projekt enthaltenen Dokumente abrufen. Jedes Dokument ist eine separate Datei, die ggf. mehrere Klassendeklarationen enthält. Eine bewährte Methode besteht jedoch darin, nur eine Klasse in jeder Datei und übereinstimmende Namen für die Datei und die Klasse zu verwenden. Doch nicht jeder Entwickler folgt immer diesem Standard.

Ermitteln der Controller

Abbildung 4 zeigt, wie nach allen Klassendeklarationen in jedem Dokument gesucht und ermittelt wird, ob die Klasse von „ApiController“ abgeleitet ist.

Abbildung 4: Suchen nach den Controllern in einem Projekt

public IEnumerable<ClassDeclarationSyntax> FindControllers(Project project)
{
  compilation = project.GetCompilationAsync().Result;
  var targetType = compilation.GetTypeByMetadataName(
    "System.Web.Http.ApiController");
  foreach (var document in project.Documents)
  {
    var tree = document.GetSyntaxTreeAsync().Result;
    var semanticModel = compilation.GetSemanticModel(tree);
    foreach (var type in tree.GetRoot().DescendantNodes().
      OfType<ClassDeclarationSyntax>()
      .Where(type => GetBaseClasses(semanticModel, type).Contains(targetType)))
    {
      yield return type;
    }
  }
}

Da die Kompilierung Zugriff auf alle Verweise besitzt, die zum Kompilieren des Projekts erforderlich sind, treten beim Auflösen des Zieltyps keine Probleme auf. Nach dem Abrufen des Kompilierungsobjekts habe ich mit der Kompilierung des Projekts begonnen. Nach einer Weile werde ich jedoch unterbrochen, sobald die Details zum Abrufen der erforderlichen Metadaten vorliegen.

Abbildung 5 zeigt die GetBaseClasses-Methode, die die Schwerstarbeit der Ermittlung übernimmt, ob die aktuelle Klasse von der Zielklasse abgeleitet ist. Hier erfolgt etwas mehr Verarbeitung als unbedingt erforderlich wäre. Wenn ich ermitteln möchte, ob eine Klasse von „ApiController“ abgeleitet ist, kümmern mich die dabei implementierten Schnittstellen nicht wirklich. Durch Einschließen dieser Details entsteht jedoch eine praktische Hilfsmethode, die zahlreiche Einsatzmöglichkeiten bietet.

Abbildung 5: Suchen nach Basisklassen und Schnittstellen

public static IEnumerable<INamedTypeSymbol> GetBaseClasses
  (SemanticModel model, BaseTypeDeclarationSyntax type)
{
  var classSymbol = model.GetDeclaredSymbol(type);
  var returnValue = new List<INamedTypeSymbol>();
  while (classSymbol.BaseType != null)
  {
    returnValue.Add(classSymbol.BaseType);
    if (classSymbol.Interfaces != null)
      returnValue.AddRange(classSymbol.Interfaces);
    classSymbol = classSymbol.BaseType;
  }
  return returnValue;
}

Dieser Typ von Analyse wird durch Reflektion kompliziert, weil ein reflektiver Ansatz Rekursion verwendet und während des Vorgangs potentziell alle Assemblys laden muss, um Zugriff auf alle zwischengeschalteten Typen zu erhalten. Dieser Typ von Analyse ist mit dem Codemodell nicht einmal möglich. Er ist jedoch mit Roslyn bei Verwendung von „SemanticModel“ relativ simpel. „SemanticModel“ ist eine Schatzkiste mit Metadaten. Das Modell stellt alles dar, was der Compiler über den Code weiß, nachdem mühsam die Syntaxbäume an Symbole gebunden wurden. Es eignet sich nicht nur zum Erfassen der Basistypen, sondern kann auch für die Beantwortung schwieriger Fragen (z. B. Lösung überladen/überschreiben oder Suchen nach allen Verweisen auf eine Methode oder Eigenschaft oder nach einem beliebigen Symbol) verwendet werden.

Suchen nach dem zugehörigen Modell

An diesem Punkt besitze ich Zugriff auf alle Controller im Projekt. Die JavaScript-Klasse eignet sich auch gut zum Bereitstellen der in den Modellen gefundenen Eigenschaften, die von den Aktionen im Controller zurückgegeben wurden. Damit Sie verstehen, wie dies funktioniert, sehen Sie sich den folgenden Code an, der die Ausgabe der Ausführung von Gerüstbau für eine „WebApi“ zeigt:

public class Activity
  {
    public int Id { get; set; }
    public int ActivityCode { get; set; }
    public int ProjectId { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
  }

In diesem Fall wurde der Gerüstbau anhand der Modelle ausgeführt. Abbildung 6 zeigt dies.

Abbildung 6: Generierter API-Controller

public class ActivitiesController : ApiController
  {
    private ApplicationDbContext db = new ApplicationDbContext();
    // GET: api/Activities
    public IQueryable<Activity> GetActivities()
    {
      return db.Activities;
    }
    // GET: api/Activities/5
    [ResponseType(typeof(Activity))]
    public IHttpActionResult GetActivity(int id)
    {
      Activity activity = db.Activities.Find(id);
      if (activity == null)
      {
        return NotFound();
      }
      return Ok(activity);
    }
    // POST: api/Activities
    [ResponseType(typeof(Activity))]
    public IHttpActionResult PostActivity(Activity activity)
    {
      if (!ModelState.IsValid)
      {
        return BadRequest(ModelState);
      }
      db.Activities.Add(activity);
      db.SaveChanges();
      return CreatedAtRoute("DefaultApi", new { id = activity.Id }, activity);
    }
    // DELETE: api/Activities/5
    [ResponseType(typeof(Activity))]
    public IHttpActionResult DeleteActivity(int id)
    {
      Activity activity = db.Activities.Find(id);
      if (activity == null)
      {
        return NotFound();
      }
      db.Activities.Remove(activity);
      db.SaveChanges();
      return Ok(activity);
    }

Das den Aktionen hinzugefügt ResponseType-Attribut verknüpft das „ViewModel“ mit dem Controller. Mithilfe dieses Attributs können Sie den Namen des Modells abrufen, das mit der Aktion verknüpft ist. Wenn der Controller mithilfe von Gerüstbau erstellt wurde, ist jede Aktion mit dem gleichen Modell verknüpft. Manuell erstellte Controller oder nach ihrer Generierung bearbeitete Controller sind aber ggf. nicht so konsistent. Abbildung 7 zeigt, wie der Vergleich mit allen Aktionen erfolgt, um eine vollständige Liste der einem Controller zugeordneten Modelle abzurufen, wenn mehrere Modelle vorhanden sind.

Abbildung 7: Suchen nach Modellen, die einem Controller zugeordnet sind

public IEnumerable<TypeInfo> FindAssociatedModel
  (SemanticModel semanticModel, TypeDeclarationSyntax controller)
{
  var returnValue = new List<TypeInfo>();
  var attributes = controller.DescendantNodes().OfType<AttributeSyntax>()
    .Where(a => a.Name.ToString() == "ResponseType");
  var parameters = attributes.Select(a =>
    a.ArgumentList.Arguments.FirstOrDefault());
  var types = parameters.Select(p=>p.Expression).OfType<TypeOfExpressionSyntax>();
  foreach (var t in types)
  {
    var symbol = semanticModel.GetTypeInfo(t.Type);
    if (symbol.Type.SpecialType == SpecialType.System_Void) continue;
    returnValue.Add( symbol);
  }
  return returnValue.Distinct();
}

Diese Methode enthält interessante Programmlogik. Ein Teil dieser Logik ist ziemlich raffiniert. Erinnern Sie sich daran, wie das ResponseType-Attribut aussieht:

[ResponseType(typeof(Activity))]

Ich möchte auf die Eigenschaften in dem Typ zugreifen, auf den im typeof-Ausdruck verwiesen wird, der der erste Parameter des Attributs ist. In diesem Fall handelt es sich um „Activity“. Die Variable „attributes“ ist eine Liste der im Controller gefundenen ResponseType-Attribute. Die Variable „parameters“ ist eine Liste der Parameter für diese Attribute. Jeder dieser Parameter ist eine „TypeOfExpressionSyntax“, und ich kann den zugehörigen Typ durch die Eigenschaft „type“ der TypeOfExpressionSyntax-Objekte abrufen. Erneut wird das „SemanticModel“ für das Ermitteln des Symbols für diesen Typ verwendet. Es stellt alle gewünschten Details bereit.

„Distinct“ am Ende der Methode stellt sicher, dass jedes zurückgegebene Modell eindeutig ist. In den meisten Fällen ist davon auszugehen, dass Duplikate vorhanden sind, weil mehrere Aktionen im Controller mit dem gleichen Modell verknüpft sind. Außerdem bietet es sich an, zu überprüfen, ob der „ResponseType“ den Wert „void“ aufweist. Interessante Eigenschaften sind hier nicht zu erwarten.

Untersuchen des zugehörigen Modells

Der folgende Code zeigt, wie nach den Eigenschaften aus allen im Controller gefundenen Modellen gesucht wird:

public IEnumerable<ISymbol> GetProperties(IEnumerable<TypeInfo> models)
{
  return models.Select(typeInfo => typeInfo.Type.GetMembers()
    .Where(m => m.Kind == SymbolKind.Property))
    .SelectMany(properties => properties).Distinct();
}

Suchen nach den Aktionen

Ich möchte nicht nur die Eigenschaften aus den zugehörigen Modellen anzeigen, sondern auch Verweise auf die Methoden einschließen, die sich im Controller befinden. Die Methoden in einem Controller sind Aktionen. Ich bin nur an den öffentlichen Methoden interessiert. Da diese WebApi-Aktionen sind, sollten sie alle in das entsprechende HTTP-Verb übersetzt werden.

Für das Ausführen dieser Zuordnung gelten verschiedene Konventionen. Die Konvention, die vom Gerüstbau beachtet wird, bezieht sich darauf, dass der Methodenname mit dem Namen des Verbs beginnt. Die put-Methode würde daher „PutActivity“, die post-Methode „PostActivity“ und die delete-Methode „DeleteActivity“ lauten. Allgemein sind zwei get-Methoden vorhanden: „GetActivity“ und „GetActivities“. Sie können den Unterschied zwischen den get-Methoden feststellen, indem Sie die Rückgabetypen für diese Methoden untersuchen. Wenn der Rückgabetyp direkt oder indirekt die IEnumerable-Schnittstelle implementiert, ruft die get-Methode alle Elemente ab. Andernfalls ruft die Methode nur ein Element ab.

Der andere mögliche Ansatz besteht darin, dass Sie explizit Attribute zum Angeben des Verbs hinzufügen. In diesem Fall kann die Methode einen beliebigen Namen besitzen. Abbildung 8 zeigt den Code für „GetActions“, der die öffentlichen Methoden identifiziert. Diese werden dann Verben mithilfe beider Methoden zugeordnet.

Abbildung 8: Suchen nach den Aktionen für einen Controller

public IEnumerable<string> GetActions(ClassDeclarationSyntax controller)
{
  var semanticModel = compilation.GetSemanticModel(controller.SyntaxTree);
  var actions = controller.Members.OfType<MethodDeclarationSyntax>();
  var returnValue = new List<string>();
  foreach (var action in actions.Where
        (a => a.Modifiers.Any(m => m.Kind() == SyntaxKind.PublicKeyword)))
  {
    var mapName = MapByMethodName(semanticModel, action);
    if (mapName != null)
      returnValue.Add(mapName);
    else
    {
      mapName = MapByAttribute(semanticModel, action);
      if (mapName != null)
        returnValue.Add(mapName);
    }
  }
  return returnValue.Distinct();
}

Die GetActions-Methode versucht die Zuordnung zuerst basierend auf dem Namen der Methode auszuführen. Wenn das nicht funktioniert, wird die Zuordnung anhand von Attributen versucht. Wenn die Methode nicht zugeordnet werden kann, wird sie nicht in die Liste der Aktionen eingeschlossen. Wenn Sie eine andere Konvention verwenden, anhand derer die Überprüfung erfolgen soll, können Sie diese auf einfache Weise in die GetActions-Methode integrieren. Abbildung 9 zeigt die Implementierungen für die MapByMethodName- und MapByAttribute-Methoden.

Abbildung 9: „MapByName“ und „MapByAttribute“

private static string MapByAttribute(SemanticModel semanticModel,
  MethodDeclarationSyntax action)
{
  var attributes = action.DescendantNodes().OfType<AttributeSyntax>().ToList();
  if ( attributes.Any(a=>a.Name.ToString() == "HttpGet"))
    return IdentifyIEnumerable(semanticModel, action) ? "query" : "get";
  var targetAttribute = attributes.FirstOrDefault(a =>
    a.Name.ToString().StartsWith("Http"));
  return targetAttribute?.Name.ToString().Replace("Http", "").ToLower();
}
private static string MapByMethodName(SemanticModel semanticModel,
  MethodDeclarationSyntax action)
{
  if (action.Identifier.Text.Contains("Get"))
    return IdentifyIEnumerable(semanticModel, action) ? "query" : "get";
  var regex = new Regex("\b(?'verb'post|put|delete)", RegexOptions.IgnoreCase);
  if (regex.IsMatch(action.Identifier.Text))
    return regex.Matches(action.Identifier.Text)[0]
      .Groups["verb"].Value.ToLower();
  return null;
}

Beide Methoden suchen zunächst explizit nach der Get-Aktion und bestimmen, auf welchen Typ von „get” sich die Methode bezieht.

Wenn die Aktion keiner der get-Vorgänge ist, überprüft „MapByAttribute“, ob die Aktion über ein Attribut verfügt, das mit „Http“ beginnt. Wird ein solches Attribut gefunden, kann das Verb ermittelt werden, indem der Attributname angenommen und „Http“ aus dem Attributnamen entfernt wird. Es ist nicht erforderlich, explizit anhand jedes Attributs zu ermitteln, welches Verb verwendet werden soll.

„MapByMethodName“ ist ähnlich strukturiert. Nachdem zuerst überprüft wird, ob eine Get-Aktion vorhanden ist, verwendet diese Methode einen regulären Ausdruck, um zu ermitteln, ob eines der anderen Verben übereinstimmt. Wenn eine Übereinstimmung gefunden wird, können Sie den Verbnamen aus der benannten Erfassungsgruppe abrufen.

Beide Zuordnungsmethoden müssen zwischen den Aktionen zum Abrufen eines einzelnen Elements und aller Elemente unterscheiden, und beide verwenden die im folgenden Code gezeigte Identify­Enumerable-Methode:

private static bool IdentifyIEnumerable(SemanticModel semanticModel,
  MethodDeclarationSyntax actiol2n)
{
  var symbol = semanticModel.GetSymbolInfo(action.ReturnType);
  var typeSymbol = symbol.Symbol as ITypeSymbol;
  if (typeSymbol == null) return false;
  return typeSymbol.AllInterfaces.Any(i => i.Name == "IEnumerable");
}

Erneut spielt „SemanticModel“ eine entscheidende Rolle. Ich kann zwischen den get-Methoden unterscheiden, indem ich den Rückgabetyp der Methode untersuche. „SemanticModel“ gibt das Symbol gebunden an den Rückgabetyp zurück. Mit diesem Symbol kann ich feststellen, ob der Rückgabetyp die IEnumerable-Schnittstelle implementiert. Wenn die Methode ein List<T>- oder ein Enumerable<T>-Objekt oder einen Sammlungstyp zurückgibt, implementiert sie die IEnumerable-Schnittstelle.

Die T4-Vorlage

Nachdem nun alle Metadaten erfasst wurden, ist es an der Zeit, sich mit der T4-Vorlage zu beschäftigen, die alle diese Puzzleteile zusammensetzt. Ich beginne, indem ich dem Projekt eine Runtimetextvorlage hinzufüge.

Bei einer Runtimetextvorlage ist die Ausgabe der Ausführung der Vorlage eine Klasse, die die definierte Vorlage und nicht den Code implementiert, den ich generieren möchte. Die möglichen Aktionen in einer Textvorlage sind größtenteils mit den möglichen Aktionen in einer Runtimetextvorlage identisch. Der Unterschied besteht darin, wie Sie die Vorlage zum Generieren von Code ausführen. Bei einer Textvorlage übernimmt Visual Studio die Ausführung der Vorlage und das Erstellen der Hostingumgebung, in der die Vorlage ausgeführt wird. Bei einer Runtimetextvorlage sind Sie für das Einrichten der Hostingumgebung und Ausführen der Vorlage verantwortlich. Das bedeutet zwar mehr Arbeit für Sie, ermöglicht aber auch wesentlich mehr Kontrolle über das Ausführen der Vorlage und die Verwendung der Ausgabe. Außerdem entfallen alle ggf. vorhandenen Abhängigkeiten von Visual Studio.

Ich beginne, indem ich „AngularResource.tt“ bearbeite und der Vorlage den in Abbildung 10 gezeigten Code hinzufüge.

Abbildung 10: Anfängliche Vorlage

<#@ template debug="false" hostspecific="false" language="C#" | #>
var app = angular.module("challenge", [ "ngResource"]);
  app.factory(<#=className #>Resource . function ($resource) {
    return $resource('<#=Url#>/<#=className#>',{<=className#> : '@<#=className#>'},{
    <#=property.Name#> : "",
  query : {
    method: "GET"
    , isArray : true
    }
  ' <#=action#>: {
    method: "<#= action.ToUpper()#>
    }
  });
});

Abhängig davon, wie vertraut Sie mit JavaScript sind, ist dies ggf. neu für Sie, aber: keine Panik!

Die erste Zeile ist die Vorlagendirektive. Sie weist T4 an, dass ich Vorlagencode in C# schreibe. Die anderen beiden Attribute werden für Runtimevorlagen ignoriert. Aus Gründen der Klarheit gebe ich jedoch explizit an, dass ich keine Erwartungen an die Hostingumgebung habe und nicht erwarte, dass die Zwischendateien für das Debuggen gespeichert werden.

Die T4-Vorlage ähnelt ein wenig einer ASP-Seite. Die Tags <# und #> dienen als Trennzeichen zwischen Code, um die Vorlage und den Text zu steuern, die von der Vorlage transformiert werden sollen. Die Tags <#= #> trennen eine Variablenersetzung, die ausgewertet und in den generierten Code eingefügt werden soll.

Wenn Sie sich diese Vorlage ansehen, erkennen Sie, dass die Metadaten einen „className“, eine URL, eine Liste der Eigenschaften und eine Liste der Aktionen bereitstellen sollen. Da dies eine Runtimevorlage ist, kann ich einige Maßnahmen zur Vereinfachung vorsehen. Schauen Sie sich jedoch zuerst den Code an, der bei der Ausführung der Vorlage erstellt wird, die durch Speichern der TT-Datei oder durch Klicken mit der rechten Maustaste im Projektmappen-Explorer und Auswählen von „Benutzerdefiniertes Tool ausführen“ erfolgt.

Die Ausgabe aus der Ausführung der Vorlage ist eine neue Klasse, die mit der Vorlage übereinstimmt. Wichtiger ist Folgendes: Wenn ich nach unten scrolle, sehe ich, dass die Vorlage auch die Basisklasse generiert hat. Dies ist aus folgendem Grund wichtig: Wenn ich die Basisklasse in eine neue Datei verschiebe und die Basisklasse in der Vorlagendirektive explizit angebe, wird sie nicht mehr generiert, und ich kann diese Basisklasse wie erforderlich ändern.

Im nächsten Schritt ändere ich die Vorlagendirektive wie folgt:

<#@ template debug="false" hostspecific="false" language="C#"
  inherits="AngularResourceServiceBase" #>

Anschließend verschiebe ich „AngularResourceServiveBase“ in eine eigene Datei. Wenn ich die Vorlage erneut ausführe, erkenne ich, dass die generierte Klasse noch immer von der gleichen Basisklasse abgeleitet wird. Sie wurde jedoch nicht mehr generiert. Nun steht es mir frei, alle Änderungen an der Basisklasse vorzunehmen, die erforderlich sind.

Im nächsten Schritt füge ich der Basisklasse einige neue Methoden und Eigenschaften hinzu, um die Bereitstellung der Metadaten für die Vorlage zu vereinfachen.

Ich benötige für die neuen Methoden und Eigenschaften außerdem einige neue using-Anweisungen:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

Ich füge Eigenschaften für die URL und für den „RoslynDataProvider“ hinzu, den ich am Anfang des Artikels erstellt habe:

public string Url { get; set; }
public RoslynDataProvider MetadataProvider { get; set; }

Nachdem diese Elemente integriert wurden, benötige ich noch einige Methoden, die mit dem „MetadataProvider“ interagieren. Abbildung 11 zeigt dies.

Abbildung 11: „AngularResourceServiceBase“ hinzugefügte Hilfsmethoden

public IList<ClassDeclarationSyntax> GetControllers()
{
  var project = MetadataProvider.GetWebApiProject();
  return MetadataProvider.FindControllers(project).ToList();
}
protected IEnumerable<string> GetActions(ClassDeclarationSyntax controller)
{
  return MetadataProvider.GetActions(controller);
}
protected IEnumerable<TypeInfo> GetModels(ClassDeclarationSyntax controller)
{
  return MetadataProvider.GetModels(controller);
}
protected IEnumerable<ISymbol> GetProperties(IEnumerable<TypeInfo> models)
{
  return MetadataProvider.GetProperties(models);
}

Nachdem ich diese Methoden der Basisklasse hinzugefügt habe, bin ich bereit, die Vorlage so zu erweitern, dass sie verwendet werden. Sehen Sie sich in Abbildung 12 an, wie sich die Vorlage ändert.

Abbildung 12: Finale Version der Vorlage

<#@ template debug="false" hostspecific="false" language="C#" inherits="AngularResourceServiceBase" #>
var app = angular.module("challenge", [ "ngResource"]);
<#
  var controllers = GetControllers();
  foreach(var controller in controllers)
  {
    var className = controller.Identifier.Text.Replace("Controller", "");
#>    app.facctory(<#=className #>Resource , function ($resource) {
      return $resource('<#=Url#>/<#=className#>',{<#=className#> : '@<#=className#>'},{
<#
    var models= GetModels(controller);
    var properties = GetProperties(models);
    foreach (var property in properties)
    {
#>
      <#=property.Name#> : "",
<#
    }
    var actions = GetActions(controller);
    foreach (var action in actions)
    {
#>
<#
      if (action == "query")
      {
#>      query : {
      method: "GET"

Ausführen der Vorlage

Da es sich um eine Runtimevorlage handelt, bin ich für das Einrichten der Umgebung für die Ausführung der Vorlage verantwortlich. Abbildung 13 zeigt den Code, der zum Ausführen der Vorlage erforderlich ist.

Abbildung 13: Ausführen einer Runtimetextvorlage

private static void Main()
{
  var work = MSBuildWorkspace.Create();
  var solution = work.OpenSolutionAsync(Path to the Solution File).Result;
  var metadata = new RoslynDataProvider() {Workspace = work};
  var template = new AngularResourceService
  {
    MetadataProvider = metadata,
    Url = @"http://localhost:53595/"
  };
  var results = template.TransformText();
  var project = metadata.GetWebApiProject();
  var folders = new List<string>() { "Scripts" };
  var document = project.AddDocument("factories.js", results, folders)
    .WithSourceCodeKind(SourceCodeKind.Script)
    ;
  if (!work.TryApplyChanges(document.Project.Solution))
    Console.WriteLine("Failed to add the generated code to the project");
  Console.WriteLine(results);
  Console.ReadLine();
}

Die beim Speichern der Vorlage oder Ausführen des benutzerdefinierten Tools erstellte Klasse kann direkt instanziiert werden, und ich kann beliebige öffentliche Eigenschaften festlegen bzw. darauf zugreifen oder beliebige öffentliche Methoden aus der Basisklasse aufrufen. Auf diese Weise werden die Werte für die Eigenschaften festgelegt. Durch den Aufruf der TransformText-Methode wird die Vorlage ausgeführt, und der generierte Code wird als Zeichenfolge zurückgegeben. Die results-Variable enthält den generierten Code. Der restliche Code übernimmt das Hinzufügen eines neuen Dokuments mit dem generierten Code zum Projekt.

Bei diesem Code tritt jedoch ein Problem auf. Der Aufruf von „AddDocuments“ erstellt erfolgreich ein Dokument und speichert es im Ordner „scripts“. Wenn ich „TryApplyChanges“ aufrufe, ist der Aufruf erfolgreich. Das Problem erschließt sich erst, wenn ich die Projektmappe untersuche: Der Ordner „scripts“ enthält eine Datei „factories“. Das Problem: Es handelt sich nicht um „factories.js“, sondern um „factories.cs“. Die AddDocument-Methode ist nicht dafür konfiguriert, eine Dateierweiterung zu akzeptieren. Unabhängig von der Dateierweiterung wird das Dokument jedoch basierend auf dem Projekttyp hinzugefügt, dem es hinzugefügt wird. Das liegt in der Natur der Sache.

Nachdem das Programm ausgeführt wurde und die JavaScript-Klassen erstellt hat, befindet sich die Datei daher im Ordner „scripts“. Ich muss einfach nur die Dateierweiterung aus „.cs“ in „.js“ ändern.

Zusammenfassung

Die meisten hier ausgeführten Aufgaben beziehen sich auf das Abrufen von Metadaten mit Roslyn. Diese gleichen Verfahrensweisen werden sich unabhängig von der geplanten Verwendung dieser Metadaten als nützlich erweisen. Der T4-Code wird für zahlreiche Verwendungszwecke relevant bleiben. Wenn Sie Code für eine nicht von Roslyn unterstützte Sprache generieren möchten, ist T4 eine intelligente Wahl und kann leicht in Ihre Prozesse integriert werden. Dies ist eine gute Nachricht, weil Sie Roslyn nur zum Generieren von Code für C# und Visual Basic .NET verwenden können. T4 ermöglicht jedoch das Generieren beliebiger Texttypen, z. B. von SQL, JavaScript, HTML, CSS oder sogar unformatiertem Text.

Es ist hilfreich, Code wie diese JavaScript-Klassen generieren zu können, weil sie mühsam und fehleranfällig sind. Sie sind außerdem problemlos mit einem Muster konform. Soweit vertretbar, möchten Sie dieses Muster so konsistent wie möglich anwenden. Der wichtigste Punkt ist jedoch dieser: Wahrscheinlich verändert sich im Lauf der Zeit, wie dieser generierte Code aussehen soll (insbesondere für neue Elemente), wenn sich bewährte Methoden ausbilden. Wenn Sie nur eine T4-Vorlage so aktualisieren müssen, dass Sie das neue „beste Verfahren zum Erreichen des Ziels“ berücksichtigt, wenden Sie mit größerer Wahrscheinlichkeit die sich ergebenden bewährten Methoden an. Wenn Sie hingegen große Mengen manuell ­generierten, monotonen Code mühsam ändern müssen, führt dies möglicherweise zu mehreren Implementierungen, weil bei jeder Iteration die zu dem Zeitpunkt jeweils beliebteste bewährte Methode verwendet wird.


Nick Harrison ist Software Consultant und lebt zusammen mit seiner Frau Tracy und einer Tochter in Columbia (South Carolina, USA). Er ist ein Full-Stack-Entwickler, der seit 2002 mit .NET arbeitet, um Geschäftslösungen zu erstellen. Folgen Sie ihm auf Twitter: @Neh123us. Dort kündigt er auch seine Blogbeiträge, veröffentlichten Artikel und Vorträge an.

Unser Dank gilt dem folgenden technischen Experten bei Microsoft für die Durchsicht dieses Artikels: James McCaffrey
Dr. James McCaffrey ist in Redmond (Washington) für Microsoft Research tätig. Er hat an verschiedenen Microsoft-Produkten mitgearbeitet, unter anderem an Internet Explorer und Bing. Dr. McCaffrey erreichen Sie unter jammc@microsoft.com.