Alternative view engine for ASP.NET Core: Hooking into the ASP.NET Core architecture
Tags: csharp, dotnet
Published: 2019-08-09 16:40:15

ASP.NET Core is a great open-source framework for building web apps. One of its advantages is its modular architecture, which makes it possible to replace many of the default components with alternative ones. In this post, we'll look at how we can create and use a new view engine with ASP.NET.

## Creating a Razor View

Let's first create an ASP.NET Core MVC app with a Razor view. You'll need the [.NET Core SDK](https://dotnet.microsoft.com/download). Using the terminal on your OS or in your favorite IDE, such as [VS Code](https://code.visualstudio.com/) (a great, cross-platform IDE), create a project called Foo and initialize it with the default template for an MVC app.

```
mkdir foo
cd foo/
dotnet new mvc
```
You can now run the app with `dotnet run` and navigate to `https://localhost:5001/` to access the home page displaying a welcome message.

Now we'll add an action named Bar to the Home controller as well a corresponding Razor view.

Edit `HomeController.cs` to add the following:
```csharp
public IActionResult Bar(){
	ViewData["Message"] = "Hello World!";

	return View();
}
```

Create `Views/Home/Bar.cshtml` and add the following:
```
Your Message: @ViewData["Message"]
```

If you stop and run the app again, you should see the text `Your Message: Hello World!` when accessing `https://localhost:5001/Home/Bar`

## An alternative view engine

As we saw above, the Razor view engine that comes with ASP.NET uses markup files with the extension `.cshtml`. The Razor syntax uses the `@` symbol to transition to C# and evaluate expressions. As an alternative, what if we use the mustache syntax for markup files? We can use braces to denote variables that will need to be treated as C# expressions. In our case, the above Bar view will look like this in the new template system:

```
Your Message: {{Message}}
```

We'll use a similar convention as Razor for locating views - i.e. `Views/[controller]/[action]` and utilize the file extension `.stache`. Let's see how we can implement the Stache View Engine.

## Creating a view engine

A view engine needs to implement two interfaces: `IViewEngine` and `IView` from the namespace `Microsoft.AspNetCore.Mvc.ViewEngines`. In our project, create a top level directory named `Stache` containing the files `StacheViewEngine.cs` and `StacheView.cs`. We'll walk through the key parts of these two interfaces below. You can find the full contents of the two files on [this GitHub gist](https://gist.github.com/elfalem/10ee2705fc322baf311accb471432373).

### IViewEngine

[IViewEngine](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewengines.iviewengine?view=aspnetcore-2.2) requires implementing two methods:
```csharp
public ViewEngineResult GetView (string executingFilePath, string viewPath, bool isMainPage);

public ViewEngineResult FindView (ActionContext context, string viewName, bool isMainPage);
```

Both methods return a `ViewEngineResult` which encapsulates `ViewEngineResult.Found`, indicating an instance of the view was found, and `ViewEngineResult.NotFound`, indicating that the view was not found and containing the list of locations searched.

It's helpful to discuss how a view can be referenced when it's returned from a controller to understand the roles these two methods play in the process of rendering a view. In our Bar controller, we just write `return View();` which references the view with the same name as the action (i.e. `Bar`). We could have also directly named the view: `return View("Bar");`. Alternatively it's possible to explicitly provide the full path to a view file: `return View("~/Views/Home/Bar.stache");`. In all cases, the framework first invokes the `GetView()` method. If a view is not found, it will then invoke `FindView()`.

In cases where the view path is explicitly given, we expect `GetView()` to return an instance of that view. In cases where we only have the view name, we fallback to `FindView()` to search various paths that we've defined by convention to be possible locations where a view could exist.

In our implementation of `GetView()`, we first check if `viewPath` is an actual file path or just the name of the action. We'll get the latter if the view is referenced by name only. In that case, we'll return `ViewEngineResult.NotFound` so the call can proceed to `FindView()`.

If it's a file path, we'll pass it to a private method `GetAbsolutePath()` to ensure it's well formatted. `executingFilePath` will only have a value if there is another view that's currently executing (e.g. the parent view when processing a Razor partial view). If it has a value, it will get combined with the view path to generate a path relative to the parent executing view. Once we have the path, we check if a template exists at that path and return appropriately.

```csharp
public ViewEngineResult GetView(string executingFilePath, string viewPath, bool isMainPage)
{
  if(string.IsNullOrEmpty(viewPath) || !viewPath.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase)){
    return ViewEngineResult.NotFound(viewPath, Enumerable.Empty<string>());  
  }

  var appRelativePath = GetAbsolutePath(executingFilePath, viewPath);

  if(File.Exists(appRelativePath)){
    return ViewEngineResult.Found(viewPath, new StacheView(appRelativePath));
  }

  return ViewEngineResult.NotFound(viewPath, new List<string>{ appRelativePath});
}
```

For our implementation of `FindView()`, we're provided the view name as well as the action context from which we can get the controller name. Based on our convention for locating views, we'll plug in those two values into each of the `_viewLocationFormats` (e.g. `Views/[controller]/[action].stache`) and check for a template file until one is found. If none are found, we return `ViewEngineResult.NotFound` with all the paths we checked.

```csharp
public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage)
{
  if(context.ActionDescriptor.RouteValues.TryGetValue("controller", out var controllerName)){
    var checkedLocations = new List<string>();

    foreach(var locationFormat in _viewLocationFormats){
      var possibleViewLocation = string.Format(locationFormat, viewName, controllerName);

      if(File.Exists(possibleViewLocation)){
        return ViewEngineResult.Found(viewName, new StacheView(possibleViewLocation));
      }
      checkedLocations.Add(possibleViewLocation);
    }

    return ViewEngineResult.NotFound(viewName, checkedLocations);
  }
  throw new Exception("Controller route value not found.");
}
```

### IView

[IView](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewengines.iview?view=aspnetcore-2.2) requires implementing the `Path` property and the `RenderAsync()` method. `Path` is the location of the view template. We populate the `Path` property when instantiating the view in `GetView()` or `FindView()`.

The `RenderAsync` method is responsible for rendering the view given a `ViewContext` and is where the magic happens. In this grossly oversimplified example, we just look for the string `{{Message}}` in the template and replace it with the value provided in the `ViewData`. In a more fleshed out version, this is where your custom syntax parser would do it's work.

```csharp
public Task RenderAsync(ViewContext context)
{
  var template = File.ReadAllText(Path);

  var processedOutput = template.Replace("{{Message}}", context.ViewData["Message"]?.ToString());
  
  return context.Writer.WriteAsync(processedOutput);
}
```

## Using the alternative view engine

Let's create an alternative Bar view using the new Stache template syntax. Create the file `Views/Home/Bar.stache` with the contents:

```
Your Message: {{Message}}
```

The last step is to tell MVC to use the new view engine. In `Startup.cs` modify the MVC configuration to be:

```csharp
services.AddMvc()
	.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
	.AddViewOptions(options => {
		options.ViewEngines.Insert(0, new StacheViewEngine());
	});
```

Note that we're adding the Stache view engine as the first item in the list. This will ensure that the Stache template is used if both `.cshtml` and `.stache` markup files are found for a given view. If you'd like to default to Razor views instead, you can add Stache to the end of the list: `options.ViewEngines.Add(new StacheViewEngine());`.

If you run the app and navigate to the Bar action `https://localhost:5001/Home/Bar`, you should now see the view rendered with Stache!

## Summary

In the above example, we saw that it's relatively simple to plug in an alternative view engine in place of Razor. It illustrates the possibility of using a variety of markup templates, even simultaneously, with ASP.NET.

Although we have the foundation on which can build upon, this rudimentary example is quite limited:

* There is only one variable named `Message` that we can bind to. We should be able to use as many variables as needed for a given template.
* We need support for more advanced expressions instead of just variables (e.g. inline expressions such as `{{1 + 1}}`), loops to iterate through lists, and if-else conditions for control flow.
* We're missing some of the more advanced concepts of Razor such as layouts, partials and tag helpers.

It will be interesting to explore some of these missing features and develop our alternative view engine to implement them. Perhaps we'll do that in future posts.

*Thanks to [@sohjsolwin](https://dev.to/sohjsolwin) for providing feedback on this post. 
Also a special shout-out to the blog entry ["Creating a New View Engine in ASP.NET Core"](https://www.davepaquette.com/archive/2016/11/22/creating-a-new-view-engine-in-asp-net-core.aspx) by Dave Paquette for putting together some of the key puzzle pieces. 
Cover photo by [Eryk](https://unsplash.com/@eryk10) via [Unsplash](https://unsplash.com/)*