Made roslyn based compilation of razor files work

- Now that services are being flowed through the entire stack
we can nuke the csc service and always use the roslyn based compilation for views
- The RoslynCompilationService is still very much a prototype but it's a good
 step to see what other services might may or may not need to flow.
- Removed the PhysicalFileSystem construction from Mvc itself. This will come from
 the hosting layer and is newed up temporarily in the PathBasedViewFactory itself.
This commit is contained in:
David Fowler 2014-03-08 15:16:44 -08:00
parent 5de1ae578d
commit b482a9e3cb
19 changed files with 158 additions and 200 deletions

View File

@ -10,17 +10,12 @@ namespace MvcSample.Web
{
public class Startup
{
private readonly IApplicationEnvironment _env;
public Startup(IApplicationEnvironment env)
{
_env = env;
}
public void Configuration(IBuilder app)
{
var configuration = new Configuration();
var serviceProvider = new ServiceProvider().Add(MvcServices.GetDefaultServices(configuration, _env));
var services = MvcServices.GetDefaultServices(configuration);
var serviceProvider =
DefaultServiceProvider.Create(app.ServiceProvider, services);
var routes = new RouteCollection()
{

View File

@ -12,6 +12,12 @@
},
"configurations": {
"net45": { },
"k10" : { }
"k10" : {
"dependencies": {
"System.Collections" : "4.0.0.0",
"System.Runtime" : "4.0.20.0",
"System.ComponentModel": "4.0.0.0"
}
}
}
}

View File

@ -1,7 +1,7 @@
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="K" value="true" />
<add key="K" value="false" />
</appSettings>
<system.web>
<compilation debug="true" targetFramework="4.5" />

View File

@ -13,19 +13,16 @@ namespace MvcSample
{
const string baseUrl = "http://localhost:9001/";
private readonly IServiceProvider _serviceProvider;
private readonly IApplicationEnvironment _env;
public Program(IServiceProvider serviceProvider,
IApplicationEnvironment env)
public Program(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_env = env;
}
public void Main()
{
#if NET45
using (WebApp.Start(baseUrl, app => new Startup(_serviceProvider, _env).Configuration(app)))
using (WebApp.Start(baseUrl, app => new Startup(_serviceProvider).Configuration(app)))
{
Console.WriteLine("Listening at {0}", baseUrl);
Process.Start(baseUrl);

View File

@ -12,14 +12,11 @@ namespace MvcSample
{
public class Startup
{
private readonly IServiceProvider _serviceProvider;
private readonly IApplicationEnvironment _env;
private IServiceProvider _serviceProvider;
public Startup(IServiceProvider serviceProvider,
IApplicationEnvironment env)
public Startup(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_env = env;
}
public void Configuration(IAppBuilder app)
@ -33,11 +30,11 @@ namespace MvcSample
private void ConfigureMvc(IBuilder builder)
{
var configuration = new Configuration();
var services = MvcServices.GetDefaultServices(configuration, _env);
var serviceProvider = new ServiceProvider().Add(services);
var services = MvcServices.GetDefaultServices(configuration);
var serviceProvider = new ServiceProvider(_serviceProvider).Add(services);
serviceProvider.AddInstance<PassThroughAttribute>(new PassThroughAttribute());
var routes = new RouteCollection()
{
DefaultHandler = new MvcApplication(serviceProvider),

View File

@ -1,126 +0,0 @@
#if NET45
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.Razor
{
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<CompilationResult> Compile(string contents)
{
Directory.CreateDirectory(_tempDir);
string inFile = Path.Combine(_tempDir, Path.GetRandomFileName() + ".cs");
string outFile = Path.Combine(_tempDir, Path.GetRandomFileName() + ".dll");
StringBuilder args = new StringBuilder("/target:library ");
args.AppendFormat("/out:\"{0}\" ", outFile);
string binDir = Path.Combine(Directory.GetCurrentDirectory(), "bin");
// In the k-world, CurrentDir happens to be the bin dir
binDir = Directory.Exists(binDir) ? binDir : Directory.GetCurrentDirectory();
foreach (var file in Directory.EnumerateFiles(binDir, "*.dll"))
{
args.AppendFormat("/R:\"{0}\" ", file);
}
args.AppendFormat("\"{0}\"", inFile);
var outputStream = new MemoryStream();
// common execute
Process process = CreateProcess(args.ToString());
int exitCode;
try
{
File.WriteAllText(inFile, contents);
exitCode = await Start(process, outputStream);
}
finally
{
File.Delete(inFile);
}
string output = GetString(outputStream);
if (exitCode != 0)
{
IEnumerable<CompilationMessage> messages = output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)
.Skip(3)
.Select(e => new CompilationMessage(e));
return CompilationResult.Failed(String.Empty, messages);
}
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<int> Start(Process process, Stream output)
{
var tcs = new TaskCompletionSource<int>();
process.EnableRaisingEvents = true;
process.Exited += (sender, eventArgs) =>
{
tcs.SetResult(process.ExitCode);
};
process.Start();
var copyTask = process.StandardOutput.BaseStream.CopyToAsync(output);
await Task.WhenAll(tcs.Task, copyTask);
return process.ExitCode;
}
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;
}
}
}
#endif

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
@ -6,23 +7,22 @@ using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Net.Runtime;
using Microsoft.Net.Runtime.Services;
namespace Microsoft.AspNet.Mvc.Razor.Compilation
{
public class RoslynCompilationService : ICompilationService
{
private readonly IMetadataReferenceProvider _provider;
private readonly IDependencyExporter _exporter;
private readonly IApplicationEnvironment _environment;
private readonly IAssemblyLoaderEngine _loader;
public RoslynCompilationService(IServiceProvider serviceProvider)
public RoslynCompilationService(IApplicationEnvironment environment,
IAssemblyLoaderEngine loaderEngine,
IDependencyExporter exporter)
{
// TODO: Get these services via ctor injection when we get container chaining implemented
_provider = (IMetadataReferenceProvider)serviceProvider.GetService(typeof(IMetadataReferenceProvider));
_environment = (IApplicationEnvironment)serviceProvider.GetService(typeof(IApplicationEnvironment));
_loader = (IAssemblyLoaderEngine)serviceProvider.GetService(typeof(IAssemblyLoaderEngine));
_environment = environment;
_loader = loaderEngine;
_exporter = exporter;
}
public Task<CompilationResult> Compile(string content)
@ -30,15 +30,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var syntaxTrees = new[] { CSharpSyntaxTree.ParseText(content) };
var targetFramework = _environment.TargetFramework;
// Get references from the application itself and from
// this assembly for the base types etc
var referenceNames = new[] {
_environment.ApplicationName,
typeof(RoslynCompilationService).GetTypeInfo().Assembly.GetName().Name
};
var references = referenceNames.SelectMany(name => _provider.GetReferences(name, targetFramework))
.Cast<MetadataReference>();
var references = GetApplicationReferences().ToList();
var assemblyName = Path.GetRandomFileName();
@ -47,24 +39,75 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
syntaxTrees: syntaxTrees,
references: references);
var ms = new MemoryStream();
var result = compilation.Emit(ms);
if (!result.Success)
using (var ms = new MemoryStream())
{
var formatter = new DiagnosticFormatter();
using (var pdb = new MemoryStream())
{
var result = compilation.Emit(ms, pdbStream: pdb);
var messages = result.Diagnostics.Where(IsError).Select(d => GetCompilationMessage(formatter, d));
if (!result.Success)
{
var formatter = new DiagnosticFormatter();
return Task.FromResult(CompilationResult.Failed(content, messages));
var messages = result.Diagnostics.Where(IsError).Select(d => GetCompilationMessage(formatter, d));
return Task.FromResult(CompilationResult.Failed(content, messages));
}
var type = _loader.LoadBytes(ms.ToArray(), pdb.ToArray())
.GetExportedTypes()
.First();
return Task.FromResult(CompilationResult.Successful(String.Empty, type));
}
}
}
private IEnumerable<MetadataReference> GetApplicationReferences()
{
var assemblyNames = new[] {
_environment.ApplicationName,
typeof(RoslynCompilationService).GetTypeInfo().Assembly.GetName().Name
};
return assemblyNames.Select(a => _exporter.GetDependencyExport(a, _environment.TargetFramework))
.SelectMany(e => e.MetadataReferences.SelectMany(ConvertMetadataReference));
}
private IEnumerable<MetadataReference> ConvertMetadataReference(IMetadataReference metadataReference)
{
var fileMetadataReference = metadataReference as IMetadataFileReference;
if (fileMetadataReference != null)
{
string path = fileMetadataReference.Path;
#if NET45
return new[] { new MetadataFileReference(path) };
#else
// TODO: What about access to the file system? We need to be able to
// read files from anywhere on disk, not just under the web root
using (var stream = File.OpenRead(path))
{
return new[] { new MetadataImageReference(stream) };
}
#endif
}
// TODO: Flow loader to this code so we're not using Load() directly
var type = _loader.LoadBytes(ms.ToArray(), null)
.GetExportedTypes()
.First();
var roslynReference = metadataReference as IRoslynMetadataReference;
return Task.FromResult(CompilationResult.Successful(String.Empty, type));
if (roslynReference != null)
{
// REVIEW: We should really only compile against the app's closure
var compilatonReference = roslynReference.MetadataReference as CompilationReference;
if (compilatonReference != null)
{
return new[] { compilatonReference }.Concat(compilatonReference.Compilation.References);
}
return new[] { roslynReference.MetadataReference };
}
throw new NotSupportedException();
}
private CompilationMessage GetCompilationMessage(DiagnosticFormatter formatter, Diagnostic diagnostic)

View File

@ -1,5 +1,4 @@
using System.Runtime.Versioning;
using Microsoft.Net.Runtime.Services;
namespace Microsoft.Net.Runtime
{

View File

@ -1,5 +1,4 @@
using System.Reflection;
using Microsoft.Net.Runtime.Services;
namespace Microsoft.Net.Runtime
{

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
namespace Microsoft.Net.Runtime
{
[AssemblyNeutral]
public interface IDependencyExport
{
IList<IMetadataReference> MetadataReferences { get; }
IList<ISourceReference> SourceReferences { get; }
}
}

View File

@ -0,0 +1,10 @@
using System.Runtime.Versioning;
namespace Microsoft.Net.Runtime
{
[AssemblyNeutral]
public interface IDependencyExporter
{
IDependencyExport GetDependencyExport(string name, FrameworkName targetFramework);
}
}

View File

@ -0,0 +1,8 @@
namespace Microsoft.Net.Runtime
{
[AssemblyNeutral]
public interface IMetadataFileReference : IMetadataReference
{
string Path { get; }
}
}

View File

@ -0,0 +1,8 @@

namespace Microsoft.Net.Runtime
{
[AssemblyNeutral]
public interface IMetadataReference
{
}
}

View File

@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Runtime.Versioning;
namespace Microsoft.Net.Runtime.Services
namespace Microsoft.Net.Runtime
{
[AssemblyNeutral]
public interface IMetadataReferenceProvider

View File

@ -0,0 +1,10 @@
using Microsoft.CodeAnalysis;
namespace Microsoft.Net.Runtime
{
[AssemblyNeutral]
public interface IRoslynMetadataReference : IMetadataReference
{
MetadataReference MetadataReference { get; }
}
}

View File

@ -0,0 +1,7 @@
namespace Microsoft.Net.Runtime
{
[AssemblyNeutral]
public interface ISourceReference
{
}
}

View File

@ -1,24 +1,27 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.FileSystems;
using Microsoft.Net.Runtime;
namespace Microsoft.AspNet.Mvc.Razor
{
public class VirtualPathViewFactory : IVirtualPathViewFactory
{
private readonly IFileSystem _fileSystem;
private readonly PhysicalFileSystem _fileSystem;
private readonly IRazorCompilationService _compilationService;
public VirtualPathViewFactory(IFileSystem fileSystem, IRazorCompilationService compilationService)
public VirtualPathViewFactory(IApplicationEnvironment env, IRazorCompilationService compilationService)
{
_fileSystem = fileSystem;
// TODO: Continue to inject the IFileSystem but only when we get it from the host
_fileSystem = new PhysicalFileSystem(env.ApplicationBasePath);
_compilationService = compilationService;
}
public async Task<IView> CreateInstance(string virtualPath)
{
// TODO: We need to glean the approot from HttpContext
var appRoot = ((PhysicalFileSystem)_fileSystem).Root;
var appRoot = _fileSystem.Root;
IFileInfo fileInfo;
if (_fileSystem.TryGetFileInfo(virtualPath, out fileInfo))
{

View File

@ -35,6 +35,7 @@
"System.Dynamic.Runtime": "4.0.0.0",
"System.Globalization": "4.0.10.0",
"System.IO": "4.0.0.0",
"System.IO.FileSystem": "4.0.0.0",
"System.Linq": "4.0.0.0",
"System.Reflection": "4.0.10.0",
"System.Reflection.Compatibility": "4.0.0.0",

View File

@ -1,20 +1,17 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using Microsoft.AspNet.ConfigurationModel;
using Microsoft.AspNet.DependencyInjection;
using Microsoft.AspNet.DependencyInjection.NestedProviders;
using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Razor;
using Microsoft.Net.Runtime;
using Microsoft.AspNet.Mvc.Razor.Compilation;
namespace Microsoft.AspNet.Mvc
{
public class MvcServices
{
public static IEnumerable<IServiceDescriptor> GetDefaultServices(IConfiguration configuration,
IApplicationEnvironment env)
public static IEnumerable<IServiceDescriptor> GetDefaultServices(IConfiguration configuration)
{
var describe = new ServiceDescriber(configuration);
@ -27,19 +24,11 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Transient<IParameterDescriptorFactory, DefaultParameterDescriptorFactory>();
yield return describe.Transient<IControllerAssemblyProvider, AppDomainControllerAssemblyProvider>();
yield return describe.Transient<IActionDiscoveryConventions, DefaultActionDiscoveryConventions>();
yield return describe.Instance<IFileSystem>(new PhysicalFileSystem(env.ApplicationBasePath));
yield return describe.Instance<IMvcRazorHost>(new MvcRazorHost(typeof(RazorView).FullName));
#if NET45
// TODO: Container chaining to flow services from the host to this container
yield return describe.Transient<ICompilationService, RoslynCompilationService>();
yield return describe.Transient<ICompilationService, CscBasedCompilationService>();
// TODO: Make this work like normal when we get container chaining
// TODO: Update this when we have the new host services
// yield return describe.Instance<ICompilationService>(new RoslynCompilationService(hostServiceProvider));
#endif
yield return describe.Transient<IRazorCompilationService, RazorCompilationService>();
yield return describe.Transient<IVirtualPathViewFactory, VirtualPathViewFactory>();
yield return describe.Transient<IViewEngine, RazorViewEngine>();