Skip to content

Instantly share code, notes, and snippets.

@elfalem
Created May 1, 2020 15:53
Show Gist options
  • Save elfalem/2d2c173a9d83d912046766f19bedcc01 to your computer and use it in GitHub Desktop.
Save elfalem/2d2c173a9d83d912046766f19bedcc01 to your computer and use it in GitHub Desktop.
Alternative view engine for ASP.NET Core Part III
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace foo.Stache
{
public static class StacheTemplateCompiler
{
internal static Assembly Compile(DocumentNode parsedResult, string templateClassName){
var classDocument = CreateClassDocument(parsedResult, templateClassName);
var syntaxTree = CSharpSyntaxTree.ParseText(classDocument);
var compilation = CreateCompilation(syntaxTree);
var assembly = LoadAssembly(compilation);
return assembly;
}
private static string CreateClassDocument(DocumentNode parsedResult, string templateClassName)
{
var result = new StringBuilder();
result.AppendLine($@"
using System.Text;
namespace StacheTemplateNamespace{{
public class {templateClassName}
{{
private StringBuilder _output = new StringBuilder();");
result.AppendLine(@"
public string Execute()
{
");
foreach(var node in parsedResult.Nodes){
switch(node){
case TextNode textNode:
result.Append("_output.Append(\"");
result.Append(textNode.Value);
result.AppendLine("\");");
break;
case ExpressionNode expNode:
result.Append("_output.Append(");
result.Append(expNode.Value);
result.AppendLine(");");
break;
}
}
result.AppendLine(@"
return _output.ToString();
}
}
}
");
return result.ToString();
}
private static CSharpCompilation CreateCompilation(SyntaxTree syntaxTree){
var systemRuntimeAssemblyLocation = typeof(object).Assembly.Location;
var references = new List<MetadataReference>{
MetadataReference.CreateFromFile(systemRuntimeAssemblyLocation)
};
var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
var assemblyName = System.IO.Path.GetRandomFileName();
var compilation = CSharpCompilation.Create(assemblyName, options: compilationOptions, references: references);
compilation = compilation.AddSyntaxTrees(syntaxTree);
return compilation;
}
private static Assembly LoadAssembly(CSharpCompilation compilation){
using (var assemblyStream = new MemoryStream())
{
var result = compilation.Emit(assemblyStream, null);
if (!result.Success)
{
throw new Exception(string.Join(" ", result.Diagnostics.Select(d => $"{d.Id}-{d.GetMessage()}\n")));
}
assemblyStream.Seek(0, SeekOrigin.Begin);
return Assembly.Load(assemblyStream.ToArray(), null);
}
}
}
}
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Superpower;
namespace foo.Stache
{
public class StacheView : IView
{
public StacheView(string path, string templateName){
Path = path;
TemplateName = templateName;
}
public string Path {get; private set;}
public string TemplateName {get; private set;}
public Task RenderAsync(ViewContext context)
{
var template = File.ReadAllText(Path);
var tokens = StacheParser.Tokenizer.Tokenize(template);
var parsedResult = StacheParser.MainParser.TryParse(tokens);
if(parsedResult.HasValue){
var document = (DocumentNode)parsedResult.Value;
var assembly = StacheTemplateCompiler.Compile(document, TemplateName);
var instance = assembly.CreateInstance($"StacheTemplateNamespace.{TemplateName}");
var processedOutput = instance.GetType().GetMethod("Execute").Invoke(instance, null);
return context.Writer.WriteAsync(processedOutput.ToString());
}else{
throw new Exception(parsedResult.ErrorMessage);
}
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;
namespace foo.Stache
{
public class StacheViewEngine : IViewEngine
{
private const string ViewExtension = ".stache";
private string[] _viewLocationFormats = {
"Views/{1}/{0}" + ViewExtension,
"Views/Shared/{0}" + ViewExtension
};
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, viewName));
}
checkedLocations.Add(possibleViewLocation);
}
return ViewEngineResult.NotFound(viewName, checkedLocations);
}
throw new Exception("Controller route value not found.");
}
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)){
var viewNameWithExtension = viewPath.Substring(viewPath.LastIndexOf('/')+1);
var viewName = viewNameWithExtension.Substring(0, viewNameWithExtension.IndexOf(ViewExtension));
return ViewEngineResult.Found(viewPath, new StacheView(appRelativePath, viewName));
}
return ViewEngineResult.NotFound(viewPath, new List<string>{ appRelativePath});
}
private static string GetAbsolutePath(string executingFilePath, string viewPath){
if (IsAbsolutePath(viewPath))
{
// An absolute path already; no change required.
return viewPath.Replace("~/", string.Empty);
}
if(string.IsNullOrEmpty(executingFilePath)){
return $"/{viewPath}";
}
var index = executingFilePath.LastIndexOf('/');
return executingFilePath.Substring(0, index + 1) + viewPath;
}
private static bool IsAbsolutePath(string name) => name.StartsWith("~/") || name.StartsWith("/");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment