From 0699e7aa400a4919152f5a16c337da3843fd4ec7 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 16 Jan 2014 21:09:59 -0800 Subject: [PATCH] Adding support for disk based views with code generation and compilation --- .../Compilation/CompilationFailedException.cs | 38 ++++++ .../Compilation/CompilationMessage.cs | 19 +++ .../Compilation/CompilationResult.cs | 45 +++++++ .../Compilation/CompilerCache.cs | 44 +++++++ .../Compilation/CscBasedCompilationService.cs | 120 ++++++++++++++++++ .../Compilation/DefaultCompilationService.cs | 14 ++ .../Compilation/ICompilationService.cs | 11 ++ .../Microsoft.AspNet.CoreServices.csproj | 14 ++ Microsoft.AspNet.CoreServices/packages.config | 4 + .../IVirtualPathFactory.cs | 10 -- .../Microsoft.AspNet.Mvc.Razor.csproj | 23 +++- .../Razor/MvcCSharpRazorCodeGenerator.cs | 29 +++++ .../Razor/MvcCSharpRazorCodeParser.cs | 71 +++++++++++ .../Razor/MvcRazorHost.cs | 53 ++++++++ .../Razor/RazorCompilationService.cs | 71 +++++++++++ .../Razor/SetModelTypeCodeGenerator.cs | 27 ++++ Microsoft.AspNet.Mvc.Razor/RazorView.cs | 17 +-- Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs | 16 +++ .../ViewEngine/IVirtualPathViewFactory.cs | 10 ++ .../MetadataVirtualPathViewFactory.cs} | 10 +- .../{ => ViewEngine}/RazorViewEngine.cs | 6 +- .../ViewEngine/VirtualFileSystem.cs | 37 ++++++ .../{ => ViewEngine}/VirtualPathAttribute.cs | 0 .../ViewEngine/VirtualPathViewFactory.cs | 31 +++++ Microsoft.AspNet.Mvc.Razor/packages.config | 2 + Microsoft.AspNet.Mvc/ActionResultHelper.cs | 6 +- .../ActionResultHelperExtensions.cs | 21 +++ Microsoft.AspNet.Mvc/IActionResultHelper.cs | 2 +- .../Microsoft.AspNet.Mvc.csproj | 1 + Microsoft.AspNet.Mvc/View/ViewContext.cs | 4 +- Microsoft.AspNet.Mvc/View/ViewResult.cs | 1 - MvcSample/HomeController.cs | 2 +- MvcSample/MvcSample.csproj | 6 + MvcSample/Startup.cs | 12 +- MvcSample/Views/Home/MyView.cshtml | 2 + MvcSample/Views/Layout.cs | 2 +- MvcSample/Views/MyView.cs | 7 +- MvcSample/Views/Shared/_Layout.cshtml | 11 ++ MvcSample/packages.config | 1 + 39 files changed, 754 insertions(+), 46 deletions(-) create mode 100644 Microsoft.AspNet.CoreServices/Compilation/CompilationFailedException.cs create mode 100644 Microsoft.AspNet.CoreServices/Compilation/CompilationMessage.cs create mode 100644 Microsoft.AspNet.CoreServices/Compilation/CompilationResult.cs create mode 100644 Microsoft.AspNet.CoreServices/Compilation/CompilerCache.cs create mode 100644 Microsoft.AspNet.CoreServices/Compilation/CscBasedCompilationService.cs create mode 100644 Microsoft.AspNet.CoreServices/Compilation/DefaultCompilationService.cs create mode 100644 Microsoft.AspNet.CoreServices/Compilation/ICompilationService.cs create mode 100644 Microsoft.AspNet.CoreServices/packages.config delete mode 100644 Microsoft.AspNet.Mvc.Razor/IVirtualPathFactory.cs create mode 100644 Microsoft.AspNet.Mvc.Razor/Razor/MvcCSharpRazorCodeGenerator.cs create mode 100644 Microsoft.AspNet.Mvc.Razor/Razor/MvcCSharpRazorCodeParser.cs create mode 100644 Microsoft.AspNet.Mvc.Razor/Razor/MvcRazorHost.cs create mode 100644 Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs create mode 100644 Microsoft.AspNet.Mvc.Razor/Razor/SetModelTypeCodeGenerator.cs create mode 100644 Microsoft.AspNet.Mvc.Razor/RazorViewOfT.cs create mode 100644 Microsoft.AspNet.Mvc.Razor/ViewEngine/IVirtualPathViewFactory.cs rename Microsoft.AspNet.Mvc.Razor/{MetadataVirtualPathProvider.cs => ViewEngine/MetadataVirtualPathViewFactory.cs} (83%) rename Microsoft.AspNet.Mvc.Razor/{ => ViewEngine}/RazorViewEngine.cs (89%) create mode 100644 Microsoft.AspNet.Mvc.Razor/ViewEngine/VirtualFileSystem.cs rename Microsoft.AspNet.Mvc.Razor/{ => ViewEngine}/VirtualPathAttribute.cs (100%) create mode 100644 Microsoft.AspNet.Mvc.Razor/ViewEngine/VirtualPathViewFactory.cs create mode 100644 Microsoft.AspNet.Mvc/ActionResultHelperExtensions.cs create mode 100644 MvcSample/Views/Home/MyView.cshtml create mode 100644 MvcSample/Views/Shared/_Layout.cshtml diff --git a/Microsoft.AspNet.CoreServices/Compilation/CompilationFailedException.cs b/Microsoft.AspNet.CoreServices/Compilation/CompilationFailedException.cs new file mode 100644 index 0000000000..69a42bc6dd --- /dev/null +++ b/Microsoft.AspNet.CoreServices/Compilation/CompilationFailedException.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.CoreServices +{ + public class CompilationFailedException : Exception + { + public CompilationFailedException(IEnumerable messages, string generatedCode) + : base(FormatMessage(messages)) + { + Messages = messages.ToList(); + GeneratedCode = generatedCode; + } + + public string GeneratedCode { get; private set; } + + public IEnumerable Messages { get; private set; } + + public string CompilationSource + { + get { return GeneratedCode; } + } + + public override string Message + { + get + { + return "Compilation Failed:" + FormatMessage(Messages); + } + } + + private static string FormatMessage(IEnumerable messages) + { + return String.Join(Environment.NewLine, messages); + } + } +} diff --git a/Microsoft.AspNet.CoreServices/Compilation/CompilationMessage.cs b/Microsoft.AspNet.CoreServices/Compilation/CompilationMessage.cs new file mode 100644 index 0000000000..173841b449 --- /dev/null +++ b/Microsoft.AspNet.CoreServices/Compilation/CompilationMessage.cs @@ -0,0 +1,19 @@ +using System; + +namespace Microsoft.AspNet.CoreServices +{ + public class CompilationMessage + { + public CompilationMessage(string message) + { + Message = message; + } + + public string Message {get; private set;} + + public override string ToString() + { + return Message; + } + } +} diff --git a/Microsoft.AspNet.CoreServices/Compilation/CompilationResult.cs b/Microsoft.AspNet.CoreServices/Compilation/CompilationResult.cs new file mode 100644 index 0000000000..0e4ce696df --- /dev/null +++ b/Microsoft.AspNet.CoreServices/Compilation/CompilationResult.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.CoreServices +{ + public class CompilationResult + { + private readonly Type _type; + + private CompilationResult(string generatedCode, Type type, IEnumerable messages) + { + _type = type; + GeneratedCode = generatedCode; + Messages = messages.ToList(); + } + + public IEnumerable Messages { get; private set; } + + public string GeneratedCode { get; private set; } + + public Type CompiledType + { + get + { + if (_type == null) + { + throw new CompilationFailedException(Messages, GeneratedCode); + } + + return _type; + } + } + + public static CompilationResult Failed(string generatedCode, IEnumerable messages) + { + return new CompilationResult(generatedCode, type: null, messages: messages); + } + + public static CompilationResult Successful(string generatedCode, Type type) + { + return new CompilationResult(generatedCode, type, Enumerable.Empty()); + } + } +} diff --git a/Microsoft.AspNet.CoreServices/Compilation/CompilerCache.cs b/Microsoft.AspNet.CoreServices/Compilation/CompilerCache.cs new file mode 100644 index 0000000000..ccc701d34e --- /dev/null +++ b/Microsoft.AspNet.CoreServices/Compilation/CompilerCache.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.Caching; +using System.Threading.Tasks; +using Microsoft.Owin.FileSystems; + +namespace Microsoft.AspNet.CoreServices +{ + public class CompilerCache + { + private readonly MemoryCache _cache; + + public CompilerCache() + { + _cache = MemoryCache.Default; + } + + public Task GetOrAdd(IFileInfo file, Func> compile) + { + // Generate a content id + string contentId = file.PhysicalPath + '|' + file.LastModified.Ticks; + + var cachedType = _cache[contentId] as Type; + if (cachedType == null) + { + return CompileWith(contentId, file, compile); + } + + return Task.FromResult(CompilationResult.Successful(generatedCode: null, type: cachedType)); + } + + private async Task CompileWith(string contentId, IFileInfo file, Func> compile) + { + CompilationResult result = await compile(); + Type compiledType = result.CompiledType; + + var filePaths = new [] { file.PhysicalPath }; + var policy = new CacheItemPolicy(); + policy.ChangeMonitors.Add(new HostFileChangeMonitor(filePaths)); + + _cache.Set(contentId, result.CompiledType, policy); + return result; + } + } +} diff --git a/Microsoft.AspNet.CoreServices/Compilation/CscBasedCompilationService.cs b/Microsoft.AspNet.CoreServices/Compilation/CscBasedCompilationService.cs new file mode 100644 index 0000000000..52d5060da9 --- /dev/null +++ b/Microsoft.AspNet.CoreServices/Compilation/CscBasedCompilationService.cs @@ -0,0 +1,120 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Owin.FileSystems; + +namespace Microsoft.AspNet.CoreServices +{ + public class CscBasedCompilationService : ICompilationService + { + private static readonly string _tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + private readonly string _path; + + public CscBasedCompilationService() + { + _path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), + @"Microsoft.NET\Framework\v4.0.30319\csc.exe"); + } + + public async Task Compile(IFileInfo fileInfo) + { + Directory.CreateDirectory(_tempDir); + string outFile = Path.Combine(_tempDir, Path.GetRandomFileName() + ".dll"); + StringBuilder args = new StringBuilder("/target:library "); + args.AppendFormat("/out:\"{0}\" ", outFile); + foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "*.dll")) + { + args.AppendFormat("/R:\"{0}\" ", file); + } + args.AppendFormat("\"{0}\"", fileInfo.PhysicalPath); + var outputStream = new MemoryStream(); + var errorStream = new MemoryStream(); + + // common execute + var process = CreateProcess(args.ToString()); + int exitCode = await Start(process, outputStream, errorStream); + + string output = GetString(outputStream); + string error = GetString(errorStream); + if (exitCode != 0) + { + return CompilationResult.Failed(String.Empty, error.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) + .Select(e => new CompilationMessage(e))); + } + + var type = Assembly.LoadFrom(outFile) + .GetExportedTypes() + .First(); + return CompilationResult.Successful(String.Empty, type); + } + + + private string GetString(MemoryStream stream) + { + if (stream.Length > 0) + { + return Encoding.UTF8.GetString(stream.GetBuffer(), 0, (int)stream.Length); + } + + return String.Empty; + } + + private static async Task Start(Process process, Stream output, Stream error) + { + var tcs = new TaskCompletionSource(); + process.EnableRaisingEvents = true; + process.Exited += (sender, eventArgs) => + { + tcs.SetResult(process.ExitCode); + }; + + process.Start(); + + var tasks = new[] + { + process.StandardOutput.BaseStream.CopyToAsync(output), + process.StandardError.BaseStream.CopyToAsync(error) + }; + + int result = await tcs.Task; + + // Process has exited, draining the stdout and stderr + await Task.WhenAll(tasks); + + return result; + } + + internal Process CreateProcess(string arguments) + { + var psi = new ProcessStartInfo + { + FileName = _path, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = false, + ErrorDialog = false, + Arguments = arguments + }; + + psi.StandardOutputEncoding = Encoding.UTF8; + psi.StandardErrorEncoding = Encoding.UTF8; + + var process = new Process() + { + StartInfo = psi + }; + + return process; + } + + + } +} + diff --git a/Microsoft.AspNet.CoreServices/Compilation/DefaultCompilationService.cs b/Microsoft.AspNet.CoreServices/Compilation/DefaultCompilationService.cs new file mode 100644 index 0000000000..87bb79f76e --- /dev/null +++ b/Microsoft.AspNet.CoreServices/Compilation/DefaultCompilationService.cs @@ -0,0 +1,14 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Owin.FileSystems; + +namespace Microsoft.AspNet.CoreServices +{ + public class DefaultCompilationService : ICompilationService + { + public Task Compile(IFileInfo fileInfo) + { + return null; + } + } +} diff --git a/Microsoft.AspNet.CoreServices/Compilation/ICompilationService.cs b/Microsoft.AspNet.CoreServices/Compilation/ICompilationService.cs new file mode 100644 index 0000000000..63bed694df --- /dev/null +++ b/Microsoft.AspNet.CoreServices/Compilation/ICompilationService.cs @@ -0,0 +1,11 @@ + +using System.Threading.Tasks; +using Microsoft.Owin.FileSystems; + +namespace Microsoft.AspNet.CoreServices +{ + public interface ICompilationService + { + Task Compile(IFileInfo fileInfo); + } +} diff --git a/Microsoft.AspNet.CoreServices/Microsoft.AspNet.CoreServices.csproj b/Microsoft.AspNet.CoreServices/Microsoft.AspNet.CoreServices.csproj index 1d50d51d5e..fef8e420fd 100644 --- a/Microsoft.AspNet.CoreServices/Microsoft.AspNet.CoreServices.csproj +++ b/Microsoft.AspNet.CoreServices/Microsoft.AspNet.CoreServices.csproj @@ -30,11 +30,22 @@ 4 + + ..\packages\Microsoft.Owin.FileSystems.2.1.0-rc1\lib\net40\Microsoft.Owin.FileSystems.dll + + + + + + + + + @@ -42,6 +53,9 @@ + + +