T4 und Generierung multipler Dateien

Wie das Text Templating Transformation Toolkit (T4) verwendet wird, habe ich bereits im Beitrag “Code Coverage bei generiertem Code mit T4 korrigieren” im Abschnitt “T4 im Einsatz” beschrieben. Dabei wird eine einzelne Ausgabedatei generiert, was in den meisten Anwendungsfällen auch gewollt ist.

Gelegentlich kann es aber sinnvoll sein, mehrere separate Dateien zu generieren, z. B. um beim Debuggen nicht in einer Datei mit unzähligen Klassen navigieren zu müssen oder um die Klassenstruktur im Projekt granularer abzubilden.

In diesem Artikel zeige ich, wie man mit T4 multiple Dateien generieren kann.

T4MultiFile

Ist das erklärte Ziel mehrere separate Dateien iterativ durch Verwendung einer gemeinsamen Logik und des T4-Toolkits zu generieren, dann kann T4MultiFile eine Hilfe sein.

T4MultiFile ist eine T4 Skript-Vorlage, die unter http://www.nuget.org/packages/T4MultiFile/ zu finden ist, und in Visual Studio 2012 sehr einfach mit der NuGet-Konsole integrierbar ist. Dafür reicht der NuGet-Konsolenbefehl:

PM> Install-Package T4MultiFile

Der Projektmappe werden damit folgende Dateien hinzugefügt:

  • MultipleOutputHelper.ttinclude
  • MultipleSample.tt

MultipleOutputHelper.ttinclude

Die ttinclude-Datei sollte nicht verändert, sondern in der Vorlage referenziert werden. Sie beinhaltet den Manager für die Generierung neuer Seiten.

// Manager mit den statischen Instanzen Host und GenerationEnvironment    

var manager = Manager.Create(Host, GenerationEnvironment);

 

manager.StartNewFile(fileName); // neue Datei    

 

//    

// Dateiinhalt    

//    

 

manager.EndBlock(); // Dateiende

MultipleSample.tt

Die “MultipleSample.tt” ist eine Beispieldatei. Sie soll die Verwendung der ttinclude-Datei und des beinhalteten Managers aufzeigen. Leider hat die Version 0.1 einen Bug. Zum Korrigieren ist das schließende T4-Element “#>” an folgende Zeile der Vorlage anzuhängen:

<# var manager = Manager.Create(Host, GenerationEnvironment);

Das Resultat sieht dann so aus:

<# var manager = Manager.Create(Host, GenerationEnvironment); #>

Mit dem Speichern werden die Dateien “MultiSample.xsl” und “MultipleTest.generated.cs” generiert.

Die Generierung der Datei “MultipleSample.xsl” ist optional. Um das Generieren zu unterbinden, ist die output-Direktive der Vorlage zu löschen:

<#@ output extension=".xsl"#>

Alternativ lässt sich diese output-Ausgabe für das Mitloggen von Generierungsereignissen oder des Zeitpunkts der letzten Ausführung des Skriptes verwenden. Dafür muss nur beachtet werden, dass Inhalte, die in der output-Datei erscheinen sollen, vor manager.StartNewFile(…) oder nach manager.EndBlock() als Ausgabe definiert werden.

Beispiel

Der folgende Code beinhaltet den gesamten Inhalt einer Beispiel-Vorlage. In einer for-Schleife werden iterativ Klassen generiert, daneben noch die .log.txt, die den Startpunkt, Endpunkt und die erstellten Dateien loggt.

<#@ template debug="True" language="C#" hostspecific="True" #>    

<#@ output extension=".generated.log.txt"#>    

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

<#  bool doMultiFile=true;    

#>

<# var manager = Manager.Create(Host, GenerationEnvironment); #>


Generation started: <#= DateTime.Now.ToString()#>

 

<# for(int i=1; i < 20; i++)    

{

    string fileName = LastFileName = "MultipleTest" + i.ToString() + ".generated.cs";   

    manager.StartNewFile(fileName); 

#>

namespace T4Enumerator    

{

    public class MultipleTest<#= i.ToString()#>    

    {

    }

}

<# 

manager.EndBlock();

 

#>

File Generated: <#= LastFileName #>

<#

}

manager.Process(doMultiFile); #>

 

Generation ended: <#= DateTime.Now.ToString()#>

 

<#+ string LastFileName = ""; // globale Variable zur Ausgabe im Log#>    

Hier die generierte Ausgabe der .log.txt:

Generation started: 25.07.2013 14:55:25

File Generated: MultipleTest1.generated.cs
File Generated: MultipleTest2.generated.cs
File Generated: MultipleTest3.generated.cs

File Generated: MultipleTest19.generated.cs

Generation ended: 25.07.2013 14:55:27

Fazit

Mit T4MultiFile hat man eine T4 Skript-Vorlage, die einfach einzubinden und zu verwenden ist und die es ermöglicht multiple Ausgabedateien mit einem T4-Skript zu generieren.

Die notwendigen Schritte dafür sind:

  • Importieren der Vorlage z. B. mittels NuGet
  • Umbenennen der MultiSample.tt in einen projektbezogenen, sprechenden Namen
  • output-Direktive evtl. neu definieren oder entfernen
  • Jede neue Datei mit manager.StartNewFile(…) starten und mit manager.EndBlock() beenden
  • Logik der Dateiinhalte implementieren

Code-Coverage bei generiertem Code mit T4 korrigieren

div class=”articleAbstract”>Ich möchte Ihnen mit diesem Artikel zeigen, wie ich mit Hilfe des Text Templating Transformation Toolkit (T4) durch einen Codegenerator erzeugte Klassen aus der Code-Coverage (Testabdeckung)-Analyse ausschließe. Ich erreiche somit, dass die Analyse nur den von mir verfassten Code analysiert und ich erhalte einen aussagekräftigen Status der Testabdeckung.

Die Ausgangslage

In einem aktuellen Projekt verwende ich einen Codegenerator, der das Datenmodell einer Dynamics CRM-Implementierung und den Datenkontext zur Kommunikation mit dieser generiert. Der Codegenerator (crmsvcutil.exe) ist Bestandteil des Microsoft Dynamics CRM 2011 Software Developer Kit (SDK).
Der mit dem Codegenerator erzeugte Code besteht aus knapp 170.000(!) Zeilen Code, sodass es bei der Messung der Code-Coverage nicht verwunderlich ist, dass diese nur sehr klein ist (einstelliger Prozentsatz).


Resultat der CodeCoverage mit generiertem Code

[ExcludeFromCodeCoverage] als Lösung

Mithilfe des „ExcludeFromCodeCoverage“-Attributs des Namespace System.Diagnostics.CodeAnalysis ist es möglich, Klassen aus der Code-Coverage-Analyse auszuschließen. Die Aufgabe ist jetzt, die durch den Codegenerator erstellten Klassen mit diesem Attribut zu erweitern. Es macht jedoch keinen Sinn, eigenen Code in die generierte C#-Datei (*.cs) zu schreiben, da dieser beim Aktualisieren durch den Generator ohne Rückfrage überschrieben wird.
Die autogenerierten Klassen sind alle als „partial” deklariert. Es ist also möglich eine C#-Datei mit entsprechenden „partial” Klassen zu erstellen, die alle das „ExcludeFromCodeCoverage”-Attribut erhalten. Da dies eine Überprüfung von 170.000 Zeilen Code bedeutet, entschied ich mich das T4 zu verwenden.

T4 im Einsatz

Ziel ist es, eine Vorlage zu erzeugen, anhand derer für jede generierte “partial“ Klasse Codezeilen nach folgendem Muster anlegen werden sollen:

   1: /// <summary>

   2: /// The class part for excluding the class from the code coverage analysis

   3: /// This part is auto generated. All changes done here will be overwritten.

   4: /// </summary>

   5: [ExcludeFromCodeCoverage]

   6: public partial class SomeAutoGeneratedClass

   7: {

   8: }

Dazu füge ich dem Projekt erst einmal eine Textvorlage hinzu. Mit dem Filter „T4“ bietet das Visual Studio im „Neues Element hinzufügen“-Dialog folgende Vorlagen an:

  • Laufzeit-Textvorlage: erzeugt eine Transformationsklasse, die Codeseitig angesprochen werden kann
  • Textvorlage: Vorlage wird manuell oder beim Speichern ausgeführt


Neues Element Dialog gefiltert auf T4

Für das Beispiel verwende ich die Textvorlage.
Am Anfang der Textvorlage befindet sich der Bereich für Direktiven (http://msdn.microsoft.com/en-us/library/bb126421.aspx). Folgende Direktiven habe ich in meinem Beispiel verwendet:

  • <#@ template … #>  debug=“true“: aktiviert die Codezeilenausgabe bei Fehlern  hostspecific=“true“: ermöglicht den Zugriff auf den IServiceProvider und über diese auf die Entwicklungsumgebung language=“C#“: (oder:“VB“) definiert die verwendete Programmiersprache
  • <#@ assembly name=“…“ #> lädt Assemblies, damit deren Klassen innerhalb der Vorlage codeseitig verwendet werden können
  • <#@ import namespace=“…“ #> erlaubt den Zugriff auf den angegebenen Namespace innerhalb der Vorlage
  • <#@ output … #> extension=“.cs“ oder “.vb“ definiert das Ausgaberesultat der Vorlage

Codezeilen, die durch den Compiler abgearbeitet werden sollen und die Programmlogik in C# oder VB enthalten, sind in folgender Syntax zu schreiben:

  • <# C#/VB Code #>
  • <#+ wiederverwendbarer C#/VB Code (Variablen, Methodendeklarationen) #>
  • <#= Variablenausgabe #>

Ich starte mit dem <# … #>-Block in dem ich eine Klasseninstanz der Entwicklungsumgebung lade. Dazu muss im Deklarationsbereich <#@ template hostspecific=“true“ … #> gesetzt sein.

   1: IServiceProvider hostServiceProvider = (IServiceProvider)Host;

   2: EnvDTE.DTE dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE));

Als nächstes wird das Projekt nach dem Namespace durchsucht, den der Codegenerator erstellt hat und der die partiellen Klassen enthält.

   1: foreach (EnvDTE.ProjectItem projectItem in project.ProjectItems)

   2: {

   3:     EnvDTE.FileCodeModel fileCodeModel = projectItem.FileCodeModel;    

   4:     if(fileCodeModel != null)

   5:     {

   6:         foreach(EnvDTE.CodeElement codeElement in fileCodeModel.CodeElements)            

   7:         {

   8:             EnvDTE.CodeNamespace codeNamespace = codeElement as EnvDTE.CodeNamespace;           

   9:             if(codeNamespace == null || codeNamespace.FullName != gesuchterNamespace) 

  10:             {

  11:                 continue;

  12:             }

Für den gesuchten Namespace definiere ich den Datei-Header mit den benötigten using-Direktiven. Eventuelle Kommentare können hier mit integriert werden. Dazu muss ich den Bereich für ausführbaren Code mit „#>“ unterbrechen und schreiben direkt die entsprechenden Inhalte in das Template.

   1: namespace <#= gesuchterNamespace #>

   2: {

   3:     using System.Diagnostics.CodeAnalysis;

Anschließend setze ich den ausführbaren Codebereich mit „<#+“ fort, indem ich in einer weiteren Schleife nach partiellen Klassen suche und das Template definiere, das für jede gefundene partielle Klasse einzusetzen ist.

   1: foreach(EnvDTE.CodeElement member in codeNamespace.Members)

   2: {

   3:     if(member.Kind != vsCMElement.vsCMElementClass)

   4:     {

   5:         continue;

   6:     }

   7:     // prüfe, ob Klasse partial ist

   8:     CodeClass2 myClass = member as CodeClass2; // benötigt EnvDTE80

   9:     if(myClass == null || myClass.ClassKind != vsCMClassKind.vsCMClassKindPartialClass)

  10:     {

  11:         continue;

  12:     } #>

  13:  

  14:     [ExcludeFromCodeCoverage]

  15:     public partial class <#= member.Name #>

  16:     {

  17:     }

  18:     <#+

  19: }

Das Ergebnis

Mit jedem (Zwischen-)Speichern der Vorlage startet Visual Studio das integrierte „TextTemplatingTool“. Visual Studio zeigt daraufhin die Sicherheitswarnung: Führen Sie keine Vorlagen von nicht vertrauenswürdigen Quellen aus. Nach dem Bestätigen der Meldung wird die C#-Datei anhand der Vorlage als Kind-Element der tt-Datei erstellt.
Vorlage und resultierende C# Datei
Eine erneute Codeanalyse zeigt jetzt eine Abdeckung von knapp 96%.


Fazit

Die Unterstützung durch Visual Studio bietet leider nicht den gewohnten Komfort, was vermutlich der Grund dafür ist, warumT4 viel zu wenig eingesetzt wird. Das Erstellen der Vorlage bedeutet allerdings einen einmaligen Aufwand, der sich lohnt. Das Resultat ist eine strukturierte Vorlage, die einfach zu ändern und noch einfacher auszuführen ist.
Innerhalb der Vorlage kann beliebiger (C#/VB-)Code zum Einsatz kommen, wodurch T4 ein mächtiges Werkzeug für jeden Entwickler darstellt.

Weitere Quellen:

Vollständiger Code:

   1: <#@ template debug="true" hostspecific="true" language="C#" #>

   2: <#@ assembly name="System.Core" #>

   3: <#@ assembly name="System" #>

   4: <#@ assembly name="EnvDTE" #> 

   5: <#@ assembly name="EnvDTE80" #> 

   6: <#@ import namespace="EnvDTE" #>

   7: <#@ import namespace="EnvDTE80" #>

   8: <#@ import namespace="System.CodeDom" #>

   9: <#@ import namespace="System.Linq" #>

  10: <#@ import namespace="System.Text" #>

  11: <#@ import namespace="System.Collections.Generic" #>

  12: <#@ import namespace="System.Reflection" #>

  13: <#@ output extension=".cs" #>

  14:  

  15: <# 

  16:     IServiceProvider hostServiceProvider = (IServiceProvider)Host;

  17:     EnvDTE.DTE dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE));

  18:     EnvDTE.ProjectItem containingProjectItem = dte.Solution.FindProjectItem(Host.TemplateFile);

  19:     EnvDTE.Project project = containingProjectItem.ContainingProject;

  20:  

  21:     foreach (EnvDTE.ProjectItem projectItem in project.ProjectItems)

  22:     {

  23:         ProcessProjectItem(projectItem);

  24:     }

  25: #>

  26: <#+ 

  27:     // hier steht der gesuchte Namespace

  28:     // ================================

  29:     string ns = "Sdx.....Data.Xrm";

  30:     // ================================

  31:     bool found = false; 

  32:     private void ProcessProjectItem(EnvDTE.ProjectItem projectItem)

  33:     {

  34:         EnvDTE.FileCodeModel fileCodeModel = projectItem.FileCodeModel;    

  35:         if(fileCodeModel != null)

  36:         {

  37:             foreach(EnvDTE.CodeElement codeElement in fileCodeModel.CodeElements)

  38:             {

  39:                 if(found)

  40:                 {

  41:                     break;

  42:                 }

  43:  

  44:                 EnvDTE.CodeNamespace codeNamespace = codeElement as EnvDTE.CodeNamespace;

  45:                 if(codeNamespace == null || codeNamespace.FullName != ns)

  46:                 {

  47:                     continue;

  48:                 }

  49:  

  50:                 found = true;

  51: #>

  52: // --------------------------------------------------------------------------------------------------------------------

  53: // <copyright file="SDXCrmTypesExt.cs" company="SDX-AG">

  54: //   2013

  55: // </copyright>

  56: // <summary>

  57: //   The class part for excluding the class from the code coverage analysis

  58: //   This part is auto generated. All changes done here will be overwritten.

  59: // </summary>

  60: // <auto-generated />

  61: // --------------------------------------------------------------------------------------------------------------------

  62:  

  63: namespace <#= ns #>

  64: {

  65:     using System.Diagnostics.CodeAnalysis;

  66:  <#+            

  67:                 foreach(EnvDTE.CodeElement member in codeNamespace.Members)

  68:                 {

  69:                     // prüfe, ob Element eine Klasse ist

  70:                     if(member.Kind != vsCMElement.vsCMElementClass)

  71:                     {

  72:                         continue;

  73:                     }

  74:                     

  75:                     // prüfe, ob Klasse partial ist

  76:                     CodeClass2 myClass = member as CodeClass2;

  77:                     if(myClass == null || myClass.ClassKind != vsCMClassKind.vsCMClassKindPartialClass)

  78:                     {

  79:                         continue;

  80:                     }

  81:                     #>

  82:     

  83:     /// <summary>

  84:     /// The class part for excluding the class from the code coverage analysis

  85:     /// This part is auto generated. All changes done here will be overwritten.

  86:     /// </summary>

  87:     [ExcludeFromCodeCoverage]

  88:     public partial class <#= member.Name #>

  89:     {

  90:     }

  91: <#+

  92:                 }

  93:                 WriteLine("}");

  94:             }

  95:         }

  96:     }

  97: #>