Click here to Skip to main content
14,493,767 members

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

Rate this:
5.00 (2 votes)
Please Sign up or sign in to vote.
5.00 (2 votes)
25 Feb 2020CPOL
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

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:

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

        // 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:

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

License

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

Share

About the Author

ADOConnection
Web Developer
Russian Federation Russian Federation
No Biography provided

Comments and Discussions

 
QuestionTag Helpers Pin
Trull3-Mar-20 22:52
MemberTrull3-Mar-20 22:52 
AnswerRe: Tag Helpers Pin
ADOConnection26-Mar-20 23:57
MemberADOConnection26-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.

Stats

10.4K views
4 bookmarked