In this post, you will see how ASP.NET Core Razor turns templates into assemblies and runs them. You will also see the steps to make our string template engine based on Razor, to use outside of ASP.NET.
We Will
- See how ASP.NET Core Razor turns templates into assemblies and runs them
- Walk through steps to make our string template engine based on Razor, to use outside of ASP.NET
We Will Not
- Examine how Razor parser works in particular
Random Reader Recap / Intro
Razor is a templating engine for ASP.NET MVC views. It designed to apply model to templates to result in HTML pages.
HomeController.cs
public class HomeController : Controller
{
public IActionResult Index()
{
IndexModel model = new IndexModel()
{
Name = "Harry Harrison",
Novels = new List<Novel>()
{
new Novel()
{
Name = "Deathworld",
Year = 1960
},
new Novel()
{
Name = "Spaceship Medic",
Year = 1970
},
}
};
return this.View(model);
}
}
Index.cshtml
@model IndexModel
<h1>@Model.Name</h1>
<ul>
@foreach (Novel novel in Model.Novels)
{
<li>@novel.Year, @novel.Name</li>
}
</ul>
Output
<h1>Harry Harrison</h1>
<ul>
<li>1960, Deathworld</li>
<li>1970, Spaceship Medic</li>
</ul>
How Does It Work?
1. Template Parsing
Razor templates are compiled so at first Razor needs to translate string template into C# code.
We will use the latest ASP.NET Core Razor package: Microsoft.AspNetCore.Razor.Language
string GenerateCodeFromTemplate(string template)
{
RazorProjectEngine engine = RazorProjectEngine.Create(
RazorConfiguration.Default,
RazorProjectFileSystem.Create(@"."),
(builder) =>
{
builder.SetNamespace("MyNamespace");
});
string fileName = Path.GetRandomFileName();
RazorSourceDocument document = RazorSourceDocument.Create(template, fileName);
RazorCodeDocument codeDocument = engine.Process(
document,
null,
new List<RazorSourceDocument>(),
new List<TagHelperDescriptor>());
RazorCSharpDocument razorCSharpDocument = codeDocument.GetCSharpDocument();
return razorCSharpDocument.GeneratedCode;
}
Calling GenerateCodeFromTemplate
will result in actual class source code.
GenerateCodeFromTemplate("Hello @Model.Name")
namespace MyNamespace
{
public class Template
{
public async override global::System.Threading.Tasks.Task ExecuteAsync()
{
WriteLiteral("Hello ");
Write(Model.Name);
}
}
}
Class name will always be Template
under namespace you have chosen, MyNamespace
in my case.
This code will not compile as the WriteLiteral
and Write
functions are not defined, we need to make Template
inherit something in order to make it work.
StringBuilder builder = new StringBuilder();
builder.AppendLine("@inherits ConsoleApp9.MyTemplateBase");
builder.Append(@"Hello @Model.Name");
Console.WriteLine(GenerateCodeFromTemplate(builder.ToString()));
Now we have:
Let's define MyTemplateBase
to be ready to compile template.
It has three important members:
public dynamic Model { get; set; }
– that model we will use in template to reference data public abstract Task ExecuteAsync();
– template entry point to start execution public string Result()
– something to get result in future
public abstract class MyTemplateBase
{
private readonly StringBuilder stringBuilder = new StringBuilder();
public dynamic Model { get; set; }
public abstract Task ExecuteAsync();
public void WriteLiteral(string literal)
{
this.stringBuilder.Append(literal);
}
public void Write(object obj)
{
this.stringBuilder.Append(obj);
}
public string Result()
{
return this.stringBuilder.ToString();
}
}
2. Compiling
We will use Roslyn to compile this code. Package: Microsoft.CodeAnalysis.CSharp
In order to build something with Roslyn, you need to build SyndexTree
(s) and reference assemblies (as you would do in regular console app).
static MemoryStream Compile(string assemblyName, string code)
{
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(code);
CSharpCompilation compilation = CSharpCompilation.Create(
assemblyName,
new[]
{
syntaxTree
},
new []
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(MyTemplateBase).Assembly.Location),
MetadataReference.CreateFromFile(typeof(DynamicObject).Assembly.Location),
MetadataReference.CreateFromFile(
Assembly.Load(new AssemblyName("Microsoft.CSharp")).Location),
MetadataReference.CreateFromFile(
Assembly.Load(new AssemblyName("netstandard")).Location),
MetadataReference.CreateFromFile(
Assembly.Load(new AssemblyName("System.Runtime")).Location),
},
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
MemoryStream memoryStream = new MemoryStream();
EmitResult emitResult = compilation.Emit(memoryStream);
if (!emitResult.Success)
{
return null;
}
memoryStream.Position = 0;
return memoryStream;
}
Bingo, now we have assembly byte code right in our memory stream!
3. Running
Let's load byte code.
Assembly assembly = Assembly.Load(memoryStream.ToArray());
Type templateType = assembly.GetType("MyNamespace.Template");
Now we can create instance and try to run the thing.
MyTemplateBase instance = (MyTemplateBase) Activator.CreateInstance(templateType);
instance.Model = new
{
Name = "Harry Harrison"
};
instance.ExecuteAsync().Wait();
Console.WriteLine(instance.Result());
This will result in error as the anonymous object's property cannot be accessed right away.
'object' does not contain a definition for Name
To overcome this, we will use wrapper based on DynamicObject
(array and nested objects handling removed for brevity).
public class AnonymousTypeWrapper : DynamicObject
{
private readonly object model;
public AnonymousTypeWrapper(object model)
{
this.model = model;
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
PropertyInfo propertyInfo = this.model.GetType().GetProperty(binder.Name);
if (propertyInfo == null)
{
result = null;
return false;
}
result = propertyInfo.GetValue(this.model, null);
return true;
}
}
Finally, we apply the last part of the puzzle:
MyTemplateBase instance = (MyTemplateBase)Activator.CreateInstance(templateType);
var model = new
{
Name = "Harry Harrison"
};
instance.Model = new AnonymousTypeWrapper(model);
instance.ExecuteAsync().Wait();
Console.WriteLine(instance.Result());
Source Code and Nuget Package
History