diff --git a/Mvc.NoFun.sln b/Mvc.NoFun.sln index f67e635663..90fabd036d 100644 --- a/Mvc.NoFun.sln +++ b/Mvc.NoFun.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -81,6 +81,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Mvc.Te EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "MvcSandbox", "samples\MvcSandbox\MvcSandbox.xproj", "{14ED4476-9F24-4776-8417-EA6927F6C9C9}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools", "src\Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools\Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools.xproj", "{F8BF7D95-0633-407F-BB0B-02563F13C068}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design", "src\Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design\Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design.xproj", "{4339FC9B-AEC6-442A-B413-A41555ED76C7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -464,6 +468,30 @@ Global {14ED4476-9F24-4776-8417-EA6927F6C9C9}.Release|Mixed Platforms.Build.0 = Release|Any CPU {14ED4476-9F24-4776-8417-EA6927F6C9C9}.Release|x86.ActiveCfg = Release|Any CPU {14ED4476-9F24-4776-8417-EA6927F6C9C9}.Release|x86.Build.0 = Release|Any CPU + {F8BF7D95-0633-407F-BB0B-02563F13C068}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8BF7D95-0633-407F-BB0B-02563F13C068}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8BF7D95-0633-407F-BB0B-02563F13C068}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {F8BF7D95-0633-407F-BB0B-02563F13C068}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {F8BF7D95-0633-407F-BB0B-02563F13C068}.Debug|x86.ActiveCfg = Debug|Any CPU + {F8BF7D95-0633-407F-BB0B-02563F13C068}.Debug|x86.Build.0 = Debug|Any CPU + {F8BF7D95-0633-407F-BB0B-02563F13C068}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8BF7D95-0633-407F-BB0B-02563F13C068}.Release|Any CPU.Build.0 = Release|Any CPU + {F8BF7D95-0633-407F-BB0B-02563F13C068}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {F8BF7D95-0633-407F-BB0B-02563F13C068}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {F8BF7D95-0633-407F-BB0B-02563F13C068}.Release|x86.ActiveCfg = Release|Any CPU + {F8BF7D95-0633-407F-BB0B-02563F13C068}.Release|x86.Build.0 = Release|Any CPU + {4339FC9B-AEC6-442A-B413-A41555ED76C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4339FC9B-AEC6-442A-B413-A41555ED76C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4339FC9B-AEC6-442A-B413-A41555ED76C7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4339FC9B-AEC6-442A-B413-A41555ED76C7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4339FC9B-AEC6-442A-B413-A41555ED76C7}.Debug|x86.ActiveCfg = Debug|Any CPU + {4339FC9B-AEC6-442A-B413-A41555ED76C7}.Debug|x86.Build.0 = Debug|Any CPU + {4339FC9B-AEC6-442A-B413-A41555ED76C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4339FC9B-AEC6-442A-B413-A41555ED76C7}.Release|Any CPU.Build.0 = Release|Any CPU + {4339FC9B-AEC6-442A-B413-A41555ED76C7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {4339FC9B-AEC6-442A-B413-A41555ED76C7}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {4339FC9B-AEC6-442A-B413-A41555ED76C7}.Release|x86.ActiveCfg = Release|Any CPU + {4339FC9B-AEC6-442A-B413-A41555ED76C7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -502,5 +530,7 @@ Global {8FC726B5-E766-44E0-8B38-1313B6D8D9A7} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {9879B5D5-2325-4A81-B4DF-F279FE8FEEB4} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {14ED4476-9F24-4776-8417-EA6927F6C9C9} = {DAAE4C74-D06F-4874-A166-33305D2643CE} + {F8BF7D95-0633-407F-BB0B-02563F13C068} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} + {4339FC9B-AEC6-442A-B413-A41555ED76C7} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AssemblyPart.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AssemblyPart.cs index 6426753d4b..7ad0d8fa0d 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AssemblyPart.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AssemblyPart.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using Microsoft.AspNetCore.Mvc.Core.ApplicationParts; using Microsoft.Extensions.DependencyModel; namespace Microsoft.AspNetCore.Mvc.ApplicationParts @@ -12,8 +13,16 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts /// /// An backed by an . /// - public class AssemblyPart : ApplicationPart, IApplicationPartTypeProvider, ICompilationReferencesProvider + public class AssemblyPart : + ApplicationPart, + IApplicationPartTypeProvider, + ICompilationReferencesProvider, + IPrecompiledViewsProvider { + public static readonly string PrecompiledViewsAssemblySuffix = ".PrecompiledViews"; + public static readonly string ViewCollectionNamespace = "AspNetCore"; + public static readonly string ViewCollectionTypeName = "__PrecompiledViewCollection"; + /// /// Initalizes a new instance. /// @@ -41,6 +50,27 @@ public AssemblyPart(Assembly assembly) /// public IEnumerable Types => Assembly.DefinedTypes; + /// + public IReadOnlyCollection PrecompiledViews + { + get + { + var precompiledAssemblyName = new AssemblyName(Assembly.FullName); + precompiledAssemblyName.Name = precompiledAssemblyName.Name + PrecompiledViewsAssemblySuffix; + + var viewFactoryTypeName = $"{ViewCollectionNamespace}.{ViewCollectionTypeName},{precompiledAssemblyName}"; + var viewFactoryType = Type.GetType(viewFactoryTypeName); + + if (viewFactoryType == null) + { + return null; + } + + var precompiledViews = Activator.CreateInstance(viewFactoryType) as PrecompiledViews; + return precompiledViews?.ViewInfos; + } + } + /// public IEnumerable GetReferencePaths() { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IPrecompiledViewsProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IPrecompiledViewsProvider.cs new file mode 100644 index 0000000000..0325777330 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IPrecompiledViewsProvider.cs @@ -0,0 +1,19 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ApplicationParts; + +namespace Microsoft.AspNetCore.Mvc.Core.ApplicationParts +{ + /// + /// Exposes a sequence of precompiled views associated with an . + /// + public interface IPrecompiledViewsProvider + { + /// + /// Gets the sequence of . + /// + IReadOnlyCollection PrecompiledViews { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/PrecompiledViewInfo.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/PrecompiledViewInfo.cs new file mode 100644 index 0000000000..3cfc14cfa8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/PrecompiledViewInfo.cs @@ -0,0 +1,34 @@ +// 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; + +namespace Microsoft.AspNetCore.Mvc.Core.ApplicationParts +{ + /// + /// Provides information for precompiled views. + /// + public class PrecompiledViewInfo + { + /// + /// Creates a new instance of . + /// + /// The path of the view. + /// The view . + public PrecompiledViewInfo(string path, Type type) + { + Path = path; + Type = type; + } + + /// + /// The path of the view. + /// + public string Path { get; } + + /// + /// The view . + /// + public Type Type { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/PrecompiledViews.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/PrecompiledViews.cs new file mode 100644 index 0000000000..4fed43ff6b --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/PrecompiledViews.cs @@ -0,0 +1,24 @@ +// 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.Collections.Generic; + +namespace Microsoft.AspNetCore.Mvc.Core.ApplicationParts +{ + /// + /// A container for instances. + /// + public abstract class PrecompiledViews + { + /// + /// Initializes a new instance of . + /// + /// The sequence of . + protected PrecompiledViews(IReadOnlyCollection precompiledViews) + { + ViewInfos = precompiledViews; + } + + public IReadOnlyCollection ViewInfos { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DesignTime/IMvcBuilderConfiguration.cs b/src/Microsoft.AspNetCore.Mvc.Core/DesignTime/IMvcBuilderConfiguration.cs new file mode 100644 index 0000000000..49f9a90d6c --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/DesignTime/IMvcBuilderConfiguration.cs @@ -0,0 +1,20 @@ +// 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.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Mvc.DesignTime +{ + /// + /// Configures the . Implement this interface to enable design-time configuration + /// (for instance during pre-compilation of views) of . + /// + public interface IMvcBuilderConfiguration + { + /// + /// Configures the . + /// + /// + void ConfigureMvc(IMvcBuilder builder); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/CommonOptions.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/CommonOptions.cs new file mode 100644 index 0000000000..9540724a97 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/CommonOptions.cs @@ -0,0 +1,39 @@ +// 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.Extensions.CommandLineUtils; + +namespace Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design.Internal +{ + public class CommonOptions + { + public static readonly string ConfigureCompilationTypeTemplate = "--configure-compilation-type"; + public static readonly string ContentRootTemplate = "--content-root"; + + public CommandArgument ProjectArgument { get; private set; } + + public CommandOption ConfigureCompilationType { get; private set; } + + public CommandOption ContentRootOption { get; private set; } + + public void Configure(CommandLineApplication app) + { + app.Description = "Precompiles an application."; + app.HelpOption("-?|-h|--help"); + + ProjectArgument = app.Argument( + "project", + "The path to the project (project folder or project.json) with precompilation."); + + ConfigureCompilationType = app.Option( + ConfigureCompilationTypeTemplate, + "Type with Configure method", + CommandOptionType.SingleValue); + + ContentRootOption = app.Option( + ContentRootTemplate, + "The application's content root.", + CommandOptionType.SingleValue); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/DebugHelper.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/DebugHelper.cs new file mode 100644 index 0000000000..dde371bb33 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/DebugHelper.cs @@ -0,0 +1,25 @@ +// 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. + +#if DEBUG +using System; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design.Internal +{ + public static class DebugHelper + { + public static void HandleDebugSwitch(ref string[] args) + { + if (args.Length > 0 && string.Equals("--debug", args[0], StringComparison.OrdinalIgnoreCase)) + { + args = args.Skip(1).ToArray(); + Console.WriteLine("Waiting for debugger to attach. Press ENTER to continue"); + Console.WriteLine($"Process ID: {Process.GetCurrentProcess().Id}"); + Console.ReadLine(); + } + } + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/MvcServicesProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/MvcServicesProvider.cs new file mode 100644 index 0000000000..f9be9aaa37 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/MvcServicesProvider.cs @@ -0,0 +1,115 @@ +// 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Internal; +using Microsoft.AspNetCore.Mvc.DesignTime; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design.Internal +{ + public class MvcServicesProvider + { + private readonly string _projectPath; + private readonly string _contentRoot; + private readonly string _applicationName; + + public MvcServicesProvider( + string projectPath, + string applicationName, + string contentRoot, + string configureCompilationType) + { + _projectPath = projectPath; + _contentRoot = contentRoot; + _applicationName = applicationName; + + var mvcBuilderConfiguration = GetConfigureCompilationAction(configureCompilationType); + var serviceProvider = GetProvider(mvcBuilderConfiguration); + + Host = serviceProvider.GetRequiredService(); + Compiler = serviceProvider.GetRequiredService(); + + FileProvider = serviceProvider.GetRequiredService().FileProvider; + } + + public IMvcRazorHost Host { get; } + + public CSharpCompiler Compiler { get; } + + public IFileProvider FileProvider { get; } + + private IMvcBuilderConfiguration GetConfigureCompilationAction(string configureCompilationType) + { + Type type; + if (!string.IsNullOrEmpty(configureCompilationType)) + { + type = Type.GetType(configureCompilationType); + if (type == null) + { + throw new InvalidOperationException($"Unable to find type '{type}."); + } + } + else + { + var assemblyName = new AssemblyName(_applicationName); + var assembly = Assembly.Load(assemblyName); + type = assembly + .GetExportedTypes() + .FirstOrDefault(typeof(IMvcBuilderConfiguration).IsAssignableFrom); + } + + if (type == null) + { + return null; + } + + var instance = Activator.CreateInstance(type) as IMvcBuilderConfiguration; + if (instance == null) + { + throw new InvalidOperationException($"Type {configureCompilationType} does not implement " + + $"{typeof(IMvcBuilderConfiguration)}."); + } + + return instance; + } + + private IServiceProvider GetProvider(IMvcBuilderConfiguration mvcBuilderConfiguration) + { + var services = new ServiceCollection(); + + var hostingEnvironment = new HostingEnvironment + { + ApplicationName = _applicationName, + WebRootFileProvider = new PhysicalFileProvider(_projectPath), + ContentRootFileProvider = new PhysicalFileProvider(_contentRoot), + ContentRootPath = _contentRoot, + }; + var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore"); + + services + .AddSingleton(hostingEnvironment) + .AddSingleton(diagnosticSource) + .AddLogging() + .AddSingleton(); + + var mvcCoreBuilder = services + .AddMvcCore() + .AddRazorViewEngine(); + + var mvcBuilder = new MvcBuilder(mvcCoreBuilder.Services, mvcCoreBuilder.PartManager); + mvcBuilderConfiguration?.ConfigureMvc(mvcBuilder); + + return mvcBuilder.Services.BuildServiceProvider(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/PrecompilationApplication.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/PrecompilationApplication.cs new file mode 100644 index 0000000000..72e4861055 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/PrecompilationApplication.cs @@ -0,0 +1,61 @@ +// 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.Reflection; +using Microsoft.Extensions.CommandLineUtils; + +namespace Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design.Internal +{ + public class PrecompilationApplication : CommandLineApplication + { + private readonly Type _callingType; + + public PrecompilationApplication(Type callingType) + { + _callingType = callingType; + + Name = "razor-precompile"; + FullName = "Microsoft Razor Precompilation Utility"; + Description = "Precompiles Razor views."; + ShortVersionGetter = GetInformationalVersion; + + HelpOption("-?|-h|--help"); + + OnExecute(() => + { + ShowHelp(); + return 2; + }); + } + + public new int Execute(params string[] args) + { + try + { + return base.Execute(args); + } + catch (Exception ex) + { + Console.Error.WriteLine(ex.Message); +#if DEBUG + Console.Error.WriteLine(ex); +#endif + return 1; + } + } + + private string GetInformationalVersion() + { + var assembly = _callingType.GetTypeInfo().Assembly; + var attributes = assembly.GetCustomAttributes( + typeof(AssemblyInformationalVersionAttribute)) as AssemblyInformationalVersionAttribute[]; + + var versionAttribute = attributes.Length == 0 ? + assembly.GetName().Version.ToString() : + attributes[0].InformationalVersion; + + return versionAttribute; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/PrecompileRunCommand.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/PrecompileRunCommand.cs new file mode 100644 index 0000000000..51ec692613 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/PrecompileRunCommand.cs @@ -0,0 +1,238 @@ +// 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design.Internal +{ + public class PrecompileRunCommand + { + public static readonly string ApplicationNameTemplate = "--applicationName"; + public static readonly string OutputPathTemplate = "--output-path"; + private static readonly ParallelOptions ParalellOptions = new ParallelOptions + { + MaxDegreeOfParallelism = 4 + }; + + private CommandOption OutputPathOption { get; set; } + + private CommandOption ApplicationNameOption { get; set; } + + private MvcServicesProvider ServicesProvider { get; set; } + + private CommonOptions Options { get; } = new CommonOptions(); + + private StrongNameOptions StrongNameOptions { get; } = new StrongNameOptions(); + + private string ProjectPath { get; set; } + + public void Configure(CommandLineApplication app) + { + Options.Configure(app); + StrongNameOptions.Configure(app); + + OutputPathOption = app.Option( + OutputPathTemplate, + "Path to the emit the precompiled assembly to.", + CommandOptionType.SingleValue); + + ApplicationNameOption = app.Option( + ApplicationNameTemplate, + "Name of the application to produce precompiled assembly for.", + CommandOptionType.SingleValue); + + app.OnExecute(() => Execute()); + } + + private int Execute() + { + ParseArguments(); + + ServicesProvider = new MvcServicesProvider( + ProjectPath, + ApplicationNameOption.Value(), + Options.ContentRootOption.Value(), + Options.ConfigureCompilationType.Value()); + + Console.WriteLine("Running Razor view precompilation."); + + var stopWatch = Stopwatch.StartNew(); + var results = ParseViews(); + var success = true; + foreach (var result in results) + { + if (!result.GeneratorResults.Success) + { + success = false; + foreach (var error in result.GeneratorResults.ParserErrors) + { + Console.Error.WriteLine($"{error.Location.FilePath} ({error.Location.LineIndex}): {error.Message}"); + } + } + } + + if (!success) + { + return 1; + } + + var precompileAssemblyName = $"{ApplicationNameOption.Value()}{AssemblyPart.PrecompiledViewsAssemblySuffix}"; + var compilation = CompileViews(results, precompileAssemblyName); + + var assemblyPath = Path.Combine(OutputPathOption.Value(), precompileAssemblyName + ".dll"); + var emitResult = EmitAssembly(compilation, assemblyPath); + + if (!emitResult.Success) + { + foreach (var diagnostic in emitResult.Diagnostics) + { + Console.Error.WriteLine(CSharpDiagnosticFormatter.Instance.Format(diagnostic)); + } + + return 1; + } + + stopWatch.Stop(); + Console.WriteLine($"Precompiled views emitted to {assemblyPath}."); + Console.WriteLine($"Successfully compiled {results.Length} Razor views in {stopWatch.ElapsedMilliseconds}ms."); + return 0; + } + + private EmitResult EmitAssembly(CSharpCompilation compilation, string assemblyPath) + { + Directory.CreateDirectory(Path.GetDirectoryName(assemblyPath)); + + EmitResult emitResult; + using (var assemblyStream = File.OpenWrite(assemblyPath)) + { + using (var pdbStream = File.OpenWrite(Path.ChangeExtension(assemblyPath, ".pdb"))) + { + emitResult = compilation.Emit( + assemblyStream, + pdbStream, + options: ServicesProvider.Compiler.EmitOptions); + } + } + + return emitResult; + } + + private CSharpCompilation CompileViews(ViewCompilationInfo[] results, string assemblyname) + { + var compiler = ServicesProvider.Compiler; + var compilation = compiler.CreateCompilation(assemblyname); + var syntaxTrees = new SyntaxTree[results.Length]; + + Parallel.For(0, results.Length, ParalellOptions, i => + { + var result = results[i]; + var sourceText = SourceText.From(result.GeneratorResults.GeneratedCode, Encoding.UTF8); + var fileInfo = result.RelativeFileInfo; + var syntaxTree = compiler.CreateSyntaxTree(sourceText) + .WithFilePath(fileInfo.FileInfo.PhysicalPath ?? fileInfo.RelativePath); + syntaxTrees[i] = syntaxTree; + }); + + compilation = compilation.AddSyntaxTrees(syntaxTrees); + for (var i = 0; i < results.Length; i++) + { + results[i].TypeName = ReadTypeInfo(compilation, syntaxTrees[i]); + } + + compilation = compiler.ProcessCompilation(compilation); + var codeGenerator = new ViewCollectionCodeGenerator(compiler, compilation); + codeGenerator.AddViewFactory(results); + + var assemblyName = new AssemblyName(ApplicationNameOption.Value()); + assemblyName = Assembly.Load(assemblyName).GetName(); + codeGenerator.AddAssemblyMetadata(assemblyName, StrongNameOptions); + + return codeGenerator.Compilation; + } + + private void ParseArguments() + { + ProjectPath = Options.ProjectArgument.Value; + if (string.IsNullOrEmpty(ProjectPath)) + { + throw new ArgumentException("Project path not specified."); + } + + if (!OutputPathOption.HasValue()) + { + throw new ArgumentException($"Option {OutputPathTemplate} does not specify a value."); + } + + if (!ApplicationNameOption.HasValue()) + { + throw new ArgumentException($"Option {ApplicationNameTemplate} does not specify a value."); + } + } + + private ViewCompilationInfo[] ParseViews() + { + var files = new List(); + GetRazorFiles(ServicesProvider.FileProvider, files, root: string.Empty); + var results = new ViewCompilationInfo[files.Count]; + Parallel.For(0, results.Length, ParalellOptions, i => + { + var fileInfo = files[i]; + using (var fileStream = fileInfo.FileInfo.CreateReadStream()) + { + var result = ServicesProvider.Host.GenerateCode(fileInfo.RelativePath, fileStream); + results[i] = new ViewCompilationInfo(fileInfo, result); + } + }); + + return results; + } + + private static void GetRazorFiles(IFileProvider fileProvider, List razorFiles, string root) + { + foreach (var fileInfo in fileProvider.GetDirectoryContents(root)) + { + var relativePath = Path.Combine(root, fileInfo.Name); + if (fileInfo.IsDirectory) + { + GetRazorFiles(fileProvider, razorFiles, relativePath); + } + else if (fileInfo.Name.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase)) + { + razorFiles.Add(new RelativeFileInfo(fileInfo, relativePath)); + } + } + } + + private string ReadTypeInfo(CSharpCompilation compilation, SyntaxTree syntaxTree) + { + var semanticModel = compilation.GetSemanticModel(syntaxTree, ignoreAccessibility: true); + var classDeclarations = syntaxTree.GetRoot().DescendantNodes().OfType(); + foreach (var declaration in classDeclarations) + { + var typeModel = semanticModel.GetDeclaredSymbol(declaration); + if (typeModel.ContainingType == null && typeModel.DeclaredAccessibility == Accessibility.Public) + { + return typeModel.ToDisplayString(); + } + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/SnkUtils.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/SnkUtils.cs new file mode 100644 index 0000000000..db72ea864c --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/SnkUtils.cs @@ -0,0 +1,88 @@ +// 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.Immutable; +using System.IO; + +namespace Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design.Internal +{ + // Copied from https://github.com/dotnet/cli/blob/rel/1.0.0/src/Microsoft.DotNet.ProjectModel.Workspaces/SnkUtils.cs + public static class SnkUtils + { + const byte PUBLICKEYBLOB = 0x06; + const byte PRIVATEKEYBLOB = 0x07; + + private const uint CALG_RSA_SIGN = 0x00002400; + private const uint CALG_SHA = 0x00008004; + + private const uint RSA1 = 0x31415352; //"RSA1" publickeyblob + private const uint RSA2 = 0x32415352; //"RSA2" privatekeyblob + + private const int VersionOffset = 1; + private const int ModulusLengthOffset = 12; + private const int ExponentOffset = 16; + private const int MagicPrivateKeyOffset = 8; + private const int MagicPublicKeyOffset = 20; + + public static ImmutableArray ExtractPublicKey(byte[] snk) + { + ValidateBlob(snk); + + if (snk[0] != PRIVATEKEYBLOB) + { + return ImmutableArray.Create(snk); + } + + var version = snk[VersionOffset]; + int modulusBitLength = ReadInt32(snk, ModulusLengthOffset); + uint exponent = (uint)ReadInt32(snk, ExponentOffset); + var modulus = new byte[modulusBitLength >> 3]; + + Array.Copy(snk, 20, modulus, 0, modulus.Length); + + return CreatePublicKey(version, exponent, modulus); + } + + private static void ValidateBlob(byte[] snk) + { + // 160 - the size of public key + if (snk.Length >= 160) + { + if (snk[0] == PRIVATEKEYBLOB && ReadInt32(snk, MagicPrivateKeyOffset) == RSA2 || // valid private key + snk[12] == PUBLICKEYBLOB && ReadInt32(snk, MagicPublicKeyOffset) == RSA1) // valid public key + { + return; + } + } + + throw new InvalidOperationException("Invalid key file."); + } + + private static int ReadInt32(byte[] array, int index) + { + return array[index] | array[index + 1] << 8 | array[index + 2] << 16 | array[index + 3] << 24; + } + + private static ImmutableArray CreatePublicKey(byte version, uint exponent, byte[] modulus) + { + using (var ms = new MemoryStream(160)) + using (var binaryWriter = new BinaryWriter(ms)) + { + binaryWriter.Write(CALG_RSA_SIGN); + binaryWriter.Write(CALG_SHA); + // total size of the rest of the blob (20 - size of RSAPUBKEY) + binaryWriter.Write(modulus.Length + 20); + binaryWriter.Write(PUBLICKEYBLOB); + binaryWriter.Write(version); + binaryWriter.Write((ushort)0x00000000); // reserved + binaryWriter.Write(CALG_RSA_SIGN); + binaryWriter.Write(RSA1); + binaryWriter.Write(modulus.Length << 3); + binaryWriter.Write(exponent); + binaryWriter.Write(modulus); + return ImmutableArray.Create(ms.ToArray()); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/StrongNameOptions.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/StrongNameOptions.cs new file mode 100644 index 0000000000..e079707018 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/StrongNameOptions.cs @@ -0,0 +1,44 @@ +// 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.Extensions.CommandLineUtils; + +namespace Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design.Internal +{ + public class StrongNameOptions + { + public static readonly string StrongNameKeyPath = "--key-file"; + public static readonly string DelaySignTemplate = "--delay-sign"; + public static readonly string PublicSignTemplate = "--public-sign"; + + public CommandOption KeyFileOption { get; set; } + + public CommandOption DelaySignOption { get; private set; } + + public CommandOption PublicSignOption { get; private set; } + + public void Configure(CommandLineApplication app) + { + KeyFileOption = app.Option( + StrongNameKeyPath, + "Strong name key path", + CommandOptionType.SingleValue); + + DelaySignOption = app.Option( + DelaySignTemplate, + "Determines if the precompiled view assembly is to be delay signed.", + CommandOptionType.NoValue); + + PublicSignOption = app.Option( + PublicSignTemplate, + "Determines if the precompiled view assembly is to be public signed.", + CommandOptionType.NoValue); + } + + public string KeyFile => KeyFileOption.Value(); + + public bool DelaySign => DelaySignOption.HasValue(); + + public bool PublicSign => PublicSignOption.HasValue(); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/ViewCollectionCodeGenerator.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/ViewCollectionCodeGenerator.cs new file mode 100644 index 0000000000..54d642e1c3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/ViewCollectionCodeGenerator.cs @@ -0,0 +1,86 @@ +// 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.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Core.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design.Internal +{ + public class ViewCollectionCodeGenerator + { + public ViewCollectionCodeGenerator( + CSharpCompiler compiler, + CSharpCompilation compilation) + { + Compiler = compiler; + Compilation = compilation; + } + + public CSharpCompiler Compiler { get; } + + public CSharpCompilation Compilation { get; private set; } + + public void AddViewFactory(IList result) + { + var precompiledViewsArray = new StringBuilder(); + foreach (var item in result) + { + precompiledViewsArray.AppendLine( + $"new {typeof(PrecompiledViewInfo).FullName}(@\"{item.RelativeFileInfo.RelativePath}\", typeof({item.TypeName})),"); + } + + var factoryContent = + $@" +namespace {AssemblyPart.ViewCollectionNamespace} +{{ + public class {AssemblyPart.ViewCollectionTypeName} : {typeof(PrecompiledViews).FullName} + {{ + public {AssemblyPart.ViewCollectionTypeName}() : base(new[] + {{ + {precompiledViewsArray} + }}) + {{ + }} + }} +}}"; + var syntaxTree = Compiler.CreateSyntaxTree(SourceText.From(factoryContent)); + Compilation = Compilation.AddSyntaxTrees(syntaxTree); + } + + public void AddAssemblyMetadata( + AssemblyName applicationAssemblyName, + StrongNameOptions strongNameOptions) + { + if (!string.IsNullOrEmpty(strongNameOptions.KeyFile)) + { + var updatedOptions = Compilation.Options.WithStrongNameProvider(new DesktopStrongNameProvider()); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || strongNameOptions.PublicSign) + { + updatedOptions = updatedOptions.WithCryptoPublicKey( + SnkUtils.ExtractPublicKey(File.ReadAllBytes(strongNameOptions.KeyFile))); + } + else + { + updatedOptions = updatedOptions.WithCryptoKeyFile(strongNameOptions.KeyFile) + .WithDelaySign(strongNameOptions.DelaySign); + } + + Compilation = Compilation.WithOptions(updatedOptions); + } + + var assemblyVersionContent = $"[assembly:{typeof(AssemblyVersionAttribute).FullName}(\"{applicationAssemblyName.Version}\")]"; + var syntaxTree = Compiler.CreateSyntaxTree(SourceText.From(assemblyVersionContent)); + Compilation = Compilation.AddSyntaxTrees(syntaxTree); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/ViewCompilationInfo.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/ViewCompilationInfo.cs new file mode 100644 index 0000000000..f2ac3b44e3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Internal/ViewCompilationInfo.cs @@ -0,0 +1,25 @@ +// 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.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Razor.CodeGenerators; + +namespace Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design.Internal +{ + public class ViewCompilationInfo + { + public ViewCompilationInfo( + RelativeFileInfo relativeFileInfo, + GeneratorResults generatorResults) + { + RelativeFileInfo = relativeFileInfo; + GeneratorResults = generatorResults; + } + + public RelativeFileInfo RelativeFileInfo { get; } + + public GeneratorResults GeneratorResults { get; } + + public string TypeName { get; set; } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Microsoft.AspNetCore.Mvc.Core.Test.xproj b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design.xproj similarity index 55% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/Microsoft.AspNetCore.Mvc.Core.Test.xproj rename to src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design.xproj index 7a89944c5a..253d7491f8 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Microsoft.AspNetCore.Mvc.Core.Test.xproj +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design.xproj @@ -1,20 +1,18 @@  - + - 14.0 + 14.0.25420 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - + - a8aa326e-8ee8-4f11-b750-23028e0949d7 + 4339fc9b-aec6-442a-b413-a41555ed76c7 .\obj .\bin\ + 2.0 - - - - + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Program.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Program.cs new file mode 100644 index 0000000000..ee05cf6b9c --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Program.cs @@ -0,0 +1,67 @@ +// 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.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design.Internal; + +namespace Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design +{ + public class Program + { + private readonly static Type ProgramType = typeof(Program); + + public static int Main(string[] args) + { +#if DEBUG + DebugHelper.HandleDebugSwitch(ref args); +#endif + + EnsureValidDispatchRecipient(ref args); + + var app = new PrecompilationApplication(ProgramType); + new PrecompileRunCommand().Configure(app); + return app.Execute(args); + } + + private static void EnsureValidDispatchRecipient(ref string[] args) + { + const string DispatcherVersionArgumentName = "--dispatcher-version"; + + if (!args.Contains(DispatcherVersionArgumentName, StringComparer.OrdinalIgnoreCase)) + { + return; + } + + var dispatcherArgumentIndex = Array.FindIndex( + args, + (value) => string.Equals(value, DispatcherVersionArgumentName, StringComparison.OrdinalIgnoreCase)); + var dispatcherArgumentValueIndex = dispatcherArgumentIndex + 1; + if (dispatcherArgumentValueIndex < args.Length) + { + var dispatcherVersion = args[dispatcherArgumentValueIndex]; + + var thisAssembly = ProgramType.GetTypeInfo().Assembly; + var version = thisAssembly.GetCustomAttribute() + ?.InformationalVersion + ?? thisAssembly.GetName().Version.ToString(); + + if (string.Equals(dispatcherVersion, version, StringComparison.Ordinal)) + { + // Remove dispatcher arguments from + var preDispatcherArgument = args.Take(dispatcherArgumentIndex); + var postDispatcherArgument = args.Skip(dispatcherArgumentIndex + 2); + var newProgramArguments = preDispatcherArgument.Concat(postDispatcherArgument); + args = newProgramArguments.ToArray(); + return; + } + } + + var thisAssemblyName = typeof(Program).GetTypeInfo().Assembly.GetName().Name; + + // Could not validate the dispatcher version. + throw new InvalidOperationException("Could not invoke tool"); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..2dc4003a17 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +// 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.Reflection; +using System.Resources; + +[assembly: AssemblyMetadata("Serviceable", "True")] +[assembly: NeutralResourcesLanguage("en-us")] +[assembly: AssemblyCompany("Microsoft Corporation.")] +[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] +[assembly: AssemblyProduct("Microsoft ASP.NET Core")] diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/project.json b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/project.json new file mode 100644 index 0000000000..26565cfcec --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design/project.json @@ -0,0 +1,41 @@ +{ + "version": "1.0.0-*", + "description": "Razor precompilation", + "packOptions": { + "repository": { + "type": "git", + "url": "git://github.com/AspNet/Mvc" + }, + "tags": [ + "cshtml", + "razor", + "precompilation", + "aspnetcore" + ] + }, + "buildOptions": { + "keyFile": "../../tools/Key.snk", + "warningsAsErrors": true, + "emitEntryPoint": true, + "nowarn": [ + "CS1591" + ], + "xmlDoc": true + }, + "dependencies": { + "Microsoft.AspNetCore.Hosting": "1.1.0-*", + "Microsoft.AspNetCore.Mvc.Razor": "1.1.0-*", + "Microsoft.Extensions.CommandLineUtils": "1.1.0-*" + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.0-*" + } + } + }, + "net451": {} + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools/Internal/PrecompileDispatchCommand.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools/Internal/PrecompileDispatchCommand.cs new file mode 100644 index 0000000000..664cec4e17 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools/Internal/PrecompileDispatchCommand.cs @@ -0,0 +1,183 @@ +// 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.Diagnostics; +using System.IO; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design.Internal; +using Microsoft.DotNet.InternalAbstractions; +using Microsoft.DotNet.ProjectModel; +using Microsoft.Extensions.CommandLineUtils; +using Microsoft.Extensions.Internal; +using NuGet.Frameworks; + +namespace Microsoft.AspNetCore.Mvc.Razor.Precompilation.Internal +{ + public class PrecompileDispatchCommand + { + private CommonOptions Options { get; } = new CommonOptions(); + + private CommandOption FrameworkOption { get; set; } + + private CommandOption ConfigurationOption { get; set; } + + private CommandOption OutputPathOption { get; set; } + + private NuGetFramework TargetFramework { get; set; } + + private CommandOption BuildBasePathOption { get; set; } + + private string ProjectPath { get; set; } + + private string Configuration { get; set; } + + private string OutputPath { get; set; } + + public void Configure(CommandLineApplication app) + { + Options.Configure(app); + FrameworkOption = app.Option( + "-f|--framework", + "Target Framework", + CommandOptionType.SingleValue); + + ConfigurationOption = app.Option( + "-c|--configuration", + "Configuration", + CommandOptionType.SingleValue); + + OutputPathOption = app.Option( + "-o|--output-path", + "Published path of the application.", + CommandOptionType.SingleValue); + + app.OnExecute(() => Execute()); + } + + private int Execute() + { + ParseArguments(); + + var runtimeContext = GetRuntimeContext(); + + var outputPaths = runtimeContext.GetOutputPaths(Configuration); + var applicationName = Path.GetFileNameWithoutExtension(outputPaths.CompilationFiles.Assembly); + var dispatchArgs = new List + { +#if DEBUG + "--debug", +#endif + ProjectPath, + PrecompileRunCommand.ApplicationNameTemplate, + applicationName, + PrecompileRunCommand.OutputPathTemplate, + OutputPath, + CommonOptions.ContentRootTemplate, + Options.ContentRootOption.Value() ?? Directory.GetCurrentDirectory(), + }; + + if (Options.ConfigureCompilationType.HasValue()) + { + dispatchArgs.Add(CommonOptions.ConfigureCompilationTypeTemplate); + dispatchArgs.Add(Options.ConfigureCompilationType.Value()); + } + + var compilerOptions = runtimeContext.ProjectFile.GetCompilerOptions(TargetFramework, Configuration); + if (!string.IsNullOrEmpty(compilerOptions.KeyFile)) + { + dispatchArgs.Add(StrongNameOptions.StrongNameKeyPath); + var keyFilePath = Path.GetFullPath(Path.Combine(runtimeContext.ProjectDirectory, compilerOptions.KeyFile)); + dispatchArgs.Add(keyFilePath); + + if (compilerOptions.DelaySign ?? false) + { + dispatchArgs.Add(StrongNameOptions.DelaySignTemplate); + } + + if (compilerOptions.PublicSign ?? false) + { + dispatchArgs.Add(StrongNameOptions.PublicSignTemplate); + } + } + + var toolName = typeof(Design.Program).GetTypeInfo().Assembly.GetName().Name; + var dispatchCommand = DotnetToolDispatcher.CreateDispatchCommand( + dispatchArgs, + TargetFramework, + Configuration, + outputPath: outputPaths.RuntimeOutputPath, + buildBasePath: null, + projectDirectory: ProjectPath, + toolName: toolName); + + var commandExitCode = dispatchCommand + .ForwardStdErr(Console.Error) + .ForwardStdOut(Console.Out) + .Execute() + .ExitCode; + + return commandExitCode; + } + + private void ParseArguments() + { + ProjectPath = GetProjectPath(); + Configuration = ConfigurationOption.Value() ?? DotNet.Cli.Utils.Constants.DefaultConfiguration; + + if (!FrameworkOption.HasValue()) + { + throw new Exception($"Option {FrameworkOption.Template} does not have a value."); + } + TargetFramework = NuGetFramework.Parse(FrameworkOption.Value()); + + if (!OutputPathOption.HasValue()) + { + throw new Exception($"Option {OutputPathOption.Template} does not have a value."); + } + OutputPath = OutputPathOption.Value(); + } + + private string GetProjectPath() + { + string projectPath; + if (!string.IsNullOrEmpty(Options.ProjectArgument.Value)) + { + projectPath = Path.GetFullPath(Options.ProjectArgument.Value); + if (string.Equals(Path.GetFileName(ProjectPath), "project.json", StringComparison.OrdinalIgnoreCase)) + { + projectPath = Path.GetDirectoryName(ProjectPath); + } + + if (!Directory.Exists(projectPath)) + { + throw new InvalidOperationException($"Could not find directory {projectPath}."); + } + } + else + { + projectPath = Directory.GetCurrentDirectory(); + } + + return projectPath; + } + + private ProjectContext GetRuntimeContext() + { + var workspace = new BuildWorkspace(ProjectReaderSettings.ReadFromEnvironment()); + + var projectContext = workspace.GetProjectContext(ProjectPath, TargetFramework); + if (projectContext == null) + { + Debug.Assert(FrameworkOption.HasValue()); + throw new InvalidOperationException($"Project '{ProjectPath}' does not support framework: {FrameworkOption.Value()}"); + } + + var runtimeContext = workspace.GetRuntimeContext( + projectContext, + RuntimeEnvironmentRidExtensions.GetAllCandidateRuntimeIdentifiers()); + return runtimeContext; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Cors.Test/Microsoft.AspNetCore.Mvc.Cors.Test.xproj b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools.xproj similarity index 55% rename from test/Microsoft.AspNetCore.Mvc.Cors.Test/Microsoft.AspNetCore.Mvc.Cors.Test.xproj rename to src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools.xproj index 014f4d66cb..6b5961a59f 100644 --- a/test/Microsoft.AspNetCore.Mvc.Cors.Test/Microsoft.AspNetCore.Mvc.Cors.Test.xproj +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools.xproj @@ -1,20 +1,18 @@  - + - 14.0 + 14.0.25420 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - + - 6bb4c20b-24c0-45d6-9e4c-c2620959bdd5 + f8bf7d95-0633-407f-bb0b-02563f13c068 .\obj .\bin\ + 2.0 - - - - + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools/Program.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools/Program.cs new file mode 100644 index 0000000000..3ca5029d70 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools/Program.cs @@ -0,0 +1,22 @@ +// 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.Mvc.Razor.Precompilation.Design.Internal; +using Microsoft.AspNetCore.Mvc.Razor.Precompilation.Internal; + +namespace Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools +{ + public class Program + { + public static int Main(string[] args) + { +#if DEBUG + DebugHelper.HandleDebugSwitch(ref args); +#endif + + var app = new PrecompilationApplication(typeof(Program)); + new PrecompileDispatchCommand().Configure(app); + return app.Execute(args); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..2dc4003a17 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +// 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.Reflection; +using System.Resources; + +[assembly: AssemblyMetadata("Serviceable", "True")] +[assembly: NeutralResourcesLanguage("en-us")] +[assembly: AssemblyCompany("Microsoft Corporation.")] +[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] +[assembly: AssemblyProduct("Microsoft ASP.NET Core")] diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools/project.json b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools/project.json new file mode 100644 index 0000000000..6612d92a65 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Precompilation.Tools/project.json @@ -0,0 +1,50 @@ +{ + "version": "1.0.0-*", + "description": "Razor precompilation", + "packOptions": { + "repository": { + "type": "git", + "url": "git://github.com/AspNet/Mvc" + }, + "tags": [ + "cshtml", + "razor", + "precompilation", + "aspnetcore" + ] + }, + "buildOptions": { + "outputName": "dotnet-razor-precompile", + "keyFile": "../../tools/Key.snk", + "warningsAsErrors": true, + "emitEntryPoint": true, + "nowarn": [ + "CS1591" + ], + + "xmlDoc": true + }, + "dependencies": { + "Microsoft.AspNetCore.Mvc.Razor.Precompilation.Design": "1.0.0-*", + "Microsoft.DotNet.Cli.Utils": "1.0.0-*", + "Microsoft.Extensions.CommandLineUtils": "1.1.0-*", + "Microsoft.Extensions.DotnetToolDispatcher.Sources": { + "type": "build", + "version": "1.1.0-*" + } + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.DotNet.ProjectModel.Loader": "1.0.0-*", + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.0-*" + }, + "System.Diagnostics.Process": "4.1.0-*", + "System.Runtime.Serialization.Primitives": "4.1.1-*" + } + }, + "net451": {} + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/PrecompiledViewsFeature.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/PrecompiledViewsFeature.cs new file mode 100644 index 0000000000..4efeb118fe --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/PrecompiledViewsFeature.cs @@ -0,0 +1,14 @@ +// 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; + +namespace Microsoft.AspNetCore.Mvc.Razor.Compilation +{ + public class PrecompiledViewsFeature + { + public IDictionary PrecompiledViews { get; } = + new Dictionary(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/PrecompiledViewsFeatureProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/PrecompiledViewsFeatureProvider.cs new file mode 100644 index 0000000000..7f2b477bac --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/PrecompiledViewsFeatureProvider.cs @@ -0,0 +1,28 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Core.ApplicationParts; + +namespace Microsoft.AspNetCore.Mvc.Razor.Compilation +{ + public class PrecompiledViewsFeatureProvider : IApplicationFeatureProvider + { + public void PopulateFeature(IEnumerable parts, PrecompiledViewsFeature feature) + { + foreach (var provider in parts.OfType()) + { + var precompiledViews = provider.PrecompiledViews; + if (precompiledViews != null) + { + foreach (var viewInfo in precompiledViews) + { + feature.PrecompiledViews[viewInfo.Path] = viewInfo.Type; + } + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs index f944874d6f..ba80ddbb05 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs @@ -72,6 +72,11 @@ private static void AddRazorViewEngineFeatureProviders(IMvcCoreBuilder builder) { builder.PartManager.FeatureProviders.Add(new MetadataReferenceFeatureProvider()); } + + if (!builder.PartManager.FeatureProviders.OfType().Any()) + { + builder.PartManager.FeatureProviders.Add(new PrecompiledViewsFeatureProvider()); + } } /// @@ -127,6 +132,8 @@ public static IMvcCoreBuilder InitializeTagHelper( // Internal for testing. internal static void AddRazorViewEngineServices(IServiceCollection services) { + services.TryAddSingleton(); + services.TryAddSingleton(); // This caches compilation related details that are valid across the lifetime of the application. services.TryAddSingleton(); @@ -165,7 +172,7 @@ internal static void AddRazorViewEngineServices(IServiceCollection services) // creating the singleton RazorViewEngine instance. services.TryAddTransient(); services.TryAddTransient(); - services.TryAddTransient(); + services.TryAddTransient(); // This caches Razor page activation details that are valid for the lifetime of the application. services.TryAddSingleton(); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CSharpCompiler.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CSharpCompiler.cs new file mode 100644 index 0000000000..4fdb1e07e5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CSharpCompiler.cs @@ -0,0 +1,81 @@ +// 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 Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + public class CSharpCompiler + { + private readonly CSharpCompilationOptions _compilationOptions; + private readonly CSharpParseOptions _parseOptions; + private readonly RazorReferenceManager _referenceManager; + private readonly Action _compilationCallback; + private readonly DebugInformationFormat _pdbFormat = +#if NET451 + SymbolsUtility.SupportsFullPdbGeneration() ? + DebugInformationFormat.Pdb : + DebugInformationFormat.PortablePdb; +#else + DebugInformationFormat.PortablePdb; +#endif + + public CSharpCompiler(RazorReferenceManager manager, IOptions optionsAccessor) + { + _referenceManager = manager; + _compilationOptions = optionsAccessor.Value.CompilationOptions; + _parseOptions = optionsAccessor.Value.ParseOptions; + _compilationCallback = optionsAccessor.Value.CompilationCallback; + EmitOptions = new EmitOptions(debugInformationFormat: _pdbFormat); + } + + public EmitOptions EmitOptions { get; } + + public SyntaxTree CreateSyntaxTree(SourceText sourceText) + { + return CSharpSyntaxTree.ParseText( + sourceText, + options: _parseOptions); + } + + public CSharpCompilation CreateCompilation(string assemblyName) + { + return CSharpCompilation.Create( + assemblyName, + options: _compilationOptions, + references: _referenceManager.CompilationReferences); + } + + public CSharpCompilation ProcessCompilation(CSharpCompilation compilation) + { + var compilationContext = new RoslynCompilationContext(compilation); + Rewrite(compilationContext); + _compilationCallback(compilationContext); + + return compilationContext.Compilation; + } + + private void Rewrite(RoslynCompilationContext compilationContext) + { + var compilation = compilationContext.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); + } + + compilationContext.Compilation = compilation.RemoveAllSyntaxTrees().AddSyntaxTrees(rewrittenTrees); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultCompilerCacheProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultCompilerCacheProvider.cs index f38ddd4457..aaa64d1745 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultCompilerCacheProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultCompilerCacheProvider.cs @@ -1,7 +1,8 @@ // 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.Extensions.Options; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; namespace Microsoft.AspNetCore.Mvc.Razor.Internal { @@ -13,10 +14,15 @@ public class DefaultCompilerCacheProvider : ICompilerCacheProvider /// /// Initializes a new instance of . /// + /// The /// The . - public DefaultCompilerCacheProvider(IRazorViewEngineFileProviderAccessor fileProviderAccessor) + public DefaultCompilerCacheProvider( + ApplicationPartManager applicationPartManager, + IRazorViewEngineFileProviderAccessor fileProviderAccessor) { - Cache = new CompilerCache(fileProviderAccessor.FileProvider); + var feature = new PrecompiledViewsFeature(); + applicationPartManager.PopulateFeature(feature); + Cache = new CompilerCache(fileProviderAccessor.FileProvider, feature.PrecompiledViews); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationService.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationService.cs index 833c541949..44d9c3d8bf 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationService.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationService.cs @@ -8,17 +8,13 @@ using System.Linq; using System.Reflection; using System.Text; -using System.Threading; using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.Razor.Internal { @@ -33,59 +29,27 @@ public class DefaultRoslynCompilationService : ICompilationService // error CS0246: The type or namespace name 'T' could not be found (are you missing a using directive // or an assembly reference?) private const string CS0246 = nameof(CS0246); - private readonly DebugInformationFormat _pdbFormat = -#if NET451 - SymbolsUtility.SupportsFullPdbGeneration() ? - DebugInformationFormat.Pdb : - DebugInformationFormat.PortablePdb; -#else - DebugInformationFormat.PortablePdb; -#endif - private readonly ApplicationPartManager _partManager; + + private readonly CSharpCompiler _compiler; private readonly IFileProvider _fileProvider; - private readonly Action _compilationCallback; - private readonly CSharpParseOptions _parseOptions; - private readonly CSharpCompilationOptions _compilationOptions; - private readonly IList _additionalMetadataReferences; private readonly ILogger _logger; - private object _compilationReferencesLock = new object(); - private bool _compilationReferencesInitialized; - private IList _compilationReferences; /// /// Initalizes a new instance of the class. /// - /// The . - /// Accessor to . + /// The . /// The . /// The . public DefaultRoslynCompilationService( - ApplicationPartManager partManager, - IOptions optionsAccessor, + CSharpCompiler compiler, IRazorViewEngineFileProviderAccessor fileProviderAccessor, ILoggerFactory loggerFactory) { - _partManager = partManager; + _compiler = compiler; _fileProvider = fileProviderAccessor.FileProvider; - _compilationCallback = optionsAccessor.Value.CompilationCallback; - _parseOptions = optionsAccessor.Value.ParseOptions; - _compilationOptions = optionsAccessor.Value.CompilationOptions; - _additionalMetadataReferences = optionsAccessor.Value.AdditionalCompilationReferences; _logger = loggerFactory.CreateLogger(); } - private IList CompilationReferences - { - get - { - return LazyInitializer.EnsureInitialized( - ref _compilationReferences, - ref _compilationReferencesInitialized, - ref _compilationReferencesLock, - GetCompilationReferences); - } - } - /// public CompilationResult Compile(RelativeFileInfo fileInfo, string compilationContent) { @@ -100,28 +64,15 @@ public CompilationResult Compile(RelativeFileInfo fileInfo, string compilationCo } _logger.GeneratedCodeToAssemblyCompilationStart(fileInfo.RelativePath); - var startTimestamp = _logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : 0; var assemblyName = Path.GetRandomFileName(); - var sourceText = SourceText.From(compilationContent, Encoding.UTF8); - var syntaxTree = CSharpSyntaxTree.ParseText( - sourceText, - path: assemblyName, - options: _parseOptions); - - var compilation = CSharpCompilation.Create( - assemblyName, - options: _compilationOptions, - syntaxTrees: new[] { syntaxTree }, - references: CompilationReferences); - - compilation = Rewrite(compilation); - - var compilationContext = new RoslynCompilationContext(compilation); - _compilationCallback(compilationContext); - compilation = compilationContext.Compilation; + var syntaxTree = _compiler.CreateSyntaxTree(sourceText).WithFilePath(assemblyName); + var compilation = _compiler + .CreateCompilation(assemblyName) + .AddSyntaxTrees(syntaxTree); + compilation = _compiler.ProcessCompilation(compilation); using (var assemblyStream = new MemoryStream()) { @@ -130,7 +81,7 @@ public CompilationResult Compile(RelativeFileInfo fileInfo, string compilationCo var result = compilation.Emit( assemblyStream, pdbStream, - options: new EmitOptions(debugInformationFormat: _pdbFormat)); + options: _compiler.EmitOptions); if (!result.Success) { @@ -144,8 +95,9 @@ public CompilationResult Compile(RelativeFileInfo fileInfo, string compilationCo assemblyStream.Seek(0, SeekOrigin.Begin); pdbStream.Seek(0, SeekOrigin.Begin); - var assembly = LoadStream(assemblyStream, pdbStream); + var assembly = LoadAssembly(assemblyStream, pdbStream); var type = assembly.GetExportedTypes().FirstOrDefault(a => !a.IsNested); + _logger.GeneratedCodeToAssemblyCompilationEnd(fileInfo.RelativePath, startTimestamp); return new CompilationResult(type); @@ -153,52 +105,6 @@ public CompilationResult Compile(RelativeFileInfo fileInfo, string compilationCo } } - /// - /// Gets the sequence of instances used for compilation. - /// - /// The instances. - protected virtual IList GetCompilationReferences() - { - var feature = new MetadataReferenceFeature(); - _partManager.PopulateFeature(feature); - var applicationReferences = feature.MetadataReferences; - - if (_additionalMetadataReferences.Count == 0) - { - return applicationReferences; - } - - var compilationReferences = new List(applicationReferences.Count + _additionalMetadataReferences.Count); - compilationReferences.AddRange(applicationReferences); - compilationReferences.AddRange(_additionalMetadataReferences); - - return compilationReferences; - } - - private Assembly LoadStream(MemoryStream assemblyStream, MemoryStream pdbStream) - { -#if NET451 - return Assembly.Load(assemblyStream.ToArray(), pdbStream.ToArray()); -#else - return System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromStream(assemblyStream, pdbStream); -#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, @@ -265,6 +171,17 @@ private static bool IsError(Diagnostic diagnostic) return diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error; } + public static Assembly LoadAssembly(MemoryStream assemblyStream, MemoryStream pdbStream) + { + var assembly = +#if NET451 + Assembly.Load(assemblyStream.ToArray(), pdbStream.ToArray()); +#else + System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromStream(assemblyStream, pdbStream); +#endif + return assembly; + } + private static string ReadFileContentsSafely(IFileProvider fileProvider, string filePath) { var fileInfo = fileProvider.GetFileInfo(filePath); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorReferenceManager.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorReferenceManager.cs new file mode 100644 index 0000000000..2e21de7705 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorReferenceManager.cs @@ -0,0 +1,59 @@ +// 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.Collections.Generic; +using System.Threading; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + public class RazorReferenceManager + { + private readonly ApplicationPartManager _partManager; + private readonly IList _additionalMetadataReferences; + private object _compilationReferencesLock = new object(); + private bool _compilationReferencesInitialized; + private IList _compilationReferences; + + public RazorReferenceManager( + ApplicationPartManager partManager, + IOptions optionsAccessor) + { + _partManager = partManager; + _additionalMetadataReferences = optionsAccessor.Value.AdditionalCompilationReferences; + } + + public IList CompilationReferences + { + get + { + return LazyInitializer.EnsureInitialized( + ref _compilationReferences, + ref _compilationReferencesInitialized, + ref _compilationReferencesLock, + GetCompilationReferences); + } + } + + private IList GetCompilationReferences() + { + var feature = new MetadataReferenceFeature(); + _partManager.PopulateFeature(feature); + var applicationReferences = feature.MetadataReferences; + + if (_additionalMetadataReferences.Count == 0) + { + return applicationReferences; + } + + var compilationReferences = new List(applicationReferences.Count + _additionalMetadataReferences.Count); + compilationReferences.AddRange(applicationReferences); + compilationReferences.AddRange(_additionalMetadataReferences); + + return compilationReferences; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs index b880459544..fe691ccbf2 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs @@ -2,8 +2,6 @@ // 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.Linq; using System.Reflection; using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Razor.Compilation; @@ -297,34 +295,6 @@ public void Compile_SucceedsIfReferencesAreAddedInCallback() Assert.NotNull(result.CompiledType); } - [Fact] - public void GetCompilationReferences_CombinesApplicationPartAndOptionMetadataReferences() - { - // Arrange - var options = new RazorViewEngineOptions(); - var objectAssemblyLocation = typeof(object).GetTypeInfo().Assembly.Location; - var objectAssemblyMetadataReference = MetadataReference.CreateFromFile(objectAssemblyLocation); - options.AdditionalCompilationReferences.Add(objectAssemblyMetadataReference); - var applicationPartManager = GetApplicationPartManager(); - var compilationService = new TestRoslynCompilationService(applicationPartManager, options); - var feature = new MetadataReferenceFeature(); - applicationPartManager.PopulateFeature(feature); - var partReferences = feature.MetadataReferences; - var expectedReferences = new List(); - expectedReferences.AddRange(partReferences); - expectedReferences.Add(objectAssemblyMetadataReference); - var expectedReferenceDisplays = expectedReferences.Select(reference => reference.Display); - - // Act - var references = compilationService.GetCompilationReferencesPublic(); - var referenceDisplays = references.Select(reference => reference.Display); - - // Assert - Assert.NotNull(references); - Assert.NotEmpty(references); - Assert.Equal(expectedReferenceDisplays, referenceDisplays); - } - private static DiagnosticDescriptor GetDiagnosticDescriptor(string messageFormat) { return new DiagnosticDescriptor( @@ -377,22 +347,14 @@ private static DefaultRoslynCompilationService GetRoslynCompilationService( { partManager = partManager ?? GetApplicationPartManager(); options = options ?? GetOptions(); + var optionsAccessor = GetAccessor(options); + var referenceManager = new RazorReferenceManager(partManager, optionsAccessor); + var compiler = new CSharpCompiler(referenceManager, optionsAccessor); return new DefaultRoslynCompilationService( - partManager, - GetAccessor(options), + compiler, GetFileProviderAccessor(fileProvider), NullLoggerFactory.Instance); } - - private class TestRoslynCompilationService : DefaultRoslynCompilationService - { - public TestRoslynCompilationService(ApplicationPartManager partManager, RazorViewEngineOptions options) - : base(partManager, GetAccessor(options), GetFileProviderAccessor(), NullLoggerFactory.Instance) - { - } - - public IList GetCompilationReferencesPublic() => GetCompilationReferences(); - } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/ReferenceManagerTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/ReferenceManagerTest.cs new file mode 100644 index 0000000000..12fe6d81d6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/ReferenceManagerTest.cs @@ -0,0 +1,61 @@ +// 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.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal +{ + public class ReferenceManagerTest + { + [Fact] + public void GetCompilationReferences_CombinesApplicationPartAndOptionMetadataReferences() + { + // Arrange + var options = new RazorViewEngineOptions(); + var objectAssemblyLocation = typeof(object).GetTypeInfo().Assembly.Location; + var objectAssemblyMetadataReference = MetadataReference.CreateFromFile(objectAssemblyLocation); + options.AdditionalCompilationReferences.Add(objectAssemblyMetadataReference); + + var applicationPartManager = GetApplicationPartManager(); + var feature = new MetadataReferenceFeature(); + applicationPartManager.PopulateFeature(feature); + var partReferences = feature.MetadataReferences; + var expectedReferenceDisplays = partReferences + .Concat(new[] { objectAssemblyMetadataReference }) + .Select(r => r.Display); + var referenceManager = new RazorReferenceManager(applicationPartManager, GetAccessor(options)); + + // Act + var references = referenceManager.CompilationReferences; + var referenceDisplays = references.Select(reference => reference.Display); + + // Assert + Assert.Equal(expectedReferenceDisplays, referenceDisplays); + } + + private static ApplicationPartManager GetApplicationPartManager() + { + var applicationPartManager = new ApplicationPartManager(); + var assembly = typeof(DefaultRoslynCompilationServiceTest).GetTypeInfo().Assembly; + applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly)); + applicationPartManager.FeatureProviders.Add(new MetadataReferenceFeatureProvider()); + + return applicationPartManager; + } + + private static IOptions GetAccessor(RazorViewEngineOptions options) + { + var optionsAccessor = new Mock>(); + optionsAccessor.SetupGet(a => a.Value).Returns(options); + return optionsAccessor.Object; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs index bf8cf2e720..d1756d6a72 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs @@ -209,7 +209,8 @@ public void AddMvcTwice_DoesNotAddApplicationFeatureProvidersTwice() feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature), - feature => Assert.IsType(feature)); + feature => Assert.IsType(feature), + feature => Assert.IsType(feature)); } [Fact]