Using the Razor parser outside of ASP.Net

When Scott Guthrie originally blogged about Razor, he mentioned that it was fully hostable outside of ASP.Net.  The engine itself is not quite as detached from System.Web as we’d like, but it’s close and we’re going to get it way closer in the next release.

Having said that, you can still host Razor outside of the ASP.Net pipeline with the current beta! It’s a little trickier, and you do technically need to reference System.Web.  I’ve written a sample console app that I’m attaching to this post called “rzrc” which takes in  a .cshtml or .vbhtml Razor file and runs it through the parser and code generator to produce a .cs or .vb file.  I’ll walk through the main logic here and go over what each section does.

However, I was not the first to do this! Full credit for that goes to Gustavo Machado, who wrote an excellent post in which he used Reflector to work out how to run the Razor parser and code generator.  Well done Gustavo!  There are a few things that this version does that Gustavo’s doesn’t, such as cleaning up the Web-related stuff in the generated code and selecting the language based on the Code Language, but he basically hit it spot on!

The first thing my console app does is get the input file name, extract the extension and look up what Razor Code Language it uses.  This is done using the CodeLanguageService class, which is part of the Razor APIs:

CodeLanguageService languageService = CodeLanguageService.GetServiceByExtension(extension);
if (languageService == null) {
    Console.WriteLine("{0} is not a Razor code language", extension);
    return;
}

Then, we fire up the parser and the code generator.  A CodeLanguageService is basically a factory for constructing a Code Parser, to parse the code blocks after an “@” and a matching Code Generator to write the final C# or VB class.

InlinePageParser parser = new InlinePageParser(languageService.CreateCodeParser(), new HtmlMarkupParser());
CodeGenerator codeGenerator = 
    languageService.CreateCodeGeneratorParserListener(className,
                                                        rootNamespaceName: "Template", 
                                                        applicationTypeName: "object", 
                                                        inputFileName, 
                                                        baseClass: "System.Object");

When you run the Razor Parser, you must provide it with an object implementing IParserConsumer.  This interface has callbacks which the parser will call when it encounters various Razor constructs (more details on the Razor parse tree later).  CodeGenerator implements this interface and responds to the these callbacks by generating code.  However, it does nothing with the errors, so in the console app, I’ve written a very simple IParserConsumer called CustomParserConsumer which wraps the code generator and outputs errors to the console.  I won’t put the code here, but it’s in the sample, so take a look there if you’re interested.

Now that we’ve got all the objects we need, we can actually run the parser over the input

CustomParserConsumer consumer = new CustomParserConsumer() { CodeGenerator = codeGenerator };
using (StreamReader reader = new StreamReader(inputFileName)) {
    parser.Parse(reader, consumer);
}

Once Parse returns, the Code Generator will have built a CodeDOM tree representing the generated code during the callbacks, so we know that our code is ready to go.  Right now, the Code Generator adds in some web specific things.  For example, when we constructed the Code Gneerator above, we gave it an “applicationTypeName” which (in a web context) is the type name of the class defined in Global.asax, if there is one.  Since we are trying to generate a template that isn’t related to the web, we can get the CodeDOM tree from the Code Generator and remove these things.

codeGenerator.GeneratedCode.Namespaces[0].Types[0].Members.RemoveAt(0);
codeGenerator.GeneratedCode.Namespaces[0].Types[0].BaseTypes.Clear();
codeGenerator.GeneratedCode.Namespaces[0].Imports.Clear();

Finally, we use the CodeDOM to write the code to a C# or VB class file (provider is a CodeDomProvider from System.CodeDom.Compiler):

using (StreamWriter writer = new StreamWriter(outputFile)) {
    provider.GenerateCodeFromCompileUnit(codeGenerator.GeneratedCode, writer, new CodeDom.CodeGeneratorOptions());
}

And we’re done!  This is definitely more complicated than we’d like, but there are plans to simplify this API significantly in future releases.  For the most part, all we’ve done is left the methods our ASP.Net Build Provider uses open and accessible.  I wouldn’t bet on these APIs staying around too long, but any API changes from here on should be simplifications.  For now though, check out the sample I’ve attached and play around!  Note that you must have WebMatrix installed to use the sample. 

I’ve put some comments in which start with “EXT” which contain tips on how to extend this code to your own use.  Please feel free to take this code and use it absolutely anywhere you want!  Let me know how your using Razor by either tweeting me at @anurse or email me at andrew AT andrewnurse DOT net.

Download the console app here: rzrc.zip (3.46 KB)

Wednesday, July 21, 2010 10:49:12 PM (Pacific Standard Time, UTC-08:00)
Killer - and JUST what I was hoping we'd be able to do with Razor. I really get the sense that it's way more powerful and has way more potential than just web/html output.

That said,I'm going to be an ASS and point out that you're generating a .cs or .vb file from a file with an extension with 'html' in it.

In other words, I could use this to make a KILLER code generator, but in the end, my 'source' files would need to be either .cshtml or .vbhtml files - even if I were using those files to generate dmbl, rss, html, cs, atom, xml, or whatever.
Wednesday, July 21, 2010 11:12:36 PM (Pacific Standard Time, UTC-08:00)
I did some experiments with razor 2 weeks ago to generate my code on-the-fly (
http://www.ienumerable.it/blog/post/razor) thanks to reflector.
Waiting for more detailed API documentation / samples to bring the power of razor inside my applications (and get rid of NVelocity).

Good job!
Thursday, July 22, 2010 4:19:43 AM (Pacific Standard Time, UTC-08:00)
What about 'Engine.merge(template, object)'

KISS
Steve
Thursday, July 22, 2010 6:40:15 AM (Pacific Standard Time, UTC-08:00)
It seems to me that perhaps Razor could replace T4. I like how you can use T4 but really dislike having to write any of it. Razor would make the experience of generating code much cleaner.
Thursday, July 22, 2010 11:04:00 AM (Pacific Standard Time, UTC-08:00)
Interesting if we could generate XML/XAML/RSS/Atom (*.csxml) with "Razor".
mentas
Thursday, July 22, 2010 12:52:45 PM (Pacific Standard Time, UTC-08:00)
Thanks for mentioning my post Andrew! Great post as always! I'll incorporate this stuff to my solution right away.

@Michael, I didn't like the fact that I had to name my templates *.cshtml either because I was looking for a template engine, and I didn't have to! Andrew is using the service to figure out the parsers based on the file extension, but you can do it manually and let your files have the extension you like the most, like *.csrzr for instance (which is the one I'm using right now :))

Cheers!
Gus
Thursday, July 22, 2010 12:57:08 PM (Pacific Standard Time, UTC-08:00)
@Bret: I totally agree with you, Razor looks much more powerful than T4 templates. After you wrap this logic, calling templates is pretty easy. Check out at how I call child templates from inside a razor template http://thegsharp.wordpress.com/2010/07/16/razor-from-a-console-calling-child-templates/

@mentas: I am using Razor to generate all kinds of stuff, json, xml, even c# code. Really, file extensions are not a problem.

Cheers!
Thursday, July 22, 2010 1:00:53 PM (Pacific Standard Time, UTC-08:00)
That's awesome, but would be awesome-er if we could specify a custom base class for the .xyHtml file.
I know that I *can* do this in the codeDom part, but then the template file will not have the intellisense of my custom base class.

Oh, and please please please, when you *do* allow custom base classes, please do not require them to inherit from System.Web.UI.Page (or from anything for that matter) - as it would make absolutely no sense when trying to use templates outside of an HttpContext
Friday, July 23, 2010 5:49:40 AM (Pacific Standard Time, UTC-08:00)
What I have see, only cshtml/vbhtml extensions are valid.

I suggest a standalone/generic engine that works for web, winforms, services, etc. and unlimited extensions (csxaml, csxml, csrss, etc).

I see scenarios like XML data feeds, XAML templates, auto content generators, etc. I see "Razor" more than a "HTML" render engine, it can be powerful to many other projects.

Good Work!
mentas
Sunday, July 25, 2010 5:38:15 PM (Pacific Standard Time, UTC-08:00)
Razor doesn't have any directives, we felt that everything we needed in a directive could be achieved in code or with more specific syntax. What's the scenario you're trying to accomplish?
--->
Hi, sorry for the long reply,
something like this
<%@Page Language="C#" ContentType="text/css" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>

apparantly, the old asp view already able to generate css, javascript, xml
but, no such page directive you said, on razor,
So what is your suggested approach ?
I hope it does not looks like on this post (Using the razor parser outside asp.net), by you mean achieved in code

Thanks

Andrew
Monday, July 26, 2010 10:39:05 PM (Pacific Standard Time, UTC-08:00)
@mentas - CSHTML/VBHTML are the only extensions supported by the web environment, but you can have those files _generated_ any file you want. The console app I posted uses the list of extensions from the list in CodeLanguageService, but you can use whatever extension you want and manually specify the code language. Having said that, you're right, it would be good to have a generic Razor engine that is separate from the web. We've been thinking the same thing ;)

@Andrew - Everything in that directive can be achieved in Razor. The file extension determines the language so that covers the Language attribute.

There is an @inherits keyword in the MVC preview (it did NOT make the WebMatrix beta, but it is in the upcoming MVC preview and will be in the next WebMatrix beta). You use it like this:

@inherits System.Web.Mvc.ViewPage<dynamic>

For Content-Type, there is a property on Response you can use, like this:

@{Response.ContentType = "text/css"}

Note that because you're setting a property, not calling a method which returns a value, you need "@{}"
Monday, August 09, 2010 12:05:28 AM (Pacific Standard Time, UTC-08:00)
re: @Steve "What about 'Engine.merge(template, object)'"

I agree - will there be support for loading templates from an in-memory string with a very simple setup procedure?
Monday, August 09, 2010 10:37:59 PM (Pacific Standard Time, UTC-08:00)
We're looking at what the exact APIs will be, but at the moment, what we've got is a way to easily set up the template engine, pass in a stream containing some Razor code and get back a CodeDOM tree containing the generated code. You'll also get the Parse Tree structure for free, so you can do what you want with the actual parsed document.

Working within the compiled model of C#/VB means that Razor templates can't be easily interpreted dynamically (i.e. "Engine.merge"). The C#/VB teams have been talking about writing a managed compiler and providing some APIs to consume that in interesting ways, so we might be able to investigate interpretation then. Until then, you could definitely write some code to generate code for a template, compile it, and load it up. It would be slow the first time, but you could easily cache that Assembly somewhere and keep it in sync with the template using a checksum, like ASP.Net does with pages.
Name
E-mail
(will show your gravatar icon)
Home page

Comment (Some html is allowed: a@href@title, strike) where the @ means "attribute." For example, you can use <a href="" title=""> or <blockquote cite="Scott">.  

Enter the code shown (prevents robots):

Live Comment Preview