The Bleeding Edge Of Razor

Using the Razor view engine in your own code.

Published on Monday, October 22, 2018

Over the years there's been a number of projects designed to make using Razor templates from your own code easier. For a while, these third-party libraries were the only way to easily use Razor outside ASP.NET MVC because using the ASP.NET code directly was too complicated. That started to change with ASP.NET Core and the ASP.NET team has slowly started to address this use case. In this post we'll take a look at the current bleeding edge of Razor and how you can use it today to enable template rendering in your own application.

Before we start looking at code, let's back up a step and consider what Razor is (and what it isn't). At it's core, Razor is a templating language. Templating languages are designed to make producing output content easier by intermixing raw output with instructions on how to generate additional programmatically-based output. In this case, Razor is used to produce HTML documents. An important distinction that I want to make here is that Razor is not the set of HTML helpers and other support functionality that comes along with ASP.NET MVC. For example, helpers like Html.Partial() and page directives like @section aren't part of the Razor language. Instead they're shipped with ASP.NET MVC as additional support on top of Razor, which your Razor code can use.

This distinction wasn't always clear, but recently the ASP.NET team has been focusing on separating Razor the language from Razor for ASP.NET MVC. This is partly out of necessity as Razor has grown to support at least three different dialects (ASP.NET MVC, Razor Pages, and Blazor), but it also makes using Razor for your own purposes easier too.

Rendering Phases

Turning Razor content from a string, file, or other source into final rendered HTML requires several phases:

  • Generating C# code from the template
  • Compiling the C# code into an assembly
  • Loading the assembly into memory
  • Executing your compiled template

I'll discuss each phase in more detail below. Before I do, note that Razor is under heavy development (and has been for a while). Even though a lot of the API is surfaced as public, it's been known to break in subtle ways between releases. On top of that, I learned most of this through trial-and-error and reverse engineering and make no assurances that this is the canonical way or even a correct way of doing any of this. You've been warned.

Generating Code

A Razor template starts life as a string (or file) with intermixed HTML, C# code, and Razor directives. You can think of this template as a little program that takes input like your page model and outputs the resulting HTML. Like any program it needs to be compiled and executed. The first part of this process essentially "inverts" the HTML and C# code in the template and creates C# code that "prints" the HTML parts of your template along with the raw code that you added to your template.

This phase is where a lot of the recent work in Razor has been focused. It used to be that the process of converting a Razor template to C# code happened as part of the overall MVC Razor processing. Now, a series of libraries under Microsoft.AspNetCore.Razor.Language separates Razor the language from Razor for ASP.NET MVC.

Here's how to take a Razor template stored in the file C:\Code\RazorExample\date.cshtml and generate C# from it (you'll need to add the Microsoft.AspNetCore.Razor.Language package to get access to these classes):

RazorConfiguration config = RazorConfiguration.Default;
RazorProjectFileSystem projectFileSystem =
  RazorProjectFileSystem.Create(@"C:\Code\RazorExample");
RazorProjectEngine projectEngine =
  RazorProjectEngine.Create(config, projectFileSystem);
RazorProjectItem projectItem = projectFileSystem.GetItem("date.cshtml");
RazorTemplateEngine templateEngine =
  new RazorTemplateEngine(projectEngine.Engine, projectFileSystem);
RazorCodeDocument codeDocument = templateEngine.CreateCodeDocument(projectItem);
RazorCSharpDocument cSharpDocument = templateEngine.GenerateCode(codeDocument);         

Given a data.cshtml file that looks like this:

<p>@DateTime.Now</p>

This will produce the following C# code in cSharpDocument.GeneratedCode:

#pragma checksum "E:\Code\NewRazor\date.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "7dea33102781d0fc7059874abc785e31de14ef37"
// <auto-generated/>
#pragma warning disable 1591
[assembly: global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute(typeof(Razor.Template), @"default", @"/date.cshtml")]
namespace Razor
{
    #line hidden
    [global::Microsoft.AspNetCore.Razor.Hosting.RazorSourceChecksumAttribute(@"SHA1", @"7dea33102781d0fc7059874abc785e31de14ef37", @"/date.cshtml")]
    public class Template
    {
        #pragma warning disable 1998
        public async override global::System.Threading.Tasks.Task ExecuteAsync()
        {
            WriteLiteral("<p>");
#line 1 "E:\Code\NewRazor\date.cshtml"
Write(DateTime.Now);

#line default
#line hidden
            WriteLiteral("</p>");
        }
        #pragma warning restore 1998
    }
}
#pragma warning restore 1591

Let's break that down just a little bit...

The RazorProjectFileSystem is responsible for presenting available files and their content to the Razor engine. It's primary job is to create RazorProjectItem instances given a path. These RazorProjectItem objects contain metadata about the requested file as well as access to a Stream (if the file exists). The default RazorProjectFileSystem obtained by the call to RazorProjectFileSystem.Create(string root) is aptly named DefaultRazorProjectFileSystem and wraps System.IO classes like FileInfo and FileStream. If you want to access files differently (like from a database), you'll need to implement your own RazorProjectFileSystem and RazorProjectItem.

The RazorProjectEngine is the workhorse here. It slices up your template, applies a sequence of processing phases to it to construct a syntax tree, and then lowers that syntax tree into C#. If you need to adjust the way Razor generates your code, it'll probably be through the RazorProjectEngine. In future posts I'll probably take a look at some of these possibilities.

Like the RazorProjectEngine, the RazorTemplateEngine also participates in generating code. It's main job is essentially to add imports and other required functionality to your generated code and then defer to the RazorProjectEngine for processing of the syntax tree.

Finally, RazorCodeDocument contains the abstract representation of your template and RazorCSharpDocument contains the final produced C# code.

Compiling The Code

Now that we have some C# code, we need to compile it. We're done with the Razor language bits (at least for now) and we'll use Roslyn to compile our code:

SourceText sourceText = SourceText.From(cSharpDocument.GeneratedCode, Encoding.UTF8);
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(sourceText);
CSharpCompilationOptions compilationOptions =
    new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
        .WithSpecificDiagnosticOptions(
            new Dictionary<string, ReportDiagnostic>
            {
                // Binding redirects
                { "CS1701", ReportDiagnostic.Suppress },
                { "CS1702", ReportDiagnostic.Suppress },
                { "CS1705", ReportDiagnostic.Suppress },
                { "CS8019", ReportDiagnostic.Suppress }
            });
CSharpCompilation compilation =
    CSharpCompilation.Create(
        "RazorTest",
        options: compilationOptions,
        references: GetMetadataReferences())
    .AddSyntaxTrees(syntaxTree);

In the first step we're loading the code in cSharpDocument.GeneratedCode into a Roslyn SourceText and then constructing a Roslyn SyntaxTree from it (which is different than a Razor syntax tree).

In the next statement, we're creating the options for our compilation. Specifically, we want to produce a library so we use OutputKind.DynamicallyLinkedLibrary and then turn off certain diagnostics that we know will be troublesome (you can adjust the list of suppressed diagnostics however you see fit).

In the last statement we prepare the code for compilation by using a Roslyn CSharpCompilation. This uses a factory .Create() method that takes a variety of arguments. In the code above, we're passing the name of the assembly ("RazorTest"), the options we created in the statement above, and a list of references we got by calling GetMetadataReferences() (more on that in just a second). The last call to our new CSharpCompilation object adds the syntax tree we constructed earlier.

As with any compiled code, the compiler needs to reference other libraries to find functionality. Some of these are in-the-box code libraries (like CoreFx) and others are your own assemblies that your Razor template uses. I separated this part into a GetMetadataReferences() method to keep the code clean:

private static List<MetadataReference> GetMetadataReferences() =>
    new List<MetadataReference>()
    {
        GetMetadataReference(typeof(InputTagHelper)),
        GetMetadataReference(typeof(UrlResolutionTagHelper)),
        GetMetadataReference(typeof(RazorCompiledItemAttribute)),
        GetMetadataReference(typeof(IModelExpressionProvider)),
        GetMetadataReference(typeof(IUrlHelper)),
        GetMetadataReference(typeof(object)),
        GetMetadataReference(typeof(DynamicAttribute)),
        GetMetadataReference(
            "System.Runtime, Version=0.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"),
        GetMetadataReference(
            "netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51")
    };

private static MetadataReference GetMetadataReference(Type type) =>
    MetadataReference.CreateFromFile(type.GetTypeInfo().Assembly.Location);

private static MetadataReference GetMetadataReference(string assemblyName) =>
    MetadataReference.CreateFromFile(Assembly.Load(assemblyName).Location); 

This code either loads assembly references by using a type that we know to be in the assembly or using the full name of the assembly (assuming the assembly binder can find it). This set of references should support a minimal Razor template compilation, but you may need to add or adjust it depending on your own template.

Loading The Assembly

In the interest of full disclosure, the code in the previous section doesn't actually compile our template, it just sets up the Razor compiler. The actual compilation happens in this phase at the same time we emit our new template assembly to memory:

Assembly assembly;
EmitOptions emitOptions =
    new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb);
using (MemoryStream assemblyStream = new MemoryStream())
{
    using (MemoryStream pdbStream = new MemoryStream())
    {
        EmitResult result = compilation.Emit(
            assemblyStream,
            pdbStream,
            options: emitOptions);

        if (!result.Success)
        {
            List<Diagnostic> errorsDiagnostics = result.Diagnostics
                .Where(d => d.IsWarningAsError || d.Severity == DiagnosticSeverity.Error)
                .ToList();
            foreach (Diagnostic diagnostic in errorsDiagnostics)
            {
                FileLinePositionSpan lineSpan =
                    diagnostic.Location.SourceTree.GetMappedLineSpan(
                        diagnostic.Location.SourceSpan);
                string errorMessage = diagnostic.GetMessage();
                string formattedMessage =
                    "("
                    + lineSpan.StartLinePosition.Line.ToString()
                    + ":"
                    + lineSpan.StartLinePosition.Character.ToString()
                    + ") "
                    + errorMessage;
                Console.WriteLine(formattedMessage);
            }
            return;
        }

        assemblyStream.Seek(0, SeekOrigin.Begin);
        pdbStream.Seek(0, SeekOrigin.Begin);

        assembly = Assembly.Load(assemblyStream.ToArray(), pdbStream.ToArray());
    }
}

Most of the work here happens in the compilation.Emit() method. We pass it an options object that tells it we want to produce a portable PDB (which will get embedded in the in-memory assembly and can be used for debugging the template). This method compiles and serializes the assembly to a stream.

The bulk of the code here deals with error reporting. Once the compilation and emit is done, the EmitResult object will contain a Success property that tells you if the compilation was successful. If it wasn't, you can get compilation errors by examining the EmitResult.Diagnostics property. The rest of the code above just formats a nice message using Roslyn line span information (normally, I'd create formattedMessage using string interpolation, but I used string concatenation instead to make it clearer what's going on for this post).

Finally, we reset the assembly and PDF streams to the start (now that Roslyn has written to them) and pass them to Assembly.Load() to construct an in-memory assembly we can use in the next phase.

Executing The Template

At this point we have an assembly that contains the compiled version of our template. All we have to do now is run it:

RazorCompiledItemLoader loader = new RazorCompiledItemLoader();
RazorCompiledItem item = loader.LoadItems(assembly).SingleOrDefault();
RazorPage<dynamic> page = (RazorPage<dynamic>)Activator.CreateInstance(item.Type);
TextWriter writer = new StringWriter();
page.ViewContext = new ViewContext()
{
    Writer = writer
};
page.HtmlEncoder = HtmlEncoder.Default;
page.ExecuteAsync().GetAwaiter().GetResult();
Console.WriteLine(writer.ToString());

The RazorCompiledItemLoader knows how to use reflection to find the class that represents your template in the assembly. Information about that class gets returned as a RazorCompiledItem which, among other things, contains the type of your template class.

We can create an instance of the class using Activator (though you can certainly use expression trees or some other mechanism to instantiate it via reflection). By default, Razor templates inherit from RazorPage<TModel> and the default model is dynamic so the instance we end up with is a RazorPage<dynamic> (also why we needed to make sure we loaded the assembly that contains DynamicAttribute when gathering MetadataReference objects, because that assembly is responsible for dynamic support).

When a RazorPage is executed, it requires a few things like a ViewContext and a HtmlEncoder. The code above creates a minimal ViewContext and you'll need to populate it further if your template uses other view features like the ViewBag. Then we call RazorPage.ExecuteAsync() to execute the template and get rendered HTML (I call it synchronously above, but presumably you'd be calling it in an async method and would await the call).

Bringing It All Together

Now that we've walked through how to do this on your own, it's time to mention that there are already libraries that do this for you using the new ASP.NET Core Razor engine. Two of my favorites are Gazorator (by my friend Martin Björkström, without whom this post probably never would have happened) and RazorLight. If you want to customize the process or have full control over the phases, the code above should get you started. However, if you just want to turn a Razor template into HTML I'd consider using one of these libraries to abstract all these details from your code.

But What About MVC?

If you start adding MVC conventions to your templates, you'll notice they either result in failures or just plain don't work. For example, if you add a layout to your template:

@{
    Layout = "_MyLayout.cshtml";
}

The layout simply won't be rendered. That's because the Razor language bits discussed above are a little bit leaky with regards to MVC. For example, the default RazorPage does have a Layout property so setting it in your template won't cause the compilation to fail. However, the out-of-the-box Razor language engine we use above doesn't know anything about layouts or how to render them. I'm planning on following up this post in the near future with an even deeper dive into the Razor engine where I'll discuss how to light up the MVC version of Razor you know and love and the extensibility mechanisms that are used to do so.

comments powered by Disqus