Last active
January 29, 2021 14:58
-
-
Save elfalem/c9e54f4eef8e400510c7509dd4920021 to your computer and use it in GitHub Desktop.
Alternative view engine for ASP.NET Core Part IV
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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