diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CSharpCompiler.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CSharpCompiler.cs index 638c2911ed..93703d49c9 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CSharpCompiler.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CSharpCompiler.cs @@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal private CSharpParseOptions _parseOptions; private CSharpCompilationOptions _compilationOptions; private EmitOptions _emitOptions; + private bool _emitPdb; public CSharpCompiler(RazorReferenceManager manager, IHostingEnvironment hostingEnvironment) { @@ -50,6 +51,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal } } + public virtual bool EmitPdb + { + get + { + EnsureOptions(); + return _emitPdb; + } + } + public virtual EmitOptions EmitOptions { get @@ -105,6 +115,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal private EmitOptions GetEmitOptions(DependencyContextCompilationOptions dependencyContextOptions) { + // Assume we're always producing pdbs unless DebugType = none + _emitPdb = true; DebugInformationFormat debugInformationFormat; if (string.IsNullOrEmpty(dependencyContextOptions.DebugType)) { @@ -117,11 +129,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal // Based on https://github.com/dotnet/roslyn/blob/1d28ff9ba248b332de3c84d23194a1d7bde07e4d/src/Compilers/CSharp/Portable/CommandLine/CSharpCommandLineParser.cs#L624-L640 switch (dependencyContextOptions.DebugType.ToLower()) { + case "none": + // There isn't a way to represent none in DebugInformationFormat. + // We'll set EmitPdb to false and let callers handle it by setting a null pdb-stream. + _emitPdb = false; + return new EmitOptions(); case "portable": debugInformationFormat = DebugInformationFormat.PortablePdb; break; case "embedded": - debugInformationFormat = DebugInformationFormat.Embedded; + // Roslyn does not expose enough public APIs to produce a binary with embedded pdbs. + // We'll produce PortablePdb instead to continue providing a reasonable user experience. + debugInformationFormat = DebugInformationFormat.PortablePdb; break; case "full": case "pdbonly": diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs index 00f99600a2..fca0cb415d 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Razor.Hosting; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.FileProviders; @@ -133,7 +134,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal return cachedResult; } - var normalizedPath = GetNormalizedPath(relativePath); + var normalizedPath = GetNormalizedPath(relativePath); if (_cache.TryGetValue(normalizedPath, out cachedResult)) { return cachedResult; @@ -325,7 +326,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal // error message; for now, lets just be extra protective and assume 0 imports to not give a bad error. var imports = importFeature?.GetImports(projectItem) ?? Enumerable.Empty(); var physicalImports = imports.Where(import => import.FilePath != null); - + // Now that we have non-dynamic imports we need to get their RazorProjectItem equivalents so we have their // physical file paths (according to the FileSystem). foreach (var physicalImport in physicalImports) @@ -374,13 +375,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal var assemblyName = Path.GetRandomFileName(); var compilation = CreateCompilation(generatedCode, assemblyName); + var emitOptions = _csharpCompiler.EmitOptions; + var emitPdbFile = _csharpCompiler.EmitPdb && emitOptions.DebugInformationFormat != DebugInformationFormat.Embedded; + using (var assemblyStream = new MemoryStream()) - using (var pdbStream = new MemoryStream()) + using (var pdbStream = emitPdbFile ? new MemoryStream() : null) { var result = compilation.Emit( assemblyStream, pdbStream, - options: _csharpCompiler.EmitOptions); + options: emitOptions); if (!result.Success) { @@ -392,9 +396,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal } assemblyStream.Seek(0, SeekOrigin.Begin); - pdbStream.Seek(0, SeekOrigin.Begin); + pdbStream?.Seek(0, SeekOrigin.Begin); - var assembly = Assembly.Load(assemblyStream.ToArray(), pdbStream.ToArray()); + var assembly = Assembly.Load(assemblyStream.ToArray(), pdbStream?.ToArray()); _logger.GeneratedCodeToAssemblyCompilationEnd(codeDocument.Source.FilePath, startTimestamp); return assembly; diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CSharpCompilerTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CSharpCompilerTest.cs index 613bab7147..3faddaa0e6 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CSharpCompilerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CSharpCompilerTest.cs @@ -174,10 +174,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal } - [Theory] - [InlineData("portable", DebugInformationFormat.PortablePdb)] - [InlineData("embedded", DebugInformationFormat.Embedded)] - public void EmitOptions_ReadsDebugTypeFromDependencyContext(string debugType, DebugInformationFormat expected) + [Fact] + public void EmitOptions_ReadsDebugTypeFromDependencyContext() { // Arrange var dependencyContextOptions = new DependencyContextCompilationOptions( @@ -190,7 +188,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal keyFile: null, delaySign: null, publicSign: null, - debugType: debugType, + debugType: "portable", emitEntryPoint: null, generateXmlDocumentation: null); var referenceManager = Mock.Of(); @@ -200,7 +198,62 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal // Act & Assert var emitOptions = compiler.EmitOptions; - Assert.Equal(expected, emitOptions.DebugInformationFormat); + Assert.Equal(DebugInformationFormat.PortablePdb, emitOptions.DebugInformationFormat); + Assert.True(compiler.EmitPdb); + } + + [Fact] + public void EmitOptions_SetsDebugInformationFormatToPortable_WhenDebugTypeIsEmbedded() + { + // Arrange + var dependencyContextOptions = new DependencyContextCompilationOptions( + new[] { "MyDefine" }, + languageVersion: "7.1", + platform: null, + allowUnsafe: true, + warningsAsErrors: null, + optimize: null, + keyFile: null, + delaySign: null, + publicSign: null, + debugType: "embedded", + emitEntryPoint: null, + generateXmlDocumentation: null); + var referenceManager = Mock.Of(); + var hostingEnvironment = Mock.Of(); + + var compiler = new TestCSharpCompiler(referenceManager, hostingEnvironment, dependencyContextOptions); + + // Act & Assert + var emitOptions = compiler.EmitOptions; + Assert.Equal(DebugInformationFormat.PortablePdb, emitOptions.DebugInformationFormat); + Assert.True(compiler.EmitPdb); + } + + [Fact] + public void EmitOptions_DoesNotSetEmitPdb_IfDebugTypeIsNone() + { + // Arrange + var dependencyContextOptions = new DependencyContextCompilationOptions( + new[] { "MyDefine" }, + languageVersion: "7.1", + platform: null, + allowUnsafe: true, + warningsAsErrors: null, + optimize: null, + keyFile: null, + delaySign: null, + publicSign: null, + debugType: "none", + emitEntryPoint: null, + generateXmlDocumentation: null); + var referenceManager = Mock.Of(); + var hostingEnvironment = Mock.Of(); + + var compiler = new TestCSharpCompiler(referenceManager, hostingEnvironment, dependencyContextOptions); + + // Act & Assert + Assert.False(compiler.EmitPdb); } [Fact] diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerTest.cs index b75fd3fdfb..36dc4530ad 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerTest.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Hosting; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Emit; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; @@ -799,11 +800,52 @@ this should fail"; Assert.NotNull(result); } + [Fact] + public void CompileAndEmit_DoesNotThrowIfDebugTypeIsEmbedded() + { + // Arrange + var referenceManager = CreateReferenceManager(Options.Create(new RazorViewEngineOptions())); + var csharpCompiler = new TestCSharpCompiler(referenceManager, Mock.Of()) + { + EmitOptionsSettable = new EmitOptions(debugInformationFormat: DebugInformationFormat.Embedded), + }; + + var compiler = GetViewCompiler(csharpCompiler: csharpCompiler); + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "some-relative-path.cshtml")); + + // Act + var result = compiler.CompileAndEmit(codeDocument, "public class Test{}"); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public void CompileAndEmit_WorksIfEmitPdbIsNotSet() + { + // Arrange + var referenceManager = CreateReferenceManager(Options.Create(new RazorViewEngineOptions())); + var csharpCompiler = new TestCSharpCompiler(referenceManager, Mock.Of()) + { + EmitPdbSettable = false, + }; + + var compiler = GetViewCompiler(csharpCompiler: csharpCompiler); + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "some-relative-path.cshtml")); + + // Act + var result = compiler.CompileAndEmit(codeDocument, "public class Test{}"); + + // Assert + Assert.NotNull(result); + } + private static TestRazorViewCompiler GetViewCompiler( TestFileProvider fileProvider = null, Action compilationCallback = null, RazorReferenceManager referenceManager = null, - IList precompiledViews = null) + IList precompiledViews = null, + CSharpCompiler csharpCompiler = null) { fileProvider = fileProvider ?? new TestFileProvider(); var accessor = Mock.Of(a => a.FileProvider == fileProvider); @@ -812,12 +854,7 @@ this should fail"; var options = Options.Create(new RazorViewEngineOptions()); if (referenceManager == null) { - var applicationPartManager = new ApplicationPartManager(); - var assembly = typeof(RazorViewCompilerTest).Assembly; - applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly)); - applicationPartManager.FeatureProviders.Add(new MetadataReferenceFeatureProvider()); - - referenceManager = new DefaultRazorReferenceManager(applicationPartManager, options); + referenceManager = CreateReferenceManager(options); } precompiledViews = precompiledViews ?? Array.Empty(); @@ -828,15 +865,28 @@ this should fail"; { RazorExtensions.Register(builder); }); + + csharpCompiler = csharpCompiler ?? new CSharpCompiler(referenceManager, hostingEnvironment); + var viewCompiler = new TestRazorViewCompiler( fileProvider, projectEngine, - new CSharpCompiler(referenceManager, hostingEnvironment), + csharpCompiler, compilationCallback, precompiledViews); return viewCompiler; } + private static RazorReferenceManager CreateReferenceManager(IOptions options) + { + var applicationPartManager = new ApplicationPartManager(); + var assembly = typeof(RazorViewCompilerTest).Assembly; + applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly)); + applicationPartManager.FeatureProviders.Add(new MetadataReferenceFeatureProvider()); + + return new DefaultRazorReferenceManager(applicationPartManager, options); + } + private class TestRazorViewCompiler : RazorViewCompiler { public TestRazorViewCompiler( @@ -866,5 +916,21 @@ this should fail"; return Compile(relativePath); } } + + private class TestCSharpCompiler : CSharpCompiler + { + public TestCSharpCompiler(RazorReferenceManager manager, IHostingEnvironment hostingEnvironment) + : base(manager, hostingEnvironment) + { + } + + public EmitOptions EmitOptionsSettable { get; set; } + + public bool EmitPdbSettable { get; set; } + + public override EmitOptions EmitOptions => EmitOptionsSettable; + + public override bool EmitPdb => EmitPdbSettable; + } } } \ No newline at end of file