From 211561a6a6eefa0adbdc63619b3bb3cb92cae2bc Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Sun, 25 Feb 2018 19:17:21 -0800 Subject: [PATCH] Implement 2-phase compilation This is a working (all tests passing) implementation of the two-phase compilation system we will need for component discovery. This builds on top of work we've doing in Razor, including the Razor SDK, MSBuild tasks, and CLI/server. This currently *does* discovery components during the build process, but it doesn't use that data for anything yet. It works like this: 1. Generate class declarations (structure only, no method bodies) 2. Compile a 'temp' assembly using the .cs files and output of 1. 3. Do component discovery using the 'temp' assembly 4. Generate class definitions (including method bodies) 5. Compile the 'real' assembly using the .cs files and output of 4. --- .../HostedInAspNet.Client.csproj | 3 +- .../MonoSanityClient/MonoSanityClient.csproj | 2 +- samples/StandaloneApp/StandaloneApp.csproj | 2 +- .../Cli/Commands/BuildRazorCommand.cs | 96 ---- .../Cli/Program.cs | 1 - .../ReferenceFromSource.props | 4 + .../targets/All.targets | 6 + .../targets/RazorCompilation.targets | 422 +++++++++++++++++- .../EliminateMethodBodyPass.cs | 35 ++ .../ComponentDiscoveryRazorIntegrationTest.cs | 63 +++ .../RazorIntegrationTestBase.cs | 128 ++++-- 11 files changed, 603 insertions(+), 159 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.Blazor.Build/Cli/Commands/BuildRazorCommand.cs create mode 100644 test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentDiscoveryRazorIntegrationTest.cs diff --git a/samples/HostedInAspNet.Client/HostedInAspNet.Client.csproj b/samples/HostedInAspNet.Client/HostedInAspNet.Client.csproj index a64eebc99a..83a30dd105 100644 --- a/samples/HostedInAspNet.Client/HostedInAspNet.Client.csproj +++ b/samples/HostedInAspNet.Client/HostedInAspNet.Client.csproj @@ -1,7 +1,8 @@ - + netstandard2.0 + Exe diff --git a/samples/MonoSanityClient/MonoSanityClient.csproj b/samples/MonoSanityClient/MonoSanityClient.csproj index 760b322366..7cc3c9d1fd 100644 --- a/samples/MonoSanityClient/MonoSanityClient.csproj +++ b/samples/MonoSanityClient/MonoSanityClient.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/samples/StandaloneApp/StandaloneApp.csproj b/samples/StandaloneApp/StandaloneApp.csproj index 253651db56..2fa7d0b67a 100644 --- a/samples/StandaloneApp/StandaloneApp.csproj +++ b/samples/StandaloneApp/StandaloneApp.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/src/Microsoft.AspNetCore.Blazor.Build/Cli/Commands/BuildRazorCommand.cs b/src/Microsoft.AspNetCore.Blazor.Build/Cli/Commands/BuildRazorCommand.cs deleted file mode 100644 index 94c8b9dbca..0000000000 --- a/src/Microsoft.AspNetCore.Blazor.Build/Cli/Commands/BuildRazorCommand.cs +++ /dev/null @@ -1,96 +0,0 @@ -// 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.Generic; -using System.IO; -using System.Linq; -using Microsoft.AspNetCore.Blazor.Razor; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.Extensions.CommandLineUtils; - -namespace Microsoft.AspNetCore.Blazor.Build.Cli.Commands -{ - internal class BuildRazorCommand - { - public static void Command(CommandLineApplication command) - { - // Later, we might want to have the complete list of inputs passed in from MSBuild - // so developers can include/exclude whatever they want. The MVC Razor view precompiler - // does this by writing the list to a temporary 'response' file then passing the path - // to that file into its build executable (see: https://github.com/aspnet/MvcPrecompilation/blob/dev/src/Microsoft.AspNetCore.Mvc.Razor.ViewCompilation/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.ViewCompilation.targets) - // For now it's sufficient to assume we want to include '**\*.cshtml' - var sourceDirPath = command.Option("--source", - "The path to the directory containing Razor files", - CommandOptionType.SingleValue); - var outputFilePath = command.Option("--output", - "The location where the resulting C# source file should be written", - CommandOptionType.SingleValue); - var baseNamespace = command.Option("--namespace", - "The base namespace for the generated C# classes.", - CommandOptionType.SingleValue); - var verboseFlag = command.Option("--verbose", - "Indicates that verbose console output should written", - CommandOptionType.NoValue); - - command.OnExecute(() => - { - if (!VerifyRequiredOptionsProvided(sourceDirPath, outputFilePath, baseNamespace)) - { - return 1; - } - - var sourceDirPathValue = sourceDirPath.Value(); - if (!Directory.Exists(sourceDirPathValue)) - { - Console.WriteLine($"ERROR: Directory not found: {sourceDirPathValue}"); - return 1; - } - - var fileSystem = RazorProjectFileSystem.Create(sourceDirPathValue); - var engine = RazorProjectEngine.Create(BlazorExtensionInitializer.DefaultConfiguration, fileSystem, b => - { - BlazorExtensionInitializer.Register(b); - }); - - var diagnostics = new List(); - var sourceFiles = FindRazorFiles(sourceDirPathValue).ToList(); - using (var outputWriter = new StreamWriter(outputFilePath.Value())) - { - foreach (var sourceFile in sourceFiles) - { - var item = fileSystem.GetItem(sourceFile); - - var codeDocument = engine.Process(item); - var cSharpDocument = codeDocument.GetCSharpDocument(); - - outputWriter.WriteLine(cSharpDocument.GeneratedCode); - diagnostics.AddRange(cSharpDocument.Diagnostics); - } - } - - foreach (var diagnostic in diagnostics) - { - Console.WriteLine(diagnostic.ToString()); - } - - var hasError = diagnostics.Any(item => item.Severity == RazorDiagnosticSeverity.Error); - return hasError ? 1 : 0; - }); - } - - private static IEnumerable FindRazorFiles(string rootDirPath) - => Directory.GetFiles(rootDirPath, "*.cshtml", SearchOption.AllDirectories); - - private static bool VerifyRequiredOptionsProvided(params CommandOption[] options) - { - var violations = options.Where(o => !o.HasValue()).ToList(); - foreach (var violation in violations) - { - Console.WriteLine($"ERROR: No value specified for required option '{violation.LongName}'."); - } - - return !violations.Any(); - } - } -} diff --git a/src/Microsoft.AspNetCore.Blazor.Build/Cli/Program.cs b/src/Microsoft.AspNetCore.Blazor.Build/Cli/Program.cs index 071d152ca5..d8022c7ce7 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/Cli/Program.cs +++ b/src/Microsoft.AspNetCore.Blazor.Build/Cli/Program.cs @@ -17,7 +17,6 @@ namespace Microsoft.AspNetCore.Blazor.Build app.HelpOption("-?|-h|--help"); app.Command("build", BuildCommand.Command); - app.Command("buildrazor", BuildRazorCommand.Command); if (args.Length > 0) { diff --git a/src/Microsoft.AspNetCore.Blazor.Build/ReferenceFromSource.props b/src/Microsoft.AspNetCore.Blazor.Build/ReferenceFromSource.props index 539ee2ff05..b8b1784992 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/ReferenceFromSource.props +++ b/src/Microsoft.AspNetCore.Blazor.Build/ReferenceFromSource.props @@ -10,6 +10,10 @@ + + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + dotnet "$(MSBuildThisFileDirectory)../tools/Microsoft.AspNetCore.Blazor.Build.dll" diff --git a/src/Microsoft.AspNetCore.Blazor.Build/targets/RazorCompilation.targets b/src/Microsoft.AspNetCore.Blazor.Build/targets/RazorCompilation.targets index 9c39d2f965..3714d48c9c 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/targets/RazorCompilation.targets +++ b/src/Microsoft.AspNetCore.Blazor.Build/targets/RazorCompilation.targets @@ -1,27 +1,411 @@ - - - - $(RootNamespace) - true - $(IntermediateOutputPath)BlazorRazorComponents.g.cs - $(ProjectDir.TrimEnd('\')) - - - - - - + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + + <_BlazorAngleSharpAssemblyPath>$(MSBuildThisFileDirectory)../tools/Microsoft.AspNetCore.Blazor.AngleSharp.dll + <_BlazorExtensionAssemblyPath>$(MSBuildThisFileDirectory)../tools/Microsoft.AspNetCore.Blazor.Razor.Extensions.dll + + Blazor-0.1 + + + + + + + MSBuild:BlazorGenerateDeclaration + + + + + + Blazor-0.1;Blazor.AngleSharp-0.1;$(CustomRazorExtension) + + + Blazor-0.1;Blazor.AngleSharp-0.1;$(CustomRazorExtension) + + + + Microsoft.AspNetCore.Blazor.AngleSharp + $(_BlazorAngleSharpAssemblyPath) + + + Microsoft.AspNetCore.Blazor.Razor.Extensions + $(_BlazorExtensionAssemblyPath) + + + + <_BlazorTempAssembly Include="$(IntermediateOutputPath)$(TargetName).BlazorTemp.dll" /> + - - - MSBuild:BlazorCompileRazorComponents - - + + + + _DefineBlazorPaths; + _AssignBlazorGenerateTargetPaths; + _HashBlazorGenerateInputs; + + + BlazorGenerateDeclaration; + BlazorResolveComponents; + + + + + + + + <_BlazorGenerateDeclarationComponentManifest>$(IntermediateOutputPath)Blazor.declaration.components.json + <_BlazorGenerateDefinitionComponentManifest>$(IntermediateOutputPath)Blazor.definition.components.json + <_BlazorComponentInputCache>$(IntermediateOutputPath)Blazor.components.input.cache + + + + + + + + + + + + + $(IntermediateOutputPath)$([System.IO.Path]::ChangeExtension('%(BlazorGenerateWithTargetPath.TargetPath)', 'g.i.cs')) + + + $(IntermediateOutputPath)$([System.IO.Path]::ChangeExtension('%(BlazorGenerateWithTargetPath.TargetPath)', 'g.cs')) + + + + + + + %(Identity) + + + + + + + + + + + + <_BlazorGenerateInputsHash> + <_BlazorGenerateInputsHashFile>$(IntermediateOutputPath)Blazor.inputs.txt + + + + + + + + + + + + + + + + + + + + <_BlazorGenerateDeclarationWithTargetPath Include="@(BlazorGenerateWithTargetPath)"> + %(GeneratedDeclaration) + + <_BlazorDeclarationConfiguration Include="@(RazorConfiguration->WithMetadataValue('Identity', 'BlazorDeclaration-0.1'))" /> + + + + + + + + + + + + + + + + + + + + <_BlazorReferencePathWithRefAssemblies Include="@(ReferencePathWithRefAssemblies);@(_BlazorTempAssembly)"/> + + + + <_BlazorDeclarationConfiguration Include="@(RazorConfiguration->WithMetadataValue('Identity', 'BlazorDeclaration-0.1'))" /> + + + + + + + + + + + + + + + + + + + + <_BlazorGenerateDefinitionWithTargetPath Include="@(BlazorGenerateWithTargetPath)"> + %(GeneratedDefinition) + + <_BlazorDefinitionConfiguration Include="@(RazorConfiguration->WithMetadataValue('Identity', 'Blazor-0.1'))" /> + + + + + + + + + + + + + + + + + + + + + $(NoWarn);1701;1702 + + + + + $(NoWarn);2008 + + + + + + + + + + + $(AppConfig) + + + + + false + + + + + + + + + + true + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/EliminateMethodBodyPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/EliminateMethodBodyPass.cs index 313ecfe7a1..1efd6e1b8d 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/EliminateMethodBodyPass.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/EliminateMethodBodyPass.cs @@ -25,7 +25,42 @@ namespace Microsoft.AspNetCore.Blazor.Razor } var method = documentNode.FindPrimaryMethod(); + if (method == null) + { + return; + } + method.Children.Clear(); + + // After we clear all of the method body there might be some unused fields, which can be + // blocking if compiling with warnings as errors. Suppress this warning so that it doesn't + // get annoying in VS. + documentNode.Children.Insert(documentNode.Children.IndexOf(documentNode.FindPrimaryNamespace()), new CSharpCodeIntermediateNode() + { + Children = + { + // Field is assigned but never used + new IntermediateToken() + { + Content = "#pragma warning disable 0414" + Environment.NewLine, + Kind = TokenKind.CSharp, + }, + + // Field is never assigned + new IntermediateToken() + { + Content = "#pragma warning disable 0649" + Environment.NewLine, + Kind = TokenKind.CSharp, + }, + + // Field is never used + new IntermediateToken() + { + Content = "#pragma warning disable 0169" + Environment.NewLine, + Kind = TokenKind.CSharp, + }, + }, + }); } } } diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentDiscoveryRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentDiscoveryRazorIntegrationTest.cs new file mode 100644 index 0000000000..d5f0d1ad95 --- /dev/null +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/ComponentDiscoveryRazorIntegrationTest.cs @@ -0,0 +1,63 @@ +// 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 Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Microsoft.AspNetCore.Blazor.Build.Test +{ + public class ComponentDiscoveryRazorIntegrationTest : RazorIntegrationTestBase + { + internal override bool UseTwoPhaseCompilation => true; + + [Fact] + public void ComponentDiscovery_CanFindComponent_DefinedinCSharp() + { + // Arrange + AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@" +using Microsoft.AspNetCore.Blazor.Components; + +using namespace Test +{ + public class MyComponent : BlazorComponent + { + } +} +")); + + // Act + var result = CompileToCSharp("@addTagHelper *, TestAssembly"); + + // Assert + var bindings = result.CodeDocument.GetTagHelperContext(); + Assert.Single(bindings.TagHelpers, t => t.Name == "Test.MyComponent"); + } + + [Fact] + public void ComponentDiscovery_CanFindComponent_DefinedinCshtml() + { + // Arrange + + // Act + var result = CompileToCSharp("UniqueName.cshtml", "@addTagHelper *, TestAssembly"); + + // Assert + var bindings = result.CodeDocument.GetTagHelperContext(); + Assert.Single(bindings.TagHelpers, t => t.Name == "Test.UniqueName"); + } + + [Fact] + public void ComponentDiscovery_CanFindComponent_BuiltIn() + { + // Arrange + + // Act + var result = CompileToCSharp("@addTagHelper *, Microsoft.AspNetCore.Blazor"); + + // Assert + var bindings = result.CodeDocument.GetTagHelperContext(); + Assert.Single(bindings.TagHelpers, t => t.Name == "Microsoft.AspNetCore.Blazor.Routing.NavLink"); + } + } +} diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorIntegrationTestBase.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorIntegrationTestBase.cs index 810c69f17a..c84cf02a03 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorIntegrationTestBase.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorIntegrationTestBase.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Blazor.Test.Helpers; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Razor; using Xunit; using Xunit.Sdk; @@ -26,10 +27,35 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test internal const string ArbitraryWindowsPath = "x:\\dir\\subdir\\Test"; internal const string ArbitraryMacLinuxPath = "/dir/subdir/Test"; - private RazorProjectEngine _projectEngine; + // Creating the initial compilation + reading references is on the order of 250ms without caching + // so making sure it doesn't happen for each test. + private static readonly CSharpCompilation BaseCompilation; + + static RazorIntegrationTestBase() + { + var referenceAssemblyRoots = new[] + { + typeof(System.Runtime.AssemblyTargetedPatchBandAttribute).Assembly, // System.Runtime + typeof(BlazorComponent).Assembly, + typeof(RazorIntegrationTestBase).Assembly, // Reference this assembly, so that we can refer to test component types + }; + + var referenceAssemblies = referenceAssemblyRoots + .SelectMany(assembly => assembly.GetReferencedAssemblies().Concat(new[] { assembly.GetName() })) + .Distinct() + .Select(Assembly.Load) + .Select(assembly => MetadataReference.CreateFromFile(assembly.Location)) + .ToList(); + BaseCompilation = CSharpCompilation.Create( + "TestAssembly", + Array.Empty(), + referenceAssemblies, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + } public RazorIntegrationTestBase() { + AdditionalSyntaxTrees = new List(); Configuration = BlazorExtensionInitializer.DefaultConfiguration; FileSystem = new VirtualRazorProjectFileSystem(); WorkingDirectory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ArbitraryWindowsPath : ArbitraryMacLinuxPath; @@ -38,6 +64,8 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test DefaultFileName = "TestComponent.cshtml"; } + internal List AdditionalSyntaxTrees { get; } + internal virtual RazorConfiguration Configuration { get; } internal virtual string DefaultBaseNamespace { get; } @@ -46,26 +74,21 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test internal virtual VirtualRazorProjectFileSystem FileSystem { get; } - internal virtual RazorProjectEngine ProjectEngine - { - get - { - if (_projectEngine == null) - { - _projectEngine = CreateProjectEngine(); - } - - return _projectEngine; - } - } + internal virtual bool UseTwoPhaseCompilation { get; } internal virtual string WorkingDirectory { get; } - internal RazorProjectEngine CreateProjectEngine() + internal RazorProjectEngine CreateProjectEngine(RazorConfiguration configuration, MetadataReference[] references) { - return RazorProjectEngine.Create(Configuration, FileSystem, b => + return RazorProjectEngine.Create(configuration, FileSystem, b => { BlazorExtensionInitializer.Register(b); + + b.Features.Add(new CompilationTagHelperFeature()); + b.Features.Add(new DefaultMetadataReferenceFeature() + { + References = references, + }); }); } @@ -95,13 +118,51 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test cshtmlRelativePath, Encoding.UTF8.GetBytes(cshtmlContent)); - var codeDocument = ProjectEngine.Process(projectItem); - return new CompileToCSharpResult + if (UseTwoPhaseCompilation) { - CodeDocument = codeDocument, - Code = codeDocument.GetCSharpDocument().GeneratedCode, - Diagnostics = codeDocument.GetCSharpDocument().Diagnostics, - }; + // The first phase won't include any metadata references for component discovery. This mirrors + // what the build does. + var projectEngine = CreateProjectEngine(BlazorExtensionInitializer.DeclarationConfiguration, Array.Empty()); + var codeDocument = projectEngine.Process(projectItem); + + // Result of generating declarations + var declaration = new CompileToCSharpResult + { + CodeDocument = codeDocument, + Code = codeDocument.GetCSharpDocument().GeneratedCode, + Diagnostics = codeDocument.GetCSharpDocument().Diagnostics, + }; + + // Result of doing 'temp' compilation + var tempAssembly = CompileToAssembly(declaration, BaseCompilation); + + // Add the 'temp' compilation as a metadata reference + var references = BaseCompilation.References.Concat(new[] { tempAssembly.Compilation.ToMetadataReference() }).ToArray(); + projectEngine = CreateProjectEngine(BlazorExtensionInitializer.DefaultConfiguration, references); + + // Result of real code + codeDocument = projectEngine.Process(projectItem); + return new CompileToCSharpResult + { + CodeDocument = codeDocument, + Code = codeDocument.GetCSharpDocument().GeneratedCode, + Diagnostics = codeDocument.GetCSharpDocument().Diagnostics, + }; + } + else + { + // For single phase compilation tests just use the base compilation's references. + // This will include the built-in Blazor components. + var projectEngine = CreateProjectEngine(Configuration, BaseCompilation.References.ToArray()); + + var codeDocument = projectEngine.Process(projectItem); + return new CompileToCSharpResult + { + CodeDocument = codeDocument, + Code = codeDocument.GetCSharpDocument().GeneratedCode, + Diagnostics = codeDocument.GetCSharpDocument().Diagnostics, + }; + } } protected CompileToAssemblyResult CompileToAssembly(string cshtmlRelativePath, string cshtmlContent) @@ -115,8 +176,10 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test return CompileToAssembly(cSharpResult); } - protected CompileToAssemblyResult CompileToAssembly(CompileToCSharpResult cSharpResult) + protected CompileToAssemblyResult CompileToAssembly(CompileToCSharpResult cSharpResult, CSharpCompilation baseCompilation = null) { + baseCompilation = baseCompilation ?? BaseCompilation; + if (cSharpResult.Diagnostics.Any()) { var diagnosticsLog = string.Join(Environment.NewLine, cSharpResult.Diagnostics.Select(d => d.ToString()).ToArray()); @@ -127,25 +190,8 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test { CSharpSyntaxTree.ParseText(cSharpResult.Code) }; - var referenceAssembliesContainingTypes = new[] - { - typeof(System.Runtime.AssemblyTargetedPatchBandAttribute), // System.Runtime - typeof(BlazorComponent), - typeof(RazorIntegrationTestBase), // Reference this assembly, so that we can refer to test component types - }; - var references = referenceAssembliesContainingTypes - .SelectMany(type => type.Assembly.GetReferencedAssemblies().Concat(new[] { type.Assembly.GetName() })) - .Distinct() - .Select(Assembly.Load) - .Select(assembly => MetadataReference.CreateFromFile(assembly.Location)) - .ToList(); - var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary); - var assemblyName = "TestAssembly" + Guid.NewGuid().ToString("N"); - var compilation = CSharpCompilation.Create(assemblyName, - syntaxTrees, - references, - options); + var compilation = baseCompilation.AddSyntaxTrees(syntaxTrees).AddSyntaxTrees(AdditionalSyntaxTrees); using (var peStream = new MemoryStream()) { compilation.Emit(peStream); @@ -155,6 +201,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test .Where(d => d.Severity != DiagnosticSeverity.Hidden); return new CompileToAssemblyResult { + Compilation = compilation, Diagnostics = diagnostics, Assembly = diagnostics.Any() ? null : Assembly.Load(peStream.ToArray()) }; @@ -216,6 +263,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test protected class CompileToAssemblyResult { public Assembly Assembly { get; set; } + public Compilation Compilation { get; set; } public string VerboseLog { get; set; } public IEnumerable Diagnostics { get; set; } }