Many .NET developers are familiar with Razor syntax. It's found in ASP.NET Core views (.cshtml), Razor pages, Razor component files (.razor), Blazor, Razor class libraries, and more. In general, Razor provides features for evaluating .NET code such as C# inside HTML. Despite being deceptively simple with only a handful of syntax primitives, there's a lot going on behind the scenes to make everything work correctly. In this post, you'll explore the fundamentals of the Razor engine. Then you’ll learn how it processes your Razor syntax to produce .NET assemblies and runs those assemblies to create content. Then in a follow-up post, once you've got a good handle on the Razor engine, you'll look at some open-source tools that leverage the Razor engine in various ways beyond what ships in .NET Core and how you can leverage these libraries to create Razor templates for sending emails or for other purposes. By the time you’ve read both parts, you should have a good handle on both the architecture and usage of Razor for your own purposes outside ASP.NET.
What is Razor
Before getting into any technical details of the Razor engine, take a step back and consider templating languages in general. Consider the word template and how it describes something to be filled in, like a stencil. In other words, a templating language allows you to mix raw output with programmatic statements to produce a result.
Behind the scenes, most templating languages do this by "flipping" the template document into a program where the programmatic portions of the template are executed and the raw content portions are output as strings by code. There are many templating languages, including Handlebars, Mustache, and Liquid. Most templating languages are specialized for a particular host language and Razor is no exception. Razor is designed specifically for use with C# and other .NET code.
For example, consider the following short Razor template:
<div>Hi Dave,</div> <div>Today is @DateTime.Now.ToString("m").</div> <div>See you later!</div>
The output of this template would look something like:
<div>Hi Dave,</div> <div>Today is March 15.</div> <div>See you later!</div>
Notice how the
@DateTime.Now.ToString("m") is a programmatic statement that needs to be evaluated. A simplified version of the code Razor generates for this template might look like:
await writer.WriteLineAsync("<div>Hi Dave,</div>"); await writer.WriteAsync("<div>Today is "); await writer.WriteAsync(DateTime.Now.ToString("m")); await writer.WriteLineAsync(".</div>"); await writer.WriteLineAsync("<div>See you later!</div>");
It's worth noting that there's a distinction between templating languages and markup languages. While a templating language like Razor mixes programmatic control and evaluation with raw output, a more general markup language without templating features often describes output alone using special syntax. A good example of such a markup language is the Markdown markup language. The content you write in Markdown is transformed into something different based on the Markdown syntax being used (i.e.
_emphasis_ is rendered as
<em>emphasis</em>), but there's no provision for variable evaluation or controlling the output based on programmatic constructs. In a templating language, you generally write raw output intermixed with code, while in a markup language you typically write pseudo-output that's intended to be transformed into the final content.
Razor as an Engine
It's tempting to oversimplify how Razor works due to it being a low-level black-box that you don't need to look inside or understand. However, because of all the different environments in which Razor is used, the underlying technology needs to be both flexible and extensible. Most of the time when you talk about Razor, you're talking about the ASP.NET Core implementation of the Razor engine. This is what provides mechanisms like partial views, layouts, tag helpers, and more. However, underneath all that is a more general purpose Razor engine that understands the Razor syntax and how to process it, but doesn't know anything about Razor in the context of an ASP.NET Core website or page. The Razor functionality that you know and love from ASP.NET Core is actually built on top of this general purpose engine.
Each "flavor" of Razor like ASP.NET Core MVC, Blazor, etc. implements this general Razor engine in slightly different ways and with slightly different conventions and capabilities. It's also possible to build custom implementations of Razor that leverage the same general engine, and there are several third-party libraries that do this with varying degrees of compatibility with the ASP.NET Core's Razor implementation (more on these in the next post). All the while, the underlying Razor engine is gradually becoming more complex to support a growing list of environments and features such as Blazor and being used from within MSBuild tasks.
The distinction between the Razor engine and the ASP.NET Core implementation is important because not all tools that can process Razor syntax using the general purpose Razor engine will do so with support for all the features implemented in ASP.NET Core. This might seem confusing or even broken if you're thinking of Razor as a single monolithic library. To understand this better, you'll need to dig deeper into the general purpose Razor engine and look at each of the four primary phases it goes through to process your document.
Razor Processing Phases
When you compile and then execute a Razor template, the engine performs work in roughly four phases: parsing, code generation, compilation, and execution. Note that this is simplified because many of these steps are recursive or can happen in a different order, but it's generally correct at a high level.
At various stages of the process, the Razor engine can also perform additional work like caching, watching for file changes, re-processing, etc. By hooking into one or more of these phases, a library can provide Razor functionality outside the scope of the ASP.NET Core implementation and can even layer on portions of the ASP.NET Core functionality in addition to its own.
In this first phase, the template (i.e. your document) is parsed and any Razor syntax that indicates C# code or control structures are separated from raw output content. There are actually two parsers involved in this phase. In the first pass, the Razor parser looks for syntax that indicates a switch from code to content or vice-versa, such as
@ or HTML elements (when in code). Then, once the code has been identified and isolated from the content, the Roslyn (C# compiler) parser examines the code portions and a syntax tree is constructed from the template that includes nodes for both code and content.
If you’re unfamiliar with syntax trees (sometimes called “abstract syntax trees” or “ASTs”), they provide a representation of the structure of a document as a tree. Each node in the tree represents a single logical portion of syntax from the document, and the hierarchy of the tree reflects how that syntax is nested. Generally, a syntax tree omits non-relevant text such as whitespace (often called “trivial text” or “trivia”). In this case, the inclusion of whitespace and other trivia depends on whether the node in the syntax tree represents C# code or raw output content.
Using the syntax tree, the template is then converted into C# code that can be executed. The code generation happens in a series of phases, and each phase can manipulate the generated code from the previous phase. This allows various Razor features or even entire implementations of the Razor engine to incrementally augment the code generation that came before it. This part is performed in an intermediate tree structure until all the phases are complete and the actual final C# code can be generated.
The result of the code generation phase is typically a class for each Razor document, with a specific rendering method as the entry point to call when you want to execute (i.e. render) that particular Razor document. Raw output in the original template is converted into statements that write the raw output to a stream passed into the rendering method. In most Razor implementations, this rendering method is named
RenderAsync and the class that the rendering method is in usually derives from a base view class that represents the model type as a generic type parameter. This is why using the property
Model within your Razor template is strongly typed.
After the Razor template has been processed and code has been generated, the generated code is compiled into an assembly using Roslyn (the C# compiler). Depending on the environment and other conditions like cache settings, this assembly might only exist in memory, or it might be cached to disk for faster access later.
When it comes time to render a Razor document into final content, the render method in the compiled assembly is executed by the host (i.e. the ASP. NET Core runtime). This often requires some sort of project system to locate files like partial views, layouts, etc. and recursively process and render them as well. Other runtime information from outside the Razor engine like view context is also passed into the render method in this phase. The separation of compilation from execution allows the Razor template to be processed once, but then rendered multiple times with different variables and external context.
By peeking behind the scenes of Razor, I hope you've gained an appreciation for how flexible the Razor engine is and what really happens when you render that web page. In this next post, you'll take a look at some available open-source implementations that hook into the Razor engine in various ways and make it easier to use Razor outside of ASP.NET projects. More specifically, you'll consider how Razor templates can be leveraged in combination with open-source Razor engines to easily create powerful dynamic email templates to send emails with SendGrid in just a few lines of code.
Dave Glick has been professionally developing software for nearly two decades, most recently on the .NET platform. He is a Microsoft MVP in the Visual Studio and Development Technologies category and is currently the Principal Software Engineer for a small non-profit where he architects and leads the development of line of business applications, data forensics tools, and DevOps infrastructure. He is passionate about open source and its community, publishes several projects of his own, and has contributed to many others.
Configure autocreate of Twilio Conversations on incoming SMS, and automatically add participants and messages to the conversation using Google Cloud Functions and C# .NET.
Use C# .NET to schedule SMS to organize a mysterious date using Twilio SMS.
Learn how to read secrets and sensitive information from HashiCorp Vault and load it into .NET configuration with C#.
Learn about the new features, improvements, and breaking changes in the 7th release of the Twilio helper library for ASP.NET.
Learn how to retrieve the Visual Studio dev tunnel URL and use it to automatically update your Twilio Webhook URLs during startup of your ASP.NET Core application.
Develop a C# .NET application to generate spooky audio with Amazon Polly, upload the audio to Amazon S3, and then play your spooky audio as a phone call using Twilio Voice.