// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.PortableExecutable; #if DOTNET5_5 using System.Runtime.Loader; #endif using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.Mvc.Razor.Internal; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Emit; using Microsoft.Dnx.Compilation.CSharp; using Microsoft.Extensions.CompilationAbstractions; using Microsoft.Extensions.PlatformAbstractions; using Microsoft.Extensions.OptionsModel; namespace Microsoft.AspNet.Mvc.Razor.Compilation { /// /// A type that uses Roslyn to compile C# content. /// public class RoslynCompilationService : ICompilationService { private readonly Lazy _supportsPdbGeneration = new Lazy(SymbolsUtility.SupportsSymbolsGeneration); private readonly ConcurrentDictionary _metadataFileCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); private readonly ILibraryExporter _libraryExporter; private readonly IApplicationEnvironment _environment; private readonly IFileProvider _fileProvider; private readonly Lazy> _applicationReferences; private readonly string _classPrefix; private readonly Action _compilationCallback; private readonly CSharpParseOptions _parseOptions; private readonly CSharpCompilationOptions _compilationOptions; #if DOTNET5_5 private readonly RazorLoadContext _razorLoadContext; #endif /// /// Initalizes a new instance of the class. /// /// The environment for the executing application. /// /// The accessor for the used to load compiled assemblies. /// /// The library manager that provides export and reference information. /// /// The that provides Roslyn compilation settings. /// /// The that was used to generate the code. public RoslynCompilationService( IApplicationEnvironment environment, ILibraryExporter libraryExporter, IMvcRazorHost host, IOptions optionsAccessor) { _environment = environment; _libraryExporter = libraryExporter; _applicationReferences = new Lazy>(GetApplicationReferences); _fileProvider = optionsAccessor.Value.FileProvider; _classPrefix = host.MainClassNamePrefix; _compilationCallback = optionsAccessor.Value.CompilationCallback; _parseOptions = optionsAccessor.Value.ParseOptions; _compilationOptions = optionsAccessor.Value.CompilationOptions; #if DOTNET5_5 _razorLoadContext = new RazorLoadContext(); #endif } /// public CompilationResult Compile(RelativeFileInfo fileInfo, string compilationContent) { if (fileInfo == null) { throw new ArgumentNullException(nameof(fileInfo)); } if (compilationContent == null) { throw new ArgumentNullException(nameof(compilationContent)); } var assemblyName = Path.GetRandomFileName(); var syntaxTree = SyntaxTreeGenerator.Generate( compilationContent, assemblyName, _parseOptions); var references = _applicationReferences.Value; var compilation = CSharpCompilation.Create( assemblyName, options: _compilationOptions, syntaxTrees: new[] { syntaxTree }, references: references); compilation = Rewrite(compilation); var compilationContext = new RoslynCompilationContext(compilation); _compilationCallback(compilationContext); compilation = compilationContext.Compilation; using (var ms = new MemoryStream()) { using (var pdb = new MemoryStream()) { EmitResult result; if (_supportsPdbGeneration.Value) { result = compilation.Emit(ms, pdbStream: pdb); } else { result = compilation.Emit(ms); } if (!result.Success) { return GetCompilationFailedResult( fileInfo.RelativePath, compilationContent, assemblyName, result.Diagnostics); } Assembly assembly; ms.Seek(0, SeekOrigin.Begin); if (_supportsPdbGeneration.Value) { pdb.Seek(0, SeekOrigin.Begin); assembly = LoadStream(ms, pdb); } else { assembly = LoadStream(ms, assemblySymbols: null); } var type = assembly.GetExportedTypes() .First(t => t.Name.StartsWith(_classPrefix, StringComparison.Ordinal)); return new CompilationResult(type); } } } private Assembly LoadStream(MemoryStream ms, MemoryStream assemblySymbols) { #if NET451 return Assembly.Load(ms.ToArray(), assemblySymbols?.ToArray()); #else return _razorLoadContext.Load(ms, assemblySymbols); #endif } private CSharpCompilation Rewrite(CSharpCompilation compilation) { var rewrittenTrees = new List(); foreach (var tree in compilation.SyntaxTrees) { var semanticModel = compilation.GetSemanticModel(tree, ignoreAccessibility: true); var rewriter = new ExpressionRewriter(semanticModel); var rewrittenTree = tree.WithRootAndOptions(rewriter.Visit(tree.GetRoot()), tree.Options); rewrittenTrees.Add(rewrittenTree); } return compilation.RemoveAllSyntaxTrees().AddSyntaxTrees(rewrittenTrees); } // Internal for unit testing internal CompilationResult GetCompilationFailedResult( string relativePath, string compilationContent, string assemblyName, IEnumerable diagnostics) { var diagnosticGroups = diagnostics .Where(IsError) .GroupBy(diagnostic => GetFilePath(relativePath, diagnostic), StringComparer.Ordinal); var failures = new List(); foreach (var group in diagnosticGroups) { var sourceFilePath = group.Key; string sourceFileContent; if (string.Equals(assemblyName, sourceFilePath, StringComparison.Ordinal)) { // The error is in the generated code and does not have a mapping line pragma sourceFileContent = compilationContent; sourceFilePath = Resources.GeneratedCodeFileName; } else { sourceFileContent = ReadFileContentsSafely(_fileProvider, sourceFilePath); } var compilationFailure = new CompilationFailure( sourceFilePath, sourceFileContent, compilationContent, group.Select(d => d.ToDiagnosticMessage(_environment.RuntimeFramework))); failures.Add(compilationFailure); } return new CompilationResult(failures); } private static string GetFilePath(string relativePath, Diagnostic diagnostic) { if (diagnostic.Location == Location.None) { return relativePath; } return diagnostic.Location.GetMappedLineSpan().Path; } private List GetApplicationReferences() { var references = new List(); // Get the MetadataReference for the executing application. If it's a Roslyn reference, // we can copy the references created when compiling the application to the Razor page being compiled. // This avoids performing expensive calls to MetadataReference.CreateFromImage. var libraryExport = _libraryExporter.GetExport(_environment.ApplicationName); if (libraryExport?.MetadataReferences != null && libraryExport.MetadataReferences.Count > 0) { Debug.Assert(libraryExport.MetadataReferences.Count == 1, "Expected 1 MetadataReferences, found " + libraryExport.MetadataReferences.Count); var roslynReference = libraryExport.MetadataReferences[0] as IRoslynMetadataReference; var compilationReference = roslynReference?.MetadataReference as CompilationReference; if (compilationReference != null) { references.AddRange(compilationReference.Compilation.References); references.Add(roslynReference.MetadataReference); return references; } } var export = _libraryExporter.GetAllExports(_environment.ApplicationName); if (export != null) { foreach (var metadataReference in export.MetadataReferences) { // Taken from https://github.com/aspnet/KRuntime/blob/757ba9bfdf80bd6277e715d6375969a7f44370ee/src/... // Microsoft.Extensions.Runtime.Roslyn/RoslynCompiler.cs#L164 // We don't want to take a dependency on the Roslyn bit directly since it pulls in more dependencies // than the view engine needs (Microsoft.Extensions.Runtime) for example references.Add(ConvertMetadataReference(metadataReference)); } } return references; } private MetadataReference ConvertMetadataReference(IMetadataReference metadataReference) { var roslynReference = metadataReference as IRoslynMetadataReference; if (roslynReference != null) { return roslynReference.MetadataReference; } var embeddedReference = metadataReference as IMetadataEmbeddedReference; if (embeddedReference != null) { return MetadataReference.CreateFromImage(embeddedReference.Contents); } var fileMetadataReference = metadataReference as IMetadataFileReference; if (fileMetadataReference != null) { return CreateMetadataFileReference(fileMetadataReference.Path); } var projectReference = metadataReference as IMetadataProjectReference; if (projectReference != null) { using (var ms = new MemoryStream()) { projectReference.EmitReferenceAssembly(ms); return MetadataReference.CreateFromImage(ms.ToArray()); } } throw new NotSupportedException(); } private MetadataReference CreateMetadataFileReference(string path) { var metadata = _metadataFileCache.GetOrAdd(path, _ => { using (var stream = File.OpenRead(path)) { var moduleMetadata = ModuleMetadata.CreateFromStream(stream, PEStreamOptions.PrefetchMetadata); return AssemblyMetadata.Create(moduleMetadata); } }); return metadata.GetReference(filePath: path); } private static bool IsError(Diagnostic diagnostic) { return diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error; } private static string ReadFileContentsSafely(IFileProvider fileProvider, string filePath) { var fileInfo = fileProvider.GetFileInfo(filePath); if (fileInfo.Exists) { try { using (var reader = new StreamReader(fileInfo.CreateReadStream())) { return reader.ReadToEnd(); } } catch { // Ignore any failures } } return null; } #if DOTNET5_5 private class RazorLoadContext : AssemblyLoadContext { protected override Assembly Load(AssemblyName assemblyName) { return Default.LoadFromAssemblyName(assemblyName); } public Assembly Load(Stream assembly, Stream assemblySymbols) { return LoadFromStream(assembly, assemblySymbols); } } #endif } }