Alternative view engine for ASP.NET Core: Passing data from controller
Tags: csharp, dotnet
Published: 2021-01-29 17:21:40

[Part III](https://dev.to/dealeron/alternative-view-engine-for-asp-net-core-compiling-templates-2m2f) in this series looked at how view templates are dynamically compiled including the processing of simple expressions (like `1 + 1`) embedded in the templates. However, view templates are useful when we can pass values to them to be shown in the variable areas of the template. In this final post of the series, we'll demonstrate how values added to the `ViewData` in the controller can be passed to the view template and rendered in the final output.

In the Razor view engine, accessing a value from ViewData looks like this:
```
Hello @@ViewData["name"]
```

For our mustache syntax, we want a more readable syntax like this:
```
Hello {{name}}
```

For the above to work `name` must exist as some sort of variable in the compiled template and have an assigned value.

You can find the full contents of the source files discussed below on [this GitHub gist](https://gist.github.com/elfalem/c9e54f4eef8e400510c7509dd4920021).

## Identify Replaceable Values
The first step is knowing all the replaceable values in the view template. Since we allow expressions in our mustache syntax, we need a way to parse C# expressions and identify the variables. As we are already compiling the view template, we can use diagnostic messages from the compilation process to indirectly obtain the list of replaceable values.

In `StacheTemplateCompiler.cs`, add the following method:
```csharp
private static List<string> GetMissingProperties(CSharpCompilation compilation)
{
  var missingProperties = compilation.GetDiagnostics().Where(d => d.Id.Equals("CS0103"))
			.Select(d => d.GetMessage()).Distinct()
			.Select(m => m.Replace("The name '", string.Empty)
			.Replace("' does not exist in the current context", string.Empty));

  return missingProperties.ToList();
}
```

We're using the Roslyn compiler for the heavy lifting. Any names it reports as not existing in the current context are replaceable values in the view template.

## Adjust Compiled Template
Once we know this list, we can create the class document a second time and declare the replaceable values as properties. In `StacheTemplateCompiler.cs`, insert the following into the `Compile()` method right before loading the assembly:
```csharp
var missingProperties = GetMissingProperties(compilation);
if(missingProperties.Any())
{
	var secondClassDocument = CreateClassDocument(parsedResult, templateClassName, missingProperties);
	var secondSyntaxTree = CSharpSyntaxTree.ParseText(secondClassDocument);
	compilation = compilation.ReplaceSyntaxTree(syntaxTree, secondSyntaxTree);
}
```

Additionally modify `CreateClassDocument()` to accept a list of strings as the last paramater named `properties` (optional, with default value of null). Insert the following code right after the creation of the `StringBuilder` object.

```csharp
if(properties != null)
{
	foreach(var property in properties){
          result.AppendLine($"public dynamic {property} {{get; set;}}");
	}
}
```

We declare each replaceable value as a dynamic property. Although we have been using strings in the examples, values sent through `ViewData` from the controller could have other types. Therefore treating it as a dynamic property gives us the flexibility to accept all types.

To use the `dynamic` keyword, we need to include references to additional assemblies when creating the compilation. Update the beginning of the `CreateCompilation()` method as follows:

```csharp
var assemblyLocations = new List<string>{
	// system runtime assembly
	typeof(object).Assembly.Location,
	// the following are needed for dynamic keyword support
	Assembly.Load(new AssemblyName("System.Linq.Expressions")).Location,
	Assembly.Load(new AssemblyName("Microsoft.CSharp")).Location,
	Assembly.Load(new AssemblyName("System.Runtime")).Location,
	Assembly.Load(new AssemblyName("netstandard")).Location
};

var references = assemblyLocations.Select(location => MetadataReference.CreateFromFile(location));
```

## Pass Values to View Template
At this point, we have code to successfully complile a view template while including C# properties for each replaceable value in the template. The last step is to set the properties with the the actual values provided from `ViewData` when rendering the view template.

In `StacheView.cs`, update the `RenderAsync()` method to set the properties using Reflection before invoking the `Execute()` method on the compiled instance:
```csharp
var properties = instance.GetType().GetRuntimeProperties()
  .Where((property) =>
	{
	  return property.GetIndexParameters().Length == 0 &&
		  property.SetMethod != null &&
		  !property.SetMethod.IsStatic;
});

foreach(var prop in properties){
	if(context.ViewData.ContainsKey(prop.Name)){
		prop.SetValue(instance, context.ViewData[prop.Name]);
	}
}
```

To test the changes, we will modify the `Bar` action in `HomeController.cs` we have been using to the following:
```csharp
public IActionResult Bar(){
    ViewData["name"] = "Jonathan";

    return View();
}
```

And modify the view template `Bar.stache` as follows:
```csharp
Hello {{name}}, your name has {{name.Length}} characters.
```

If you now run the application (`dotnet run`) and navigate to `https://localhost:5001/home/bar`, you should see:
```
Hello Jonathan, your name has 8 characters.
```

We have successfuly passed a value from the controller to the view template.

## Summary
In this series, we created an alternative view engine with its own template syntax. We parsed the template, created a compilation, and used it to render values supplied by the controller. Hopefully this illustrated the modular nature of ASP.NET and its ability to accept third party view engines.

If you're interested in other alternatives, [this Stack Overflow answer](https://stackoverflow.com/questions/1451319/asp-net-mvc-view-engine-comparison) compares various view engines. Also check out [NVue](https://github.com/elfalem/NVue), my experimental attempt at a view engine based on the Vue.js template syntax.