Earlier in November, the ASP.NET Monsters had the opportunity to take part in the ASP.NET Core hackathon at the Microsoft MVP Summit. In past years, we have used the hackathon as an opportunity to spend some time working on GenFu. This year, we wanted to try something a little different.
The Crazy Idea
A few months ago, we had Taylor Mullen on The Monsters Weekly to chat about Razor in ASP.NET Core. At some point during that interview, it was pointed that MVC is designed in a way that a new view engine could easily be plugged into the framework. It was also noted that implementing a view engine is a really big job. This got us to thinking…what if we could find an existing view engine of some sort. How easy would it be to get actually put a new view engine in MVC?
And so, that was our goal for the hackathon. Find a way to replace Razor with an alternate view engine in a single day of hacking.
Finding a Replacement
We wanted to pick something that in no way resembled Razor. Simon suggested Pug (previously known as Jade), a popular view template engine used in Express. In terms of syntax, Pug is about as different from Razor as it possibly could be. Pug uses whitespace to indicate nesting of elements and does away with angle brackets all together. For example, the following template:
div |
would generate this HTML:
<div> |
Calling Pug from ASP.NET Core
The first major hurdle for us was figuring out a way to compile pug templates from within an ASP.NET Core application. Pug is a JavaScript based template engine and we only had a single day to pull this off so a full port of the engine to C# was not feasible.
Our first thought was to use Edgejs to call Pug’s JavaScript compile function. Some quick prototyping showed us that this worked but Edgejs doesn’t have support for .NET Core. This lead us to explore the JavaScriptServices packages created by the ASP.NET Core team. Specifically the Node Services package which allows us to easily call out to a JavaScript module from within an ASP.NET Core application.
To our surprise, this not only worked, it was also easy! We created a very simple file called pugcompile.js.
var pug = require('pug'); |
Calling this JavaScript from C# is easy thanks to the Node Services package. Assuming model
is the view model we want to bind to the template and mytemplate.pug
is the name of the file containing the pug template:
var html = await _nodeServices.InvokeAsync<string>("pugcompile", "mytemplate.pug", model); |
Now that we had proven this was possible, it was time to integrate this with MVC by creating a new MVC View Engine.
Creating the Pugzor View Engine
We decided to call our view engine Pugzor which is a combination of Pug and Razor. Of course, this doesn’t really make much sense since our view engine really has nothing to do with Razor but naming is hard and we thought we were being funny.
Keeping in mind our goal of implementing a view engine in a single day, we wanted to do this with the simplest way possible. After spending some time digging through the source code for MVC, we determined that we needed to implement the IViewEngine
interface as well as implement a custom IView
.
The IViewEngine
is responsible for locating a view based on a ActionContext
and a ViewName
. When a controller returns a View
, it is the IViewEngine
‘s FindView
method that is responsible for finding a view based on some convetions. The FindView
method returns a ViewEngineResult
which is a simple class containing a boolean Success
property indicating whether or not a view was found and an IView View
property containing the view if it was found.
/// <summary> |
We decided to use the same view location conventions as Razor. That is, a view is located in Views/{ControllerName}/{ActionName}.pug
.
Here is a simplified version of the FindView method for the PugzorViewEngine
:
public ViewEngineResult FindView( |
You can view the complete implentation on GitHub.
Next, we created a class called PugzorView
which implements IView
. The PugzorView
takes in a path to a pug template and an instance of INodeServices
. The MVC framework calls the IView
‘s RenderAsync
when it is wants the view to be rendered. In this method, we call out to pugcompile
and then write the resulting HTML out to the view context.
public class PugzorView : IView |
The only thing left was to configure MVC to use our new view engine. At first, we thought we could easy add a new view engine using the AddViewOptions
extension method when adding MVC to the service collection.
services.AddMvc() |
This is where we got stuck. We can’t add a concrete instance of the PugzorViewEngine
to the ViewEngines
collection in the Startup.ConfigureServices
method because the view engine needs to take part in dependency injection. The PugzorViewEngine
has a dependency on INodeServices
and we want that to be injected by ASP.NET Core’s dependency injection framework. Luckily, the all knowning Razor master Taylor Mullen was on hand to show us the right way to register our view engine.
The recommended approach for adding a view engine to MVC is to create a custom setup class that implements IConfigureOptions<MvcViewOptions>
. The setup class takes in an instance of our IPugzorViewEngine
via constructor injection. In the configure method, that view engine is added to the list of view engines in the MvcViewOptions
.
public class PugzorMvcViewOptionsSetup : IConfigureOptions<MvcViewOptions> |
Now all we need to do is register the setup class and view engine the Startup.ConfigureServices
method.
services.AddTransient<IConfigureOptions<MvcViewOptions>, PugzorMvcViewOptionsSetup>(); services.AddSingleton<IPugzorViewEngine, PugzorViewEngine>(); |
Like magic, we now have a working view engine. Here’s a simple example:
Controllers/HomeController.cs
public IActionResult Index() |
Views/Home/Index.pug
block body |
Result
<h2>Hello</h2> |
All the features of pug work as expected, including templage inheritance and inline JavaScript code. Take a look at our test website for some examples.
Packaging it all up
So we reached our goal of creating an alternate view engine for MVC in a single day. We had some time left so we thought we would try to take this one step further and create a NuGet package. There were some challenges here, specifically related to including the required node modules in the NuGet package. Simon is planning to write a separate blog post on that topic.
You can give it a try yourself. Add a reference to the pugzor.core
NuGet package then call .AddPugzor()
after .AddMvc()
in the Startup.ConfigureServices
method.
public void ConfigureServices(IServiceCollection services) { // Add framework services. services.AddMvc().AddPugzor(); } |
Razor still works as the default but if no Razor view is found, the MVC framework will try using the PugzorViewEngine. If a matching pug template is found, that template will be rendered.
Wrapping it up
We had a blast working on this project. While this started out as a silly excercise, we sort of ended up with something that could be useful. We were really surprised at how easy it was to create a new view engine for MVC. We don’t expect that Pugzor will be wildly popular but since it works we thought we would put it out there and see what people think.
We have some open issues and some ideas for how to extend the PugzorViewEngine
. Let us know what you think or jump in and contribute some code. We accept pull requests :-)