Created
May 1, 2020 15:53
-
-
Save elfalem/2d2c173a9d83d912046766f19bedcc01 to your computer and use it in GitHub Desktop.
Alternative view engine for ASP.NET Core Part III
This file contains 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 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); | |
} | |
} | |
} | |
} |
This file contains 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.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); | |
} | |
} | |
} | |
} |
This file contains 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 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