Click here to Skip to main content
15,884,298 members
Articles / Web Development / ASP.NET / ASP.NET Core

Building String Razor Template Engine with Bare Hands (.NET Core)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
25 Feb 2020CPOL2 min read 29.6K   6   9
How ASP.NET Core Razor turns templates into assemblies and runs them
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

C#
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

HTML
@model IndexModel

<h1>@Model.Name</h1>

<ul>
    @foreach (Novel novel in Model.Novels)
    {
        <li>@novel.Year, @novel.Name</li>
    }
</ul>

Output

HTML
<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

C#
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.

C#
GenerateCodeFromTemplate("Hello @Model.Name")
C#
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.

C#
StringBuilder builder = new StringBuilder();

builder.AppendLine("@inherits ConsoleApp9.MyTemplateBase");
builder.Append(@"Hello @Model.Name");

Console.WriteLine(GenerateCodeFromTemplate(builder.ToString()));

Now we have:

Image 1

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
C#
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).

C#
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.

C#
Assembly assembly = Assembly.Load(memoryStream.ToArray()); 
Type templateType = assembly.GetType("MyNamespace.Template");

Now we can create instance and try to run the thing.

C#
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).

C#
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);

        // nested objects and array handling goes here
        // full code: https://github.com/adoconnection/RazorEngineCore/blob/master/
        // RazorEngineCore/AnonymousTypeWrapper.cs

        return true;
    }
}

Finally, we apply the last part of the puzzle:

C#
MyTemplateBase instance = (MyTemplateBase)Activator.CreateInstance(templateType);

var model = new
{
    Name = "Harry Harrison"
};

instance.Model = new AnonymousTypeWrapper(model);
instance.ExecuteAsync().Wait();

Console.WriteLine(instance.Result());

Image 2

Source Code and Nuget Package

History

  • 2020.02.21 - v1
This article was originally posted at https://github.com/adoconnection/RazorEngineCore

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Founder Limetime.io
Russian Federation Russian Federation
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHow to debug the C# part of my complied tamplate Pin
Member 159324484-Apr-23 21:25
Member 159324484-Apr-23 21:25 
AnswerRe: How to debug the C# part of my complied tamplate Pin
Alexnader Selishchev6-Apr-23 2:29
Alexnader Selishchev6-Apr-23 2:29 
GeneralRe: How to debug the C# part of my complied tamplate Pin
Member 159324486-Apr-23 4:43
Member 159324486-Apr-23 4:43 
GeneralRe: How to debug the C# part of my complied tamplate Pin
Alexnader Selishchev18-Feb-24 1:12
Alexnader Selishchev18-Feb-24 1:12 
PraiseThank You Pin
Clystian22-Apr-21 16:17
Clystian22-Apr-21 16:17 
GeneralRe: Thank You Pin
Alexnader Selishchev18-Feb-24 1:12
Alexnader Selishchev18-Feb-24 1:12 
QuestionTag Helpers Pin
Trull3-Mar-20 22:52
Trull3-Mar-20 22:52 
AnswerRe: Tag Helpers Pin
Alexnader Selishchev26-Mar-20 23:57
Alexnader Selishchev26-Mar-20 23:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.