Click here to Skip to main content
15,891,529 members
Articles / Programming Languages / XML

Template-based Code Generation with Razor and Roslyn

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
26 Oct 2020Apache9 min read 7.7K   7  
In this blog post, I complete my “template-based card image generator” project.
Here, I will discuss one of my favorite .NET capabilities: compiling C# code at runtime and using the generated types. I will also show how to use Razor templates to generate non-HTML files.

The whole process goes through five steps:

Image 1

  1. Using Razor to compile a C#/XAML template into the C# code of a code generator class.
  2. Using Roslyn to compile the C# generator into an assembly.
  3. Using LoadContext to temporarily load the generator assembly and instantiate the generator object.
  4. Using the generator object to bind the template to XML data and generate a XAML file.
  5. Using WPF to generate a picture file from XAML.

I already discussed the details of how I implemented databinding it the first post of this series. The second post describes the last step: using XAML to generate images.

Today, I will go through al the details involved in the code generation: steps 1 through 4. This post is going to present a lot of code touching very interesting technologies.

The complete source code for the project is available on GitHub. The repo contains a fully working generator of card images, which I named Card Artist. Board game designers can use it to create their own cards for playtesting or for professional printing. The GitHub repo also contains binaries for the application as well as documentation on how to create your own card templates.

All the code in this post is written for .NET 5. The sections regarding Roslyn should work with minimal changes in earlier versions of .NET Core as well as in .NET Framework. The sections regarding Razor should work in .NET Core 2+ but Razor is a relatively new technology so I would suggest sticking with the most updated version of .NET that you can use.

Step 1: The Razor’s Edge

Razor is a templating language and technology that is part of ASP.NET. Normally, in a Razor template, you mix XML and C# code to generate HTML pages. Today, we will abuse Razor to generate XAML files. This way of using Razor is not supported but, because both XAML and HTML are XML-based languages, it works pretty well. Potentially, the same technique can be used to generate other non-XML text with results of variable quality.

It is worth noting that the code in this section leverages .NET classes that are public but not documented. It is possible that this code will require changes if used with future versions of .NET. Luckily, the source code of Razor is available on GitHub so we are able to read and learn how to use these classes.

The code in this section leverages .NET classes that are public but not documented.
It is possible that this code will require changes if used with future versions of .NET.

I will use the simplest way to work with a Razor template: creating a RazorEngine from the Microsoft.AspNetCore.Razor.Language package. This approach is deprecated but it is the easiest way to parse a single Razor file. The RazorEngine.Create method is used internally by other non-deprecated Razor classes, so I considered it safe to use even if it is deprecated.

C#
//A class member was marked with the Obsolete attribute
#pragma warning disable CS0618
var razorEngine = RazorEngine.Create(b =>
{
  FunctionsDirective.Register(b);
  b.SetBaseType(typeof(RazorTemplateBase).FullName);
});
#pragma warning restore CS0618 

The RazorEngine allows to parse a Razor template file and write the code of a C# class that implements a code generator with the behavior defined by the template.

I have also registered the Functions directive which allows to use a @functions block to write C# code at the class level (all the other code generated by the template will be placed inside a method named ExecuteAsync). I have also specified that the classes generated by the RazorEngine must inherit from a class named RazorTemplateBase.

Given the code below on the left, the RazorEngine converts it to the C# code on the right.

XML
@using System;
<Foo>
@while (i++ < 3)
{
  <Bar n="@i" />
}
</Foo>

@functions
{
  int i = 0;
}
C#
namespace Razor
{
  using System;
  public class Template: RazorTemplateBase
  {
    public async override Task ExecuteAsync()
    {
      WriteLiteral("<Foo>\r\n");
      while (i++ < 3)
      {
        WriteLiteral("    <Bar");
        BeginWriteAttribute(
          "n", " n=\"", 36, "\"", 42, 1);
        WriteAttributeValue(
          "", 40, i, 40, 2, false);
        EndWriteAttribute();
        WriteLiteral(" />\r\n");
     }
     WriteLiteral("</Foo>\r\n\r\n");
   }
   int i = 0;
  }
}

The next step is to write the RazorTemplateBase class. Unfortunately, .NET doesn’t provide an interface defining the expectations for this class. We can see from the generated C# code above that RazorTemplateBase is expected to have a virtual Task ExecuteAsync() method as well as other methods named WriteLiteral, BeginWriteAttribute, WriteAttributeValue and EndWriteAttribute. I had a look at how .NET implements BaseView to figure out what are the expectations for these methods as well as what other methods should be implemented. Fortunately, at least in the current version of Razor (.NET 5), it turns out to be relatively simple.

I will start with defining the class and its contained data:

  • a TextWriter to write the output XAML to
  • a dynamic property named Data to which I will assign the XML data representing the card
C#
public abstract class RazorTemplateBase
{
  public TextWriter Output { get; } = new StringWriter();
  public dynamic Data { get; private set; }
  
  public void Init(dynamic data)
  {
    Data = data;
  }
...

The code in the Razor template will be able to access the Data property providing a very simple yet effective form of data binding.

Because the RazorEngine doesn’t generate a constructor for the C# class it writes, we cannot have a constructor with parameters in the RazorTemplateBase class. For this reason, I created an initialization method instead.

I will also add the Write and WriteLiteral methods as well as the abstract abstract ExecuteAsync. The Write methods are used to write XML text and attribute values and must perform XML encoding to escape invalid characters. The WriteLiteral methods are used to write the XML syntax elements and don’t perform any escape. We will also be able to use these methods from the template in case we have a variable that contains XML data that we want to write as-is to the output document.

C#
public abstract class RazorTemplateBase
{
  public abstract Task ExecuteAsync();

  protected virtual void Write(object value)
  {
    if (value != null)
      Write(Convert.ToString(value, CultureInfo.InvariantCulture));
  }
  
  protected virtual void Write(string value)
  {
    if (!string.IsNullOrEmpty(value))
      Output.Write(SecurityElement.Escape(value));
  }
  
  protected void WriteLiteral(object value)
  {
    if (value != null)
      WriteLiteral(Convert.ToString(value, CultureInfo.InvariantCulture));
  }
  
  protected void WriteLiteral(string value)
  {
    if (!string.IsNullOrEmpty(value))
      Output.Write(value);
  }
...

I will wrap up RazorTemplateBase by implementing the methods that are used to write attribute values.

C#
public abstract class RazorTemplateBase
{
  protected string attributeEnd;
  
  protected void BeginWriteAttribute(
    string name, string begining, int startPosition,
    string ending, int endPosition, int thingy)
  {
    if (attributeEnd != null)
      throw new Exception("Wrong state for BeginWriteAttribute");
    WriteLiteral(begining);
    attributeEnd = ending;
  }
  
  protected void WriteAttributeValue(
      string prefix, int prefixOffset, object value,
      int valueOffset, int valueLength, bool isLiteral)
  {
    if (attributeEnd == null)
      throw new Exception("Wrong state for WriteAttributeValue");
    if (isLiteral)
      WriteLiteral(value);
    else
      Write(value);
  }

  protected virtual void EndWriteAttribute()
  {
    if (attributeEnd == null)
      throw new Exception("Wrong state for EndWriteAttribute");
    WriteLiteral(attributeEnd);
    attributeEnd = null;
  }
...

I have all the pieces now to read a Razor template from file, and process it into C# code:

C#
var templateText = File.ReadAllText(razorFilePath);
var razorSourceDocument =
  RazorSourceDocument.Create(templateText, Path.GetFileName(razorFilePath));
var razorCodeDocument = RazorCodeDocument.Create(razorSourceDocument);
razorEngine.Process(razorCodeDocument);
var generatedCode = razorCodeDocument.GetCSharpDocument().GeneratedCode;

Step 2: Three Things to Do While in Roslyn

Roslyn is the nickname of the .NET compiler platform, it can be a little difficult to figure out exactly what falls under the umbrella of Roslyn because the name “Roslyn” never appears in package names and namespaces. What I am about to use requires the Microsoft.CodeAnalysis.CSharp package.

Roslyn has many functionalities but I will use it for three simple tasks:

  1. Parse a C# file
  2. Reformat C# code to fix indentation
  3. Compile C# code into an assembly

The first task is achieved with a single line of code:

C#
var parsedCode = CSharpSyntaxTree.ParseText(SourceText.From(generatedCode));

When Razor generates C# code, the result is frequently not indented correctly. This is not a problem if all we want to do is compile it but, in case our template contains errors, it would be nice to have a properly formatted C# text that is easy to read and debug.

Luckily, it requires just two additional lines of code to have Roslyn fix the indentation for us:

C#
var formattedRoot = (CSharpSyntaxNode)parsedCode.GetRoot().NormalizeWhitespace();
parsedCode = CSharpSyntaxTree.Create(formattedRoot);

Razor generates C# code that uses the new Nullable Reference Types feature. If you are using a version of .NET earlier than .NET 5, you will need to specify a CSharpParseOptions parameter indicating that you want to use the latest language version.

The last task, compilation, is the most complex. First of all, I need to specify which assemblies the compiler will have access to. Because my goal is to generate an assembly that will be loaded within the same application, a good place to start is to add references to all assemblies that the application has currently loaded (AppDomain.CurrentDomain.GetAssemblies):

I will then add references to the assemblies containing XElement and DynamicObject because my templates will use these types. I will also need the Microsoft.CSharp assembly.

C#
var references = AppDomain.CurrentDomain.GetAssemblies()
  .Where(asm => !asm.IsDynamic && !string.IsNullOrEmpty(asm.Location))
  .Select(asm => MetadataReference.CreateFromFile(asm.Location))
  .Concat(new MetadataReference[] {
    MetadataReference.CreateFromFile(
      typeof(XElement).Assembly.Location),
    MetadataReference.CreateFromFile(
      typeof(System.Dynamic.DynamicObject).Assembly.Location),
    MetadataReference.CreateFromFile(
      Assembly.Load(new AssemblyName("Microsoft.CSharp")).Location)
  }).ToList();

Finally, I can create a CSharpCompilation and use it to build the C# code into an assembly.

C#
using var templateAssemblyStream = new MemoryStream();
using var templatePdbStream = new MemoryStream();
var options = new CSharpCompilationOptions(
  OutputKind.DynamicallyLinkedLibrary,
  optimizationLevel: OptimizationLevel.Debug);
var cSharpCompilation = CSharpCompilation.Create(
  Guid.NewGuid().ToString(),
  new List<SyntaxTree> { parsedCode },
  references,
  options);
var compilationResult = cSharpCompilation.Emit(
  templateAssemblyStream,
  templatePdbStream,
  options: new EmitOptions(debugInformationFormat: DebugInformationFormat.Pdb));
if (!compilationResult.Success)
{
  // Handle errors, the compilation errors are in compilationResult.Diagnostics
}

I am choosing to also emit debugging information (Pdb) as well as using a Debug optimization level, this will result in worse performance but more understandable error messages which are going to be useful to debug any error in the template files.

I am using a GUID to generate unique assembly names because I wouldn’t be able to load multiple assemblies with the same name concurrently.

Steps 3 and 4: Rock and Roll

The last two steps are much simpler. First, we load the assembly and retrieve the .NET type of the generator.

C#
var templateAssembly = Assembly.Load(
  templateAssemblyStream.ToArray(),
  templatePdbStream.ToArray());
var generatorType = templateAssembly.GetType("Razor.Template");

Then, we instantiate a generator object, initialize it with the XML data representing the card and generate the XAML text. If you missed it, you can find the blog post covering XmlDynamicElement here.

C#
dynamic cardData = new XmlDynamicElement(cardXmlElement);
var compiledTemplate = (RazorTemplateBase)Activator.CreateInstance(generatorType);
compiledTemplate.Init(cardData);
await compiledTemplate.ExecuteAsync();
var xaml = compiledTemplate.Output.ToString();

A very cool aspect of this whole process is that we were able to run all the steps without ever writing anything to file: the generated C# code, assembly and XAML are all in memory so no complicated cleanup is needed!

Step 5, converting the XAML text into a picture, is covered in the previous blog post, I won’t repeat it here.

The Final Product

All of the work so far gives us a very straightforward way to templatize XAML code:

XML
@using System
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      Background="Black" Width="2.5in" Height="3.75in">
  <StackPanel Orientation="Vertical">
  @foreach (var row in Data["Rows"])
  {
    <TextBlock>@row</TextBlock>
  }
  </StackPanel>
</Grid>

This is very readable and much more compact that using the WPF data binding syntax. Obviously, WPF data binding is superior in many ways (e.g., two-way biding) but this approach works better because we only want to generate images.

Clean Up After Yourself

My solution works well if the application runs the generation process just once (or a few times). If we keep the application open and run the generation over and over, all the generated assemblies will stay loaded increasing the memory usage.

This can be easily addressed by using a collectible (unloadable) AssemblyLoadContext. We can load multiple assemblies into the AssemblyLoadContext and later unload it when we don’t need them anymore.

C#
var LoadContext = new AssemblyLoadContext("Generation context", true);

...

templateAssemblyStream.Seek(0, SeekOrigin.Begin);
templatePdbStream.Seek(0, SeekOrigin.Begin);
var templateAssembly = LoadContext.LoadFromStream(
  templateAssemblyStream, templatePdbStream);
var generatorType = templateAssembly.GetType("Razor.Template");
var compiledTemplate = (RazorTemplateBase)Activator.CreateInstance(generatorType);

...

LoadContext.Unload();

The ability to unload an AssemblyLoadContext is a new feature added in .NET Core 3.0. If you are targeting .NET Framework, unloading assemblies can be achieved using the AppDomain class instead.

Feature Creep

In the previous post, I have discussed how, in the way we use WPF, when have to write absolute paths when referencing images. Using absolute paths in code is a bad practice so I will add a feature to RazorTemplateBase to allow using paths relative to a “project folder”.

I will simply add a ProjectRoot property to RazorTemplateBase and a Path method to convert relative paths to absolute.

C#
public abstract class RazorTemplateBase
{
  public Uri ProjectRoot { get; private set; }

  public void Init(dynamic data, Uri projectRoot)
  {
    Output = new StringWriter();
    ProjectRoot = projectRoot;
    Data = data;
  }

  public string Path(object relativePath) =>
    new Uri(ProjectRoot, relativePath.ToString()).LocalPath;

Now I can use the Path method from the Razor templates:

XML
<Image Source="@Path(@"Resources\Backround.png")" />

Another issue is that my code earlier was using a fixed list of reference assemblies during the generator compilation. This limits which .NET features the user writing the template can use.

I will address this by allowing to add assembly references directly from the template text by writing an XML comment in the following format:

XML
<!--reference Resources\Utils.dll-->

Now, back in the step 2 code, when filling the references list, I can use a Regex to identify these special comments in the template code and convert them to metadata references for the compiler to use.

C#
var projectRoot = new Uri(ProjectPath + @"\");
foreach (Match match in Regex.Matches(templateText,
  @"<!--\s*reference\s+(.+?)\s*-->", RegexOptions.IgnoreCase))
{
  var refName = match.Groups[1].Value;
  var isDll = refName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase);
  references.Add(MetadataReference.CreateFromFile(LoadContext.LoadFromAssemblyPath(
     isDll ? new Uri(projectRoot, refName).LocalPath :
             new AssemblyName(refName)
  ).Location));

Closing Argument

Thanks for reading this three-post series. I hope you learned something new reading it, I definitely learned a lot writing it.

In case you are interested in board game design, you can download Card Artist for free and use it to generate card rendering for your own games.

I have also written a step-by-step tutorial that shows how to write a template to generate cards for the Middara Unintentional Malum board game. These cards are pretty complex and the tutorial, in addition to showing many useful WPF and Razor techniques, demonstrates how this approach to card design can be used for professional projects.

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0


Written By
Software Developer (Senior) Microsoft
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --