Prozedurales XML – mit Razor!

Wäre es nicht cool, wenn man die Razor-Syntax auch außerhalb von ASP.NET MVC verwenden könnte, z.B. um XML-Dateien zu definieren?

Beispielsweise könnte man damit Testdaten generieren:

<Points>
  @for(int i=0; i<3; i++)
  {
    <Point x="@i" y="0" />
  } 
</Points>

Doch da soll noch nicht Schluss sein. Es wäre extrem cool, wenn man damit auch unendliche Streams erzeugen könnte:

<Points> 
 @while(true)
 {
  for(int i=0;i<10;i++)
  {
    <Point x="@i" y="0"/> 
  }

  for(int i=9;i>=0;i--)
  {
    <Point x="@i" y="0"/> 
  } 
 } 
</Points>

Erstaunlicherweise ist die Umsetzung gar nicht so schwer, denn die RazorEngine ist sehr modular aufgebaut. Die Engine ist zwar in System.Web.Razor zu finden, kann jedoch auch außerhalb einer Webanwendung eingesetzt werden.

Die Engine ist dabei ganz klassisch in zwei Teile zerlegt: Parser und Generator (Compiler). Mit dem Parser kann man man aus einem Text einen AST (Abstract Syntax Tree) erstellen. Mit dem Generator kann eine Klasse kompiliert werden, welche die Text-Ausgabe (hier: den XML-Textstream) erzeugt. Der Parser ist universell einsetzbar,  der vorhandene Generator ist jedoch auf die Ausgabe als Text (bzw. als HTML) fokussiert.

Die Generatoren erzeugen nicht direkt C# Code oder eine .NET Assembly, sondern nutzen die Infrastruktur von System.CodeDom. Es wird quasi eine Objekt-Repräsentation des Codes im RAM erzeugt, welche man dann mittels des Compilers auch in eine Assembly kompilieren kann.

Wenn man die in System.Web.Razor vorhandenen Generatoren verwenden will, muss die Texterzeugung in einer Klasse ablaufen, welche die Methoden Execute(), Write(object) und WriteLiteral(string) implementiert.

Es ist sicher einfacher, sich mit dieser API zu arrangieren, als selbst einen Generator zu implementieren:

public abstract class TextEngine
{ 
 public abstract void Execute();
 
 protected void Write(object content)
 {
   WriteLiteral(content.ToString());
 }

 protected void WriteLiteral(string literal)
 {
   // write content to a stream...
 } 
}

Der Standard-Generator von Razor generiert dann eine Klasse, welche die Methode Execute implementiert.

Um eine Instanz zu erstellen, ist folgender Code nötig:

public static TextEngine Create(Stream stream)
{
 var lang = new CSharpRazorCodeLanguage();
 var host = new RazorEngineHost(lang);
 host.DefaultBaseClass = "TextEngine";
 host.NamespaceImports.Add("RazorTextEngine");

 string ns = "TextEngineInstances" + instancesCount++;
 var codeGen = new System.Web.Razor.Generator.CSharpRazorCodeGenerator("TextGenerator", ns, null, host);

 RazorTemplateEngine e = new RazorTemplateEngine(host);

 using (var reader = new StreamReader(stream))
 {
  var parseResult = e.ParseTemplate(reader);
  parseResult.Document.Accept(codeGen);
 }

 CSharpCodeProvider prov = new CSharpCodeProvider();
 codeGen.GeneratedCode.ReferencedAssemblies.Add("RazorTextEngine.dll");
 var compilerResults = prov.CompileAssemblyFromDom(new CompilerParameters() { GenerateInMemory = true, GenerateExecutable = false, IncludeDebugInformation = true }, codeGen.GeneratedCode);
 var generator = (TextEngine)compilerResults.CompiledAssembly.CreateInstance(ns + ".TextGenerator");
 return generator;
}

Nun liegt die Kunst darin, die Klasse TextEngine so zu implementieren, dass wir die XML-Daten bequem verarbeiten können. XmlDocument und auch XmlReader kommen mit der Klasse System.IO.TextReader zurecht. Also ist diese abstrakte Klasse zu implementieren.

Nun, hier wird es etwas kompliziert. Der Razor-Generator generiert in die Execute-Methode prozeduralen Code hinein. Klar könnte man diesen einfach ununterbrochen ausführen und den resultierenden Text in einen Stream schreiben und am Ende das Ergebnis zurückgeben. Auf diese Art und Weise könnten aber keine unendlichen Streams erzeugt werden und außerdem wäre der Speicherverbrauch nicht optimal.

Der Lösungsansatz besteht darin, dass die Methode Execute in einem separaten Worker-Thread ausgeführt wird und dass dieser Thread anhält, wenn der erzeugte Inhaltstext nicht durch die TextReader-API konsumiert wird.

Als Vereinfachung und da ich wirklich nicht talentiert genugt bin komplexen multi-threaded Code zu schreiben, schreibt der Worker-Thread die gesamte Ausgabe in eine Queue. Diese Queue kann dann bequem vom Hauptthread ausgelesen werden. Damit die Queue nicht zu groß wird, ist eine ganz simple Prüfung enthalten, die ein kurzes Thread.Sleep durchführt, sobald die Queue eine bestimmte Größe erreicht hat. Da die minimale Unterbrechung von einer Millisekunde nicht zu unterschätzen ist, darf die maximale Queue-Größe nicht zu klein gewählt sein.

Queue<char[]> content;

protected void WriteLiteral(string literal)
{
 while (content.Count > maxQueueSize)
 {
  Thread.Sleep(1);
 }

 var arr = literal.ToCharArray();

 lock (content)
  content.Enqueue(arr);
}

Die TextEngine kann wie folgt genutzt werden, z.B. um die Razor-Definition aus einer eingebetteten Ressource zu lesen:

var stream = typeof(Program).Assembly.GetManifestResourceStream("RazorXmlReaderTest.TestData.txt");

var testDataEngine = RazorTextEngine.TextEngineFactory.Create(stream);
testDataEngine.Start();
XmlDocument doc = new XmlDocument();
doc.Load(testDataEngine);
testDataEngine.Stop();
File.WriteAllText("Test1.xml", doc.InnerXml);
Console.WriteLine(doc.InnerXml);

Der vollständige Source Code kann hier  runtergeladen werden. Es ist zu beachten, dass TextReader nicht vollständig implementiert wird, denn um den TextReader in XmlDocument zu nutzen, ist ausschließlich die Methode Read(char[], int, int) zu implementieren.

Eine Krücke existiert in der Implementierung noch. Ohne einen selbst implementierten Generator ist der Generator-Klasse unbekannt, wann das Ende des Streams erreicht ist.

Als Workaround können wir das Ende-Kommando direkt in die XML-Testdaten als Razor einbetten:

@{ WriteEnd(); }

Fazit

Als Fazit fand ich Razor doch leicht zu adaptieren. Um endlose XML-Streams zu ermöglichen ist aber schon ein recht komplexes Grunddesign mit einem Workerthread nötig. Am Ende hat es geklappt und ich bin gespannt, wann ich dieses prozedurale XML das erste mal in einem realen Projekt einsetzen kann.

Source – Procedural XML