Skip to content

Instantly share code, notes, and snippets.

@elfalem
Last active January 29, 2021 14:58
Show Gist options
  • Save elfalem/c9e54f4eef8e400510c7509dd4920021 to your computer and use it in GitHub Desktop.
Save elfalem/c9e54f4eef8e400510c7509dd4920021 to your computer and use it in GitHub Desktop.
Alternative view engine for ASP.NET Core Part IV
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 missingProperties = GetMissingProperties(compilation);
if(missingProperties.Any())
{
var secondClassDocument = CreateClassDocument(parsedResult, templateClassName, missingProperties);
var secondSyntaxTree = CSharpSyntaxTree.ParseText(secondClassDocument);
compilation = compilation.ReplaceSyntaxTree(syntaxTree, secondSyntaxTree);
}
var assembly = LoadAssembly(compilation);
return assembly;
}
private static string CreateClassDocument(DocumentNode parsedResult, string templateClassName, List<string> properties = null)
{
var result = new StringBuilder();
result.AppendLine($@"
using System.Text;
namespace StacheTemplateNamespace{{
public class {templateClassName}
{{
private StringBuilder _output = new StringBuilder();");
if(properties != null)
{
foreach(var property in properties){
result.AppendLine($"public dynamic {property} {{get; set;}}");
}
}
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 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));
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 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();
}
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.Linq;
using System.Reflection;
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 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]);
}
}
var processedOutput = instance.GetType().GetMethod("Execute").Invoke(instance, null);
return context.Writer.WriteAsync(processedOutput.ToString());
}else{
throw new Exception(parsedResult.ErrorMessage);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment