From 0f072a9565037a9efa2559ad49d229d3b10c22c6 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 29 Jan 2019 09:34:43 -0800 Subject: [PATCH] Reintroduce a package for Razor runtime compilation (#6653) * Reintroduce a package for Razor runtime compilation Fixes https://github.com/aspnet/AspNetCore/issues/4947 --- eng/Dependencies.props | 2 + eng/ProjectReferences.props | 1 + src/Mvc/Mvc.sln | 49 +- src/Mvc/samples/MvcSandbox/MvcSandbox.csproj | 1 + src/Mvc/samples/MvcSandbox/Startup.cs | 4 +- .../CSharpCompiler.cs | 226 +++++ .../ChecksumValidator.cs | 122 +++ .../CompilationFailedException.cs | 35 + .../CompilationFailedExceptionFactory.cs | 157 +++ .../MvcRazorRuntimeCompilationOptionsSetup.cs | 29 + ...rRuntimeCompilationMvcBuilderExtensions.cs | 50 + ...timeCompilationMvcCoreBuilderExtensions.cs | 113 +++ .../FileProviderRazorProjectFileSystem.cs | 93 ++ .../FileProviderRazorProjectItem.cs | 65 ++ .../LazyMetadataReferenceFeature.cs | 28 + ...etCore.Mvc.Razor.RuntimeCompilation.csproj | 23 + .../MvcRazorRuntimeCompilationOptions.cs | 31 + .../PageActionDescriptorChangeProvider.cs | 98 ++ .../PageDirectiveFeature.cs | 110 +++ .../Properties/AssemblyInfo.cs | 7 + .../Properties/Resources.Designer.cs | 100 ++ .../RazorProjectPageRouteModelProvider.cs | 107 +++ .../RazorReferenceManager.cs | 79 ++ ...RazorRuntimeCompilationLoggerExtensions.cs | 179 ++++ .../Resources.resx | 135 +++ .../RuntimeCompilationFileProvider.cs | 57 ++ .../RuntimeViewCompiler.cs | 434 +++++++++ .../RuntimeViewCompilerProvider.cs | 64 ++ ...ViewCompiler.cs => DefaultViewCompiler.cs} | 4 +- ...ider.cs => DefaultViewCompilerProvider.cs} | 8 +- .../MvcRazorMvcCoreBuilderExtensions.cs | 2 +- .../Properties/AssemblyInfo.cs | 1 + .../ApplicationModels/PageRouteMetadata.cs | 18 +- .../PageRouteModelFactory.cs | 17 +- .../PageActionDescriptorProvider.cs | 1 - .../PageLoggerExtensions.cs | 20 - .../RazorBuildTest.cs | 90 +- .../ViewEngineTests.cs | 24 - .../CSharpCompilerTest.cs | 319 ++++++ .../ChecksumValidatorTest.cs | 190 ++++ .../CompilerFailedExceptionFactoryTest.cs | 355 +++++++ ...CompilationMvcCoreBuilderExtensionsTest.cs | 27 + .../FileProviderRazorProjectFileSystemTest.cs | 260 +++++ ...e.Mvc.Razor.RuntimeCompilation.Test.csproj | 13 + .../RazorReferenceManagerTest.cs | 52 + .../RuntimeCompilationFileProviderTest.cs | 31 + .../RuntimeViewCompilerTest.cs | 908 ++++++++++++++++++ .../TestInfrastructure/DirectoryNode.cs | 168 ++++ .../TestInfrastructure/FileNode.cs | 24 + .../TestInfrastructure/NotFoundProjectItem.cs | 27 + .../TestRazorProjectItem.cs | 44 + .../TestRazorReferenceManager.cs | 24 + .../VirtualRazorProjectFileSystem.cs | 39 + ...ilerTest.cs => DefaultViewCompilerTest.cs} | 4 +- .../Controllers/UpdateableViewsController.cs} | 6 +- .../RazorBuildWebSite.csproj | 3 +- .../WebSites/RazorBuildWebSite/Startup.cs | 4 + .../UpdateableFileProvider.cs | 10 +- .../test/WebSites/RazorBuildWebSite/readme.md | 2 +- .../Controllers/EmbeddedViewsController.cs | 18 - .../Views/EmbeddedShared/_Layout.cshtml | 1 - .../Views/EmbeddedShared/_Partial.cshtml | 1 - .../EmbeddedViews/EmbeddedPartial.cshtml | 1 - .../Views/EmbeddedViews/Index.cshtml | 3 - .../EmbeddedViews/RelativeNonPath.cshtml | 2 - .../Views/EmbeddedViews/_ViewImports.cshtml | 1 - .../Views/EmbeddedViews/_ViewStart.cshtml | 1 - .../Views/Shared/_EmbeddedPartial.cshtml | 1 - .../WebSites/RazorWebSite/RazorWebSite.csproj | 6 +- src/Mvc/test/WebSites/RazorWebSite/Startup.cs | 4 - 70 files changed, 5018 insertions(+), 115 deletions(-) create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/CSharpCompiler.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/ChecksumValidator.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/CompilationFailedException.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/CompilationFailedExceptionFactory.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/DependencyInjection/MvcRazorRuntimeCompilationOptionsSetup.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/DependencyInjection/RazorRuntimeCompilationMvcBuilderExtensions.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/DependencyInjection/RazorRuntimeCompilationMvcCoreBuilderExtensions.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/FileProviderRazorProjectFileSystem.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/FileProviderRazorProjectItem.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/LazyMetadataReferenceFeature.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/MvcRazorRuntimeCompilationOptions.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/PageActionDescriptorChangeProvider.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/PageDirectiveFeature.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Properties/AssemblyInfo.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Properties/Resources.Designer.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RazorProjectPageRouteModelProvider.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RazorReferenceManager.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RazorRuntimeCompilationLoggerExtensions.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Resources.resx create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RuntimeCompilationFileProvider.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RuntimeViewCompiler.cs create mode 100644 src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RuntimeViewCompilerProvider.cs rename src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/{RazorViewCompiler.cs => DefaultViewCompiler.cs} (97%) rename src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/{RazorViewCompilerProvider.cs => DefaultViewCompilerProvider.cs} (67%) create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/CSharpCompilerTest.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/ChecksumValidatorTest.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/CompilerFailedExceptionFactoryTest.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/DependencyInjection/RazorRuntimeCompilationMvcCoreBuilderExtensionsTest.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/FileProviderRazorProjectFileSystemTest.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test.csproj create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/RazorReferenceManagerTest.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/RuntimeCompilationFileProviderTest.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/RuntimeViewCompilerTest.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/DirectoryNode.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/FileNode.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/NotFoundProjectItem.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/TestRazorProjectItem.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/TestRazorReferenceManager.cs create mode 100644 src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/VirtualRazorProjectFileSystem.cs rename src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/{RazorViewCompilerTest.cs => DefaultViewCompilerTest.cs} (96%) rename src/Mvc/test/WebSites/{RazorWebSite/Controllers/UpdateableFileProviderController.cs => RazorBuildWebSite/Controllers/UpdateableViewsController.cs} (79%) rename src/Mvc/test/WebSites/{RazorWebSite/Services => RazorBuildWebSite}/UpdateableFileProvider.cs (92%) delete mode 100644 src/Mvc/test/WebSites/RazorWebSite/Controllers/EmbeddedViewsController.cs delete mode 100644 src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedShared/_Layout.cshtml delete mode 100644 src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedShared/_Partial.cshtml delete mode 100644 src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/EmbeddedPartial.cshtml delete mode 100644 src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/Index.cshtml delete mode 100644 src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/RelativeNonPath.cshtml delete mode 100644 src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/_ViewImports.cshtml delete mode 100644 src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/_ViewStart.cshtml delete mode 100644 src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/Shared/_EmbeddedPartial.cshtml diff --git a/eng/Dependencies.props b/eng/Dependencies.props index 4c93386079..91c0e3c509 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -35,6 +35,8 @@ and are generated based on the last package release. + + diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 3638e9f988..262dbf3340 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -98,6 +98,7 @@ + diff --git a/src/Mvc/Mvc.sln b/src/Mvc/Mvc.sln index 82dd2a8cad..cb448a4ef4 100644 --- a/src/Mvc/Mvc.sln +++ b/src/Mvc/Mvc.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.136 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28509.92 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -274,6 +274,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataPr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenericHostWebSite", "test\WebSites\GenericHostWebSite\GenericHostWebSite.csproj", "{D9BE3E50-5CE8-4D8D-BA19-AA219D009752}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation", "src\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj", "{F2D4A859-7B84-403E-9745-01032EC705C5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test", "test\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test.csproj", "{23A6033D-2AA6-4629-BC1B-14694E3794FF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components", "..\Components\Components\src\Microsoft.AspNetCore.Components.csproj", "{69E18B21-E4B9-4866-ABDA-3C2D9664D24C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1592,6 +1598,42 @@ Global {D9BE3E50-5CE8-4D8D-BA19-AA219D009752}.Release|Mixed Platforms.Build.0 = Release|Any CPU {D9BE3E50-5CE8-4D8D-BA19-AA219D009752}.Release|x86.ActiveCfg = Release|Any CPU {D9BE3E50-5CE8-4D8D-BA19-AA219D009752}.Release|x86.Build.0 = Release|Any CPU + {F2D4A859-7B84-403E-9745-01032EC705C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2D4A859-7B84-403E-9745-01032EC705C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2D4A859-7B84-403E-9745-01032EC705C5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {F2D4A859-7B84-403E-9745-01032EC705C5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {F2D4A859-7B84-403E-9745-01032EC705C5}.Debug|x86.ActiveCfg = Debug|Any CPU + {F2D4A859-7B84-403E-9745-01032EC705C5}.Debug|x86.Build.0 = Debug|Any CPU + {F2D4A859-7B84-403E-9745-01032EC705C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2D4A859-7B84-403E-9745-01032EC705C5}.Release|Any CPU.Build.0 = Release|Any CPU + {F2D4A859-7B84-403E-9745-01032EC705C5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {F2D4A859-7B84-403E-9745-01032EC705C5}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {F2D4A859-7B84-403E-9745-01032EC705C5}.Release|x86.ActiveCfg = Release|Any CPU + {F2D4A859-7B84-403E-9745-01032EC705C5}.Release|x86.Build.0 = Release|Any CPU + {23A6033D-2AA6-4629-BC1B-14694E3794FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23A6033D-2AA6-4629-BC1B-14694E3794FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23A6033D-2AA6-4629-BC1B-14694E3794FF}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {23A6033D-2AA6-4629-BC1B-14694E3794FF}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {23A6033D-2AA6-4629-BC1B-14694E3794FF}.Debug|x86.ActiveCfg = Debug|Any CPU + {23A6033D-2AA6-4629-BC1B-14694E3794FF}.Debug|x86.Build.0 = Debug|Any CPU + {23A6033D-2AA6-4629-BC1B-14694E3794FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23A6033D-2AA6-4629-BC1B-14694E3794FF}.Release|Any CPU.Build.0 = Release|Any CPU + {23A6033D-2AA6-4629-BC1B-14694E3794FF}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {23A6033D-2AA6-4629-BC1B-14694E3794FF}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {23A6033D-2AA6-4629-BC1B-14694E3794FF}.Release|x86.ActiveCfg = Release|Any CPU + {23A6033D-2AA6-4629-BC1B-14694E3794FF}.Release|x86.Build.0 = Release|Any CPU + {69E18B21-E4B9-4866-ABDA-3C2D9664D24C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69E18B21-E4B9-4866-ABDA-3C2D9664D24C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69E18B21-E4B9-4866-ABDA-3C2D9664D24C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {69E18B21-E4B9-4866-ABDA-3C2D9664D24C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {69E18B21-E4B9-4866-ABDA-3C2D9664D24C}.Debug|x86.ActiveCfg = Debug|Any CPU + {69E18B21-E4B9-4866-ABDA-3C2D9664D24C}.Debug|x86.Build.0 = Debug|Any CPU + {69E18B21-E4B9-4866-ABDA-3C2D9664D24C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69E18B21-E4B9-4866-ABDA-3C2D9664D24C}.Release|Any CPU.Build.0 = Release|Any CPU + {69E18B21-E4B9-4866-ABDA-3C2D9664D24C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {69E18B21-E4B9-4866-ABDA-3C2D9664D24C}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {69E18B21-E4B9-4866-ABDA-3C2D9664D24C}.Release|x86.ActiveCfg = Release|Any CPU + {69E18B21-E4B9-4866-ABDA-3C2D9664D24C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1710,6 +1752,9 @@ Global {848E2620-EAF9-4BFD-8810-4AF71E09A8FB} = {9328599D-A7AF-43BC-BE08-7503DF9B8CE6} {C75C6E51-4FFD-4902-8739-9109E51875B4} = {9328599D-A7AF-43BC-BE08-7503DF9B8CE6} {D9BE3E50-5CE8-4D8D-BA19-AA219D009752} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {F2D4A859-7B84-403E-9745-01032EC705C5} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} + {23A6033D-2AA6-4629-BC1B-14694E3794FF} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {69E18B21-E4B9-4866-ABDA-3C2D9664D24C} = {9328599D-A7AF-43BC-BE08-7503DF9B8CE6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {63D344F6-F86D-40E6-85B9-0AABBE338C4A} diff --git a/src/Mvc/samples/MvcSandbox/MvcSandbox.csproj b/src/Mvc/samples/MvcSandbox/MvcSandbox.csproj index a2ce82290f..9d3ee6d844 100644 --- a/src/Mvc/samples/MvcSandbox/MvcSandbox.csproj +++ b/src/Mvc/samples/MvcSandbox/MvcSandbox.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Mvc/samples/MvcSandbox/Startup.cs b/src/Mvc/samples/MvcSandbox/Startup.cs index c958e06b8e..c7d8120ead 100644 --- a/src/Mvc/samples/MvcSandbox/Startup.cs +++ b/src/Mvc/samples/MvcSandbox/Startup.cs @@ -26,7 +26,9 @@ namespace MvcSandbox { options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); }); - services.AddMvc().SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Latest); + services.AddMvc() + .AddRazorRuntimeCompilation() + .SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Latest); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/CSharpCompiler.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/CSharpCompiler.cs new file mode 100644 index 0000000000..4aeb006421 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/CSharpCompiler.cs @@ -0,0 +1,226 @@ +// 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.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.DependencyModel; +using DependencyContextCompilationOptions = Microsoft.Extensions.DependencyModel.CompilationOptions; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + internal class CSharpCompiler + { + private readonly RazorReferenceManager _referenceManager; + private readonly IHostingEnvironment _hostingEnvironment; + private bool _optionsInitialized; + private CSharpParseOptions _parseOptions; + private CSharpCompilationOptions _compilationOptions; + private EmitOptions _emitOptions; + private bool _emitPdb; + + public CSharpCompiler(RazorReferenceManager manager, IHostingEnvironment hostingEnvironment) + { + _referenceManager = manager ?? throw new ArgumentNullException(nameof(manager)); + _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + } + + public virtual CSharpParseOptions ParseOptions + { + get + { + EnsureOptions(); + return _parseOptions; + } + } + + public virtual CSharpCompilationOptions CSharpCompilationOptions + { + get + { + EnsureOptions(); + return _compilationOptions; + } + } + + public virtual bool EmitPdb + { + get + { + EnsureOptions(); + return _emitPdb; + } + } + + public virtual EmitOptions EmitOptions + { + get + { + EnsureOptions(); + return _emitOptions; + } + } + + public SyntaxTree CreateSyntaxTree(SourceText sourceText) + { + return CSharpSyntaxTree.ParseText( + sourceText, + options: ParseOptions); + } + + public CSharpCompilation CreateCompilation(string assemblyName) + { + return CSharpCompilation.Create( + assemblyName, + options: CSharpCompilationOptions, + references: _referenceManager.CompilationReferences); + } + + // Internal for unit testing. + protected internal virtual DependencyContextCompilationOptions GetDependencyContextCompilationOptions() + { + if (!string.IsNullOrEmpty(_hostingEnvironment.ApplicationName)) + { + var applicationAssembly = Assembly.Load(new AssemblyName(_hostingEnvironment.ApplicationName)); + var dependencyContext = DependencyContext.Load(applicationAssembly); + if (dependencyContext?.CompilationOptions != null) + { + return dependencyContext.CompilationOptions; + } + } + + return DependencyContextCompilationOptions.Default; + } + + private void EnsureOptions() + { + if (!_optionsInitialized) + { + var dependencyContextOptions = GetDependencyContextCompilationOptions(); + _parseOptions = GetParseOptions(_hostingEnvironment, dependencyContextOptions); + _compilationOptions = GetCompilationOptions(_hostingEnvironment, dependencyContextOptions); + _emitOptions = GetEmitOptions(dependencyContextOptions); + + _optionsInitialized = true; + } + } + + private EmitOptions GetEmitOptions(DependencyContextCompilationOptions dependencyContextOptions) + { + // Assume we're always producing pdbs unless DebugType = none + _emitPdb = true; + DebugInformationFormat debugInformationFormat; + if (string.IsNullOrEmpty(dependencyContextOptions.DebugType)) + { + debugInformationFormat = DebugInformationFormat.PortablePdb; + } + else + { + // Based on https://github.com/dotnet/roslyn/blob/1d28ff9ba248b332de3c84d23194a1d7bde07e4d/src/Compilers/CSharp/Portable/CommandLine/CSharpCommandLineParser.cs#L624-L640 + switch (dependencyContextOptions.DebugType.ToLower()) + { + case "none": + // There isn't a way to represent none in DebugInformationFormat. + // We'll set EmitPdb to false and let callers handle it by setting a null pdb-stream. + _emitPdb = false; + return new EmitOptions(); + case "portable": + debugInformationFormat = DebugInformationFormat.PortablePdb; + break; + case "embedded": + // Roslyn does not expose enough public APIs to produce a binary with embedded pdbs. + // We'll produce PortablePdb instead to continue providing a reasonable user experience. + debugInformationFormat = DebugInformationFormat.PortablePdb; + break; + case "full": + case "pdbonly": + debugInformationFormat = DebugInformationFormat.PortablePdb; + break; + default: + throw new InvalidOperationException(Resources.FormatUnsupportedDebugInformationFormat(dependencyContextOptions.DebugType)); + } + } + + var emitOptions = new EmitOptions(debugInformationFormat: debugInformationFormat); + return emitOptions; + } + + private static CSharpCompilationOptions GetCompilationOptions( + IHostingEnvironment hostingEnvironment, + DependencyContextCompilationOptions dependencyContextOptions) + { + var csharpCompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary); + + // Disable 1702 until roslyn turns this off by default + csharpCompilationOptions = csharpCompilationOptions.WithSpecificDiagnosticOptions( + new Dictionary + { + {"CS1701", ReportDiagnostic.Suppress}, // Binding redirects + {"CS1702", ReportDiagnostic.Suppress}, + {"CS1705", ReportDiagnostic.Suppress} + }); + + if (dependencyContextOptions.AllowUnsafe.HasValue) + { + csharpCompilationOptions = csharpCompilationOptions.WithAllowUnsafe( + dependencyContextOptions.AllowUnsafe.Value); + } + + OptimizationLevel optimizationLevel; + if (dependencyContextOptions.Optimize.HasValue) + { + optimizationLevel = dependencyContextOptions.Optimize.Value ? + OptimizationLevel.Release : + OptimizationLevel.Debug; + } + else + { + optimizationLevel = hostingEnvironment.IsDevelopment() ? + OptimizationLevel.Debug : + OptimizationLevel.Release; + } + csharpCompilationOptions = csharpCompilationOptions.WithOptimizationLevel(optimizationLevel); + + if (dependencyContextOptions.WarningsAsErrors.HasValue) + { + var reportDiagnostic = dependencyContextOptions.WarningsAsErrors.Value ? + ReportDiagnostic.Error : + ReportDiagnostic.Default; + csharpCompilationOptions = csharpCompilationOptions.WithGeneralDiagnosticOption(reportDiagnostic); + } + + return csharpCompilationOptions; + } + + private static CSharpParseOptions GetParseOptions( + IHostingEnvironment hostingEnvironment, + DependencyContextCompilationOptions dependencyContextOptions) + { + var configurationSymbol = hostingEnvironment.IsDevelopment() ? "DEBUG" : "RELEASE"; + var defines = dependencyContextOptions.Defines.Concat(new[] { configurationSymbol }); + + var parseOptions = new CSharpParseOptions(preprocessorSymbols: defines); + + if (!string.IsNullOrEmpty(dependencyContextOptions.LanguageVersion)) + { + if (LanguageVersionFacts.TryParse(dependencyContextOptions.LanguageVersion, out var languageVersion)) + { + parseOptions = parseOptions.WithLanguageVersion(languageVersion); + } + else + { + Debug.Fail($"LanguageVersion {languageVersion} specified in the deps file could not be parsed."); + } + } + + return parseOptions; + } + } +} \ No newline at end of file diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/ChecksumValidator.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/ChecksumValidator.cs new file mode 100644 index 0000000000..220d803151 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/ChecksumValidator.cs @@ -0,0 +1,122 @@ +// 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 Microsoft.AspNetCore.Razor.Hosting; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + internal static class ChecksumValidator + { + public static bool IsRecompilationSupported(RazorCompiledItem item) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + // A Razor item only supports recompilation if its primary source file has a checksum. + // + // Other files (view imports) may or may not have existed at the time of compilation, + // so we may not have checksums for them. + var checksums = item.GetChecksumMetadata(); + return checksums.Any(c => string.Equals(item.Identifier, c.Identifier, StringComparison.OrdinalIgnoreCase)); + } + + // Validates that we can use an existing precompiled view by comparing checksums with files on + // disk. + public static bool IsItemValid(RazorProjectFileSystem fileSystem, RazorCompiledItem item) + { + if (fileSystem == null) + { + throw new ArgumentNullException(nameof(fileSystem)); + } + + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + var checksums = item.GetChecksumMetadata(); + + // The checksum that matches 'Item.Identity' in this list is significant. That represents the main file. + // + // We don't really care about the validation unless the main file exists. This is because we expect + // most sites to have some _ViewImports in common location. That means that in the case you're + // using views from a 3rd party library, you'll always have **some** conflicts. + // + // The presence of the main file with the same content is a very strong signal that you're in a + // development scenario. + var primaryChecksum = checksums + .FirstOrDefault(c => string.Equals(item.Identifier, c.Identifier, StringComparison.OrdinalIgnoreCase)); + if (primaryChecksum == null) + { + // No primary checksum, assume valid. + return true; + } + + var projectItem = fileSystem.GetItem(primaryChecksum.Identifier); + if (!projectItem.Exists) + { + // Main file doesn't exist - assume valid. + return true; + } + + var sourceDocument = RazorSourceDocument.ReadFrom(projectItem); + if (!string.Equals(sourceDocument.GetChecksumAlgorithm(), primaryChecksum.ChecksumAlgorithm) || + !ChecksumsEqual(primaryChecksum.Checksum, sourceDocument.GetChecksum())) + { + // Main file exists, but checksums not equal. + return false; + } + + for (var i = 0; i < checksums.Count; i++) + { + var checksum = checksums[i]; + if (string.Equals(item.Identifier, checksum.Identifier, StringComparison.OrdinalIgnoreCase)) + { + // Ignore primary checksum on this pass. + continue; + } + + var importItem = fileSystem.GetItem(checksum.Identifier); + if (!importItem.Exists) + { + // Import file doesn't exist - assume invalid. + return false; + } + + sourceDocument = RazorSourceDocument.ReadFrom(importItem); + if (!string.Equals(sourceDocument.GetChecksumAlgorithm(), checksum.ChecksumAlgorithm) || + !ChecksumsEqual(checksum.Checksum, sourceDocument.GetChecksum())) + { + // Import file exists, but checksums not equal. + return false; + } + } + + return true; + } + + private static bool ChecksumsEqual(string checksum, byte[] bytes) + { + if (bytes.Length * 2 != checksum.Length) + { + return false; + } + + for (var i = 0; i < bytes.Length; i++) + { + var text = bytes[i].ToString("x2"); + if (checksum[i * 2] != text[0] || checksum[i * 2 + 1] != text[1]) + { + return false; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/CompilationFailedException.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/CompilationFailedException.cs new file mode 100644 index 0000000000..bb5dac8d2f --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/CompilationFailedException.cs @@ -0,0 +1,35 @@ +// 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.Linq; +using Microsoft.AspNetCore.Diagnostics; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + internal class CompilationFailedException : Exception, ICompilationException + { + public CompilationFailedException( + IEnumerable compilationFailures) + : base(FormatMessage(compilationFailures)) + { + if (compilationFailures == null) + { + throw new ArgumentNullException(nameof(compilationFailures)); + } + + CompilationFailures = compilationFailures; + } + + public IEnumerable CompilationFailures { get; } + + private static string FormatMessage(IEnumerable compilationFailures) + { + return Resources.CompilationFailed + Environment.NewLine + + string.Join( + Environment.NewLine, + compilationFailures.SelectMany(f => f.Messages).Select(message => message.FormattedMessage)); + } + } +} \ No newline at end of file diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/CompilationFailedExceptionFactory.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/CompilationFailedExceptionFactory.cs new file mode 100644 index 0000000000..86cfc0a223 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/CompilationFailedExceptionFactory.cs @@ -0,0 +1,157 @@ +// 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.Linq; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + internal static class CompilationFailedExceptionFactory + { + // error CS0234: The type or namespace name 'C' does not exist in the namespace 'N' (are you missing + // an assembly reference?) + private const string CS0234 = nameof(CS0234); + // 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); + + public static CompilationFailedException Create( + RazorCodeDocument codeDocument, + IEnumerable diagnostics) + { + // If a SourceLocation does not specify a file path, assume it is produced from parsing the current file. + var messageGroups = diagnostics.GroupBy( + razorError => razorError.Span.FilePath ?? codeDocument.Source.FilePath, + StringComparer.Ordinal); + + var failures = new List(); + foreach (var group in messageGroups) + { + var filePath = group.Key; + var fileContent = ReadContent(codeDocument, filePath); + var compilationFailure = new CompilationFailure( + filePath, + fileContent, + compiledContent: string.Empty, + messages: group.Select(parserError => CreateDiagnosticMessage(parserError, filePath))); + failures.Add(compilationFailure); + } + + return new CompilationFailedException(failures); + } + + public static CompilationFailedException Create( + RazorCodeDocument codeDocument, + string compilationContent, + string assemblyName, + IEnumerable diagnostics) + { + var diagnosticGroups = diagnostics + .Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error) + .GroupBy(diagnostic => GetFilePath(codeDocument, diagnostic), StringComparer.Ordinal); + + var failures = new List(); + foreach (var group in diagnosticGroups) + { + var sourceFilePath = group.Key; + string sourceFileContent; + if (string.Equals(assemblyName, sourceFilePath, StringComparison.Ordinal)) + { + // The error is in the generated code and does not have a mapping line pragma + sourceFileContent = compilationContent; + sourceFilePath = Resources.GeneratedCodeFileName; + } + else + { + sourceFileContent = ReadContent(codeDocument, sourceFilePath); + } + + string additionalMessage = null; + if (group.Any(g => + string.Equals(CS0234, g.Id, StringComparison.OrdinalIgnoreCase) || + string.Equals(CS0246, g.Id, StringComparison.OrdinalIgnoreCase))) + { + additionalMessage = Resources.FormatCompilation_MissingReferences( + "CopyRefAssembliesToPublishDirectory"); + } + + var compilationFailure = new CompilationFailure( + sourceFilePath, + sourceFileContent, + compilationContent, + group.Select(GetDiagnosticMessage), + additionalMessage); + + failures.Add(compilationFailure); + } + + return new CompilationFailedException(failures); + } + + private static string ReadContent(RazorCodeDocument codeDocument, string filePath) + { + RazorSourceDocument sourceDocument; + if (string.IsNullOrEmpty(filePath) || string.Equals(codeDocument.Source.FilePath, filePath, StringComparison.Ordinal)) + { + sourceDocument = codeDocument.Source; + } + else + { + sourceDocument = codeDocument.Imports.FirstOrDefault(f => string.Equals(f.FilePath, filePath, StringComparison.Ordinal)); + } + + if (sourceDocument != null) + { + var contentChars = new char[sourceDocument.Length]; + sourceDocument.CopyTo(0, contentChars, 0, sourceDocument.Length); + return new string(contentChars); + } + + return string.Empty; + } + + private static DiagnosticMessage GetDiagnosticMessage(Diagnostic diagnostic) + { + var mappedLineSpan = diagnostic.Location.GetMappedLineSpan(); + return new DiagnosticMessage( + diagnostic.GetMessage(), + CSharpDiagnosticFormatter.Instance.Format(diagnostic), + mappedLineSpan.Path, + mappedLineSpan.StartLinePosition.Line + 1, + mappedLineSpan.StartLinePosition.Character + 1, + mappedLineSpan.EndLinePosition.Line + 1, + mappedLineSpan.EndLinePosition.Character + 1); + } + + private static DiagnosticMessage CreateDiagnosticMessage( + RazorDiagnostic razorDiagnostic, + string filePath) + { + var sourceSpan = razorDiagnostic.Span; + var message = razorDiagnostic.GetMessage(); + return new DiagnosticMessage( + message: message, + formattedMessage: razorDiagnostic.ToString(), + filePath: filePath, + startLine: sourceSpan.LineIndex + 1, + startColumn: sourceSpan.CharacterIndex, + endLine: sourceSpan.LineIndex + 1, + endColumn: sourceSpan.CharacterIndex + sourceSpan.Length); + } + + private static string GetFilePath(RazorCodeDocument codeDocument, Diagnostic diagnostic) + { + if (diagnostic.Location == Location.None) + { + return codeDocument.Source.FilePath; + } + + return diagnostic.Location.GetMappedLineSpan().Path; + } + } +} \ No newline at end of file diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/DependencyInjection/MvcRazorRuntimeCompilationOptionsSetup.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/DependencyInjection/MvcRazorRuntimeCompilationOptionsSetup.cs new file mode 100644 index 0000000000..e18689ccb3 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/DependencyInjection/MvcRazorRuntimeCompilationOptionsSetup.cs @@ -0,0 +1,29 @@ +// 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 Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + internal class MvcRazorRuntimeCompilationOptionsSetup : IConfigureOptions + { + private readonly IHostingEnvironment _hostingEnvironment; + + public MvcRazorRuntimeCompilationOptionsSetup(IHostingEnvironment hostingEnvironment) + { + _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + } + + public void Configure(MvcRazorRuntimeCompilationOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.FileProviders.Add(_hostingEnvironment.ContentRootFileProvider); + } + } +} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/DependencyInjection/RazorRuntimeCompilationMvcBuilderExtensions.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/DependencyInjection/RazorRuntimeCompilationMvcBuilderExtensions.cs new file mode 100644 index 0000000000..a673954e3d --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/DependencyInjection/RazorRuntimeCompilationMvcBuilderExtensions.cs @@ -0,0 +1,50 @@ +// 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 Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class RazorRuntimeCompilationMvcBuilderExtensions + { + /// + /// Configures to support runtime compilation of Razor views and Razor Pages. + /// + /// The . + /// The . + public static IMvcBuilder AddRazorRuntimeCompilation(this IMvcBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + RazorRuntimeCompilationMvcCoreBuilderExtensions.AddServices(builder.Services); + return builder; + } + + /// + /// Configures to support runtime compilation of Razor views and Razor Pages. + /// + /// The . + /// An action to configure the . + /// The . + public static IMvcBuilder AddRazorRuntimeCompilation(this IMvcBuilder builder, Action setupAction) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (setupAction == null) + { + throw new ArgumentNullException(nameof(setupAction)); + } + + RazorRuntimeCompilationMvcCoreBuilderExtensions.AddServices(builder.Services); + builder.Services.Configure(setupAction); + return builder; + } + } +} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/DependencyInjection/RazorRuntimeCompilationMvcCoreBuilderExtensions.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/DependencyInjection/RazorRuntimeCompilationMvcCoreBuilderExtensions.cs new file mode 100644 index 0000000000..bd42adb829 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/DependencyInjection/RazorRuntimeCompilationMvcCoreBuilderExtensions.cs @@ -0,0 +1,113 @@ +// 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 Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; +using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class RazorRuntimeCompilationMvcCoreBuilderExtensions + { + /// + /// Configures to support runtime compilation of Razor views and Razor Pages. + /// + /// The . + /// The . + public static IMvcCoreBuilder AddRazorRuntimeCompilation(this IMvcCoreBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + AddServices(builder.Services); + return builder; + } + + /// + /// Configures to support runtime compilation of Razor views and Razor Pages. + /// + /// The . + /// An action to configure the . + /// The . + public static IMvcCoreBuilder AddRazorRuntimeCompilation(this IMvcCoreBuilder builder, Action setupAction) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (setupAction == null) + { + throw new ArgumentNullException(nameof(setupAction)); + } + + AddServices(builder.Services); + builder.Services.Configure(setupAction); + return builder; + } + + // Internal for testing. + internal static void AddServices(IServiceCollection services) + { + services.TryAddEnumerable( + ServiceDescriptor.Transient, MvcRazorRuntimeCompilationOptionsSetup>()); + + var compilerProvider = services.FirstOrDefault(f => + f.ServiceType == typeof(IViewCompilerProvider) && + f.ImplementationType?.Assembly == typeof(IViewCompilerProvider).Assembly && + f.ImplementationType.FullName == "Microsoft.AspNetCore.Mvc.Razor.Compilation.DefaultViewCompilerProvider"); + + if (compilerProvider != null) + { + // Replace the default implementation of IViewCompilerProvider + services.Remove(compilerProvider); + } + + services.TryAddSingleton(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(); + services.TryAddSingleton(s => + { + var fileSystem = s.GetRequiredService(); + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder => + { + RazorExtensions.Register(builder); + + // Roslyn + TagHelpers infrastructure + var referenceManager = s.GetRequiredService(); + builder.Features.Add(new LazyMetadataReferenceFeature(referenceManager)); + builder.Features.Add(new CompilationTagHelperFeature()); + + // TagHelperDescriptorProviders (actually do tag helper discovery) + builder.Features.Add(new DefaultTagHelperDescriptorProvider()); + builder.Features.Add(new ViewComponentTagHelperDescriptorProvider()); + }); + + return projectEngine; + }); + + // + // Razor Pages + // + services.TryAddEnumerable( + ServiceDescriptor.Singleton()); + + services.TryAddEnumerable( + ServiceDescriptor.Singleton()); + } + } +} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/FileProviderRazorProjectFileSystem.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/FileProviderRazorProjectFileSystem.cs new file mode 100644 index 0000000000..f15d523b21 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/FileProviderRazorProjectFileSystem.cs @@ -0,0 +1,93 @@ +// 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 Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + internal class FileProviderRazorProjectFileSystem : RazorProjectFileSystem + { + private const string RazorFileExtension = ".cshtml"; + private readonly RuntimeCompilationFileProvider _fileProvider; + private readonly IHostingEnvironment _hostingEnvironment; + + public FileProviderRazorProjectFileSystem(RuntimeCompilationFileProvider fileProvider, IHostingEnvironment hostingEnvironment) + { + if (fileProvider == null) + { + throw new ArgumentNullException(nameof(fileProvider)); + } + + if (hostingEnvironment == null) + { + throw new ArgumentNullException(nameof(hostingEnvironment)); + } + + _fileProvider = fileProvider; + _hostingEnvironment = hostingEnvironment; + } + + public IFileProvider FileProvider => _fileProvider.FileProvider; + + public override RazorProjectItem GetItem(string path) + { + path = NormalizeAndEnsureValidPath(path); + var fileInfo = FileProvider.GetFileInfo(path); + + return new FileProviderRazorProjectItem(fileInfo, basePath: string.Empty, filePath: path, root: _hostingEnvironment.ContentRootPath); + } + + public override IEnumerable EnumerateItems(string path) + { + path = NormalizeAndEnsureValidPath(path); + return EnumerateFiles(FileProvider.GetDirectoryContents(path), path, prefix: string.Empty); + } + + private IEnumerable EnumerateFiles(IDirectoryContents directory, string basePath, string prefix) + { + if (directory.Exists) + { + foreach (var fileInfo in directory) + { + if (fileInfo.IsDirectory) + { + var relativePath = prefix + "/" + fileInfo.Name; + var subDirectory = FileProvider.GetDirectoryContents(JoinPath(basePath, relativePath)); + var children = EnumerateFiles(subDirectory, basePath, relativePath); + foreach (var child in children) + { + yield return child; + } + } + else if (string.Equals(RazorFileExtension, Path.GetExtension(fileInfo.Name), StringComparison.OrdinalIgnoreCase)) + { + var filePath = prefix + "/" + fileInfo.Name; + + yield return new FileProviderRazorProjectItem(fileInfo, basePath, filePath: filePath, root: _hostingEnvironment.ContentRootPath); + } + } + } + } + + private static string JoinPath(string path1, string path2) + { + var hasTrailingSlash = path1.EndsWith("/", StringComparison.Ordinal); + var hasLeadingSlash = path2.StartsWith("/", StringComparison.Ordinal); + if (hasLeadingSlash && hasTrailingSlash) + { + return path1 + path2.Substring(1); + } + else if (hasLeadingSlash || hasTrailingSlash) + { + return path1 + path2; + } + + return path1 + "/" + path2; + } + } +} \ No newline at end of file diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/FileProviderRazorProjectItem.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/FileProviderRazorProjectItem.cs new file mode 100644 index 0000000000..f616dd4d06 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/FileProviderRazorProjectItem.cs @@ -0,0 +1,65 @@ +// 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.IO; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + public class FileProviderRazorProjectItem : RazorProjectItem + { + private string _root; + private string _relativePhysicalPath; + private bool _isRelativePhysicalPathSet; + + public FileProviderRazorProjectItem(IFileInfo fileInfo, string basePath, string filePath, string root) + { + FileInfo = fileInfo; + BasePath = basePath; + FilePath = filePath; + _root = root; + } + + public IFileInfo FileInfo { get; } + + public override string BasePath { get; } + + public override string FilePath { get; } + + public override bool Exists => FileInfo.Exists; + + public override string PhysicalPath => FileInfo.PhysicalPath; + + public override string RelativePhysicalPath + { + get + { + if (!_isRelativePhysicalPathSet) + { + _isRelativePhysicalPathSet = true; + + if (Exists) + { + if (_root != null && + !string.IsNullOrEmpty(PhysicalPath) && + PhysicalPath.StartsWith(_root, StringComparison.OrdinalIgnoreCase) && + PhysicalPath.Length > _root.Length && + (PhysicalPath[_root.Length] == Path.DirectorySeparatorChar || PhysicalPath[_root.Length] == Path.AltDirectorySeparatorChar)) + { + _relativePhysicalPath = PhysicalPath.Substring(_root.Length + 1); // Include leading separator + } + } + } + + return _relativePhysicalPath; + } + } + + public override Stream Read() + { + return FileInfo.CreateReadStream(); + } + } +} \ No newline at end of file diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/LazyMetadataReferenceFeature.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/LazyMetadataReferenceFeature.cs new file mode 100644 index 0000000000..3f150a9a5d --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/LazyMetadataReferenceFeature.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 Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + internal class LazyMetadataReferenceFeature : IMetadataReferenceFeature + { + private readonly RazorReferenceManager _referenceManager; + + public LazyMetadataReferenceFeature(RazorReferenceManager referenceManager) + { + _referenceManager = referenceManager; + } + + /// + /// Invoking ensures that compilation + /// references are lazily evaluated. + /// + public IReadOnlyList References => _referenceManager.CompilationReferences; + + public RazorEngine Engine { get; set; } + } +} \ No newline at end of file diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj new file mode 100644 index 0000000000..680bed7d5a --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj @@ -0,0 +1,23 @@ + + + + Runtime compilation support for Razor views and Razor Pages in ASP.NET Core MVC. + netcoreapp3.0 + $(NoWarn);CS1591 + true + aspnetcore;aspnetcoremvc;razor + true + + + + + + + + + + + + + + diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/MvcRazorRuntimeCompilationOptions.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/MvcRazorRuntimeCompilationOptions.cs new file mode 100644 index 0000000000..237d9554ca --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/MvcRazorRuntimeCompilationOptions.cs @@ -0,0 +1,31 @@ +// 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.Hosting; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + public class MvcRazorRuntimeCompilationOptions + { + /// + /// Gets the instances used to locate Razor files. + /// + /// + /// At startup, this collection is initialized to include an instance of + /// that is rooted at the application root. + /// + public IList FileProviders { get; } = new List(); + + /// + /// Gets paths to additional references used during runtime compilation of Razor files. + /// + /// + /// By default, the runtime compiler to gather references + /// uses to compile a Razor file. This API allows providing additional references to the compiler. + /// + public IList AdditionalReferencePaths { get; } = new List(); + } +} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/PageActionDescriptorChangeProvider.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/PageActionDescriptorChangeProvider.cs new file mode 100644 index 0000000000..b8224d7c97 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/PageActionDescriptorChangeProvider.cs @@ -0,0 +1,98 @@ +// 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.Linq; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + internal class PageActionDescriptorChangeProvider : IActionDescriptorChangeProvider + { + private readonly RuntimeCompilationFileProvider _fileProvider; + private readonly string[] _searchPatterns; + private readonly string[] _additionalFilesToTrack; + + public PageActionDescriptorChangeProvider( + RazorProjectEngine projectEngine, + RuntimeCompilationFileProvider fileProvider, + IOptions razorPagesOptions) + { + if (projectEngine == null) + { + throw new ArgumentNullException(nameof(projectEngine)); + } + + if (fileProvider == null) + { + throw new ArgumentNullException(nameof(fileProvider)); + } + + if (razorPagesOptions == null) + { + throw new ArgumentNullException(nameof(razorPagesOptions)); + } + + _fileProvider = fileProvider; + + var rootDirectory = razorPagesOptions.Value.RootDirectory; + Debug.Assert(!string.IsNullOrEmpty(rootDirectory)); + rootDirectory = rootDirectory.TrimEnd('/'); + + // Search pattern that matches all cshtml files under the Pages RootDirectory + var pagesRootSearchPattern = rootDirectory + "/**/*.cshtml"; + + // Search pattern that matches all cshtml files under the Pages AreaRootDirectory + var areaRootSearchPattern = "/Areas/**/*.cshtml"; + + _searchPatterns = new[] + { + pagesRootSearchPattern, + areaRootSearchPattern + }; + + // pagesRootSearchPattern will miss _ViewImports outside the RootDirectory despite these influencing + // compilation. e.g. when RootDirectory = /Dir1/Dir2, the search pattern will ignore changes to + // [/_ViewImports.cshtml, /Dir1/_ViewImports.cshtml]. We need to additionally account for these. + var importFeatures = projectEngine.ProjectFeatures.OfType().ToArray(); + var fileAtPagesRoot = projectEngine.FileSystem.GetItem(rootDirectory + "/Index.cshtml"); + + _additionalFilesToTrack = GetImports(importFeatures, fileAtPagesRoot); + } + + public IChangeToken GetChangeToken() + { + var fileProvider = _fileProvider.FileProvider; + + var changeTokens = new IChangeToken[_additionalFilesToTrack.Length + _searchPatterns.Length]; + for (var i = 0; i < _additionalFilesToTrack.Length; i++) + { + changeTokens[i] = fileProvider.Watch(_additionalFilesToTrack[i]); + } + + for (var i = 0; i < _searchPatterns.Length; i++) + { + var wildcardChangeToken = fileProvider.Watch(_searchPatterns[i]); + changeTokens[_additionalFilesToTrack.Length + i] = wildcardChangeToken; + } + + return new CompositeChangeToken(changeTokens); + } + + private static string[] GetImports( + IImportProjectFeature[] importFeatures, + RazorProjectItem file) + { + return importFeatures + .SelectMany(f => f.GetImports(file)) + .Where(f => f.FilePath != null) + .Select(f => f.FilePath) + .ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/PageDirectiveFeature.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/PageDirectiveFeature.cs new file mode 100644 index 0000000000..e1d5f12c30 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/PageDirectiveFeature.cs @@ -0,0 +1,110 @@ +// 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.Mvc.Razor.Extensions; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + internal static class PageDirectiveFeature + { + private static readonly RazorProjectEngine PageDirectiveEngine = RazorProjectEngine.Create(RazorConfiguration.Default, new EmptyRazorProjectFileSystem(), builder => + { + for (var i = builder.Phases.Count - 1; i >= 0; i--) + { + var phase = builder.Phases[i]; + builder.Phases.RemoveAt(i); + if (phase is IRazorDocumentClassifierPhase) + { + break; + } + } + + RazorExtensions.Register(builder); + builder.Features.Add(new PageDirectiveParserOptionsFeature()); + }); + + public static bool TryGetPageDirective(ILogger logger, RazorProjectItem projectItem, out string template) + { + if (projectItem == null) + { + throw new ArgumentNullException(nameof(projectItem)); + } + + var codeDocument = PageDirectiveEngine.Process(projectItem); + + var documentIRNode = codeDocument.GetDocumentIntermediateNode(); + if (PageDirective.TryGetPageDirective(documentIRNode, out var pageDirective)) + { + if (pageDirective.DirectiveNode is MalformedDirectiveIntermediateNode malformedNode) + { + logger.MalformedPageDirective(projectItem.FilePath, malformedNode.Diagnostics); + } + + template = pageDirective.RouteTemplate; + return true; + } + + template = null; + return false; + } + + private class PageDirectiveParserOptionsFeature : RazorEngineFeatureBase, IConfigureRazorParserOptionsFeature + { + public int Order { get; } + + public void Configure(RazorParserOptionsBuilder options) + { + options.ParseLeadingDirectives = true; + } + } + + private class EmptyRazorProjectFileSystem : RazorProjectFileSystem + { + public override IEnumerable EnumerateItems(string basePath) + { + return Enumerable.Empty(); + } + + public override IEnumerable FindHierarchicalItems(string basePath, string path, string fileName) + { + return Enumerable.Empty(); + } + + public override RazorProjectItem GetItem(string path) + { + return new NotFoundProjectItem(string.Empty, path); + } + + private class NotFoundProjectItem : RazorProjectItem + { + public NotFoundProjectItem(string basePath, string path) + { + BasePath = basePath; + FilePath = path; + } + + /// + public override string BasePath { get; } + + /// + public override string FilePath { get; } + + /// + public override bool Exists => false; + + /// + public override string PhysicalPath => throw new NotSupportedException(); + + /// + public override Stream Read() => throw new NotSupportedException(); + } + } + } +} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Properties/AssemblyInfo.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..472b09032a --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Properties/Resources.Designer.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..ce4fec5608 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Properties/Resources.Designer.cs @@ -0,0 +1,100 @@ +// +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// One or more compilation failures occurred: + /// + internal static string CompilationFailed + { + get => GetString("CompilationFailed"); + } + + /// + /// One or more compilation failures occurred: + /// + internal static string FormatCompilationFailed() + => GetString("CompilationFailed"); + + /// + /// One or more compilation references may be missing. If you're seeing this in a published application, set '{0}' to true in your project file to ensure files in the refs directory are published. + /// + internal static string Compilation_MissingReferences + { + get => GetString("Compilation_MissingReferences"); + } + + /// + /// One or more compilation references may be missing. If you're seeing this in a published application, set '{0}' to true in your project file to ensure files in the refs directory are published. + /// + internal static string FormatCompilation_MissingReferences(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Compilation_MissingReferences"), p0); + + /// + /// '{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering. + /// + internal static string FileProvidersAreRequired + { + get => GetString("FileProvidersAreRequired"); + } + + /// + /// '{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering. + /// + internal static string FormatFileProvidersAreRequired(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("FileProvidersAreRequired"), p0, p1, p2); + + /// + /// Generated Code + /// + internal static string GeneratedCodeFileName + { + get => GetString("GeneratedCodeFileName"); + } + + /// + /// Generated Code + /// + internal static string FormatGeneratedCodeFileName() + => GetString("GeneratedCodeFileName"); + + /// + /// The debug type specified in the dependency context could be parsed. The debug type value '{0}' is not supported. + /// + internal static string UnsupportedDebugInformationFormat + { + get => GetString("UnsupportedDebugInformationFormat"); + } + + /// + /// The debug type specified in the dependency context could be parsed. The debug type value '{0}' is not supported. + /// + internal static string FormatUnsupportedDebugInformationFormat(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("UnsupportedDebugInformationFormat"), p0); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RazorProjectPageRouteModelProvider.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RazorProjectPageRouteModelProvider.cs new file mode 100644 index 0000000000..a3b63ed6a8 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RazorProjectPageRouteModelProvider.cs @@ -0,0 +1,107 @@ +// 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 Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + internal class RazorProjectPageRouteModelProvider : IPageRouteModelProvider + { + private const string AreaRootDirectory = "/Areas"; + private readonly RazorProjectFileSystem _razorFileSystem; + private readonly RazorPagesOptions _pagesOptions; + private readonly PageRouteModelFactory _routeModelFactory; + private readonly ILogger _logger; + + public RazorProjectPageRouteModelProvider( + RazorProjectFileSystem razorFileSystem, + IOptions pagesOptionsAccessor, + ILoggerFactory loggerFactory) + { + _razorFileSystem = razorFileSystem; + _pagesOptions = pagesOptionsAccessor.Value; + _logger = loggerFactory.CreateLogger(); + _routeModelFactory = new PageRouteModelFactory(_pagesOptions, _logger); + } + + /// + /// Ordered to execute after . + /// + public int Order => -1000 + 10; + + public void OnProvidersExecuted(PageRouteModelProviderContext context) + { + } + + public void OnProvidersExecuting(PageRouteModelProviderContext context) + { + // When RootDirectory and AreaRootDirectory overlap, e.g. RootDirectory = /, AreaRootDirectory = /Areas; + // we need to ensure that the page is only route-able via the area route. By adding area routes first, + // we'll ensure non area routes get skipped when it encounters an IsAlreadyRegistered check. + + AddAreaPageModels(context); + AddPageModels(context); + } + + private void AddPageModels(PageRouteModelProviderContext context) + { + foreach (var item in _razorFileSystem.EnumerateItems(_pagesOptions.RootDirectory)) + { + var relativePath = item.CombinedPath; + if (context.RouteModels.Any(m => string.Equals(relativePath, m.RelativePath, StringComparison.OrdinalIgnoreCase))) + { + // A route for this file was already registered either by the CompiledPageRouteModel or as an area route. + // by this provider. Skip registering an additional entry. + + // Note: We're comparing duplicates based on root-relative paths. This eliminates a page from being discovered + // by overlapping area and non-area routes where ViewEnginePath would be different. + continue; + } + + if (!PageDirectiveFeature.TryGetPageDirective(_logger, item, out var routeTemplate)) + { + // .cshtml pages without @page are not RazorPages. + continue; + } + + var routeModel = _routeModelFactory.CreateRouteModel(relativePath, routeTemplate); + if (routeModel != null) + { + context.RouteModels.Add(routeModel); + } + } + } + + private void AddAreaPageModels(PageRouteModelProviderContext context) + { + foreach (var item in _razorFileSystem.EnumerateItems(AreaRootDirectory)) + { + var relativePath = item.CombinedPath; + if (context.RouteModels.Any(m => string.Equals(relativePath, m.RelativePath, StringComparison.OrdinalIgnoreCase))) + { + // A route for this file was already registered either by the CompiledPageRouteModel. + // Skip registering an additional entry. + continue; + } + + if (!PageDirectiveFeature.TryGetPageDirective(_logger, item, out var routeTemplate)) + { + // .cshtml pages without @page are not RazorPages. + continue; + } + + var routeModel = _routeModelFactory.CreateAreaRouteModel(relativePath, routeTemplate); + if (routeModel != null) + { + context.RouteModels.Add(routeModel); + } + } + } + } +} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RazorReferenceManager.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RazorReferenceManager.cs new file mode 100644 index 0000000000..ce0094a911 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RazorReferenceManager.cs @@ -0,0 +1,79 @@ +// 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 System.Reflection.PortableExecutable; +using System.Threading; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + internal class RazorReferenceManager + { + private readonly ApplicationPartManager _partManager; + private readonly MvcRazorRuntimeCompilationOptions _options; + private object _compilationReferencesLock = new object(); + private bool _compilationReferencesInitialized; + private IReadOnlyList _compilationReferences; + + public RazorReferenceManager( + ApplicationPartManager partManager, + IOptions options) + { + _partManager = partManager; + _options = options.Value; + } + + public virtual IReadOnlyList CompilationReferences + { + get + { + return LazyInitializer.EnsureInitialized( + ref _compilationReferences, + ref _compilationReferencesInitialized, + ref _compilationReferencesLock, + GetCompilationReferences); + } + } + + private IReadOnlyList GetCompilationReferences() + { + var referencePaths = GetReferencePaths(); + + return referencePaths + .Select(CreateMetadataReference) + .ToList(); + } + + // For unit testing + internal IEnumerable GetReferencePaths() + { + var referencesFromApplicationParts = _partManager + .ApplicationParts + .OfType() + .SelectMany(part => part.GetReferencePaths()); + + var referencePaths = referencesFromApplicationParts + .Concat(_options.AdditionalReferencePaths) + .Distinct(StringComparer.OrdinalIgnoreCase); + + return referencePaths; + } + + private static MetadataReference CreateMetadataReference(string path) + { + using (var stream = File.OpenRead(path)) + { + var moduleMetadata = ModuleMetadata.CreateFromStream(stream, PEStreamOptions.PrefetchMetadata); + var assemblyMetadata = AssemblyMetadata.Create(moduleMetadata); + + return assemblyMetadata.GetReference(filePath: path); + } + } + } +} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RazorRuntimeCompilationLoggerExtensions.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RazorRuntimeCompilationLoggerExtensions.cs new file mode 100644 index 0000000000..51ae2dc03d --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RazorRuntimeCompilationLoggerExtensions.cs @@ -0,0 +1,179 @@ +// 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 Microsoft.AspNetCore.Razor.Language; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + internal static class MvcRazorLoggerExtensions + { + private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; + + private static readonly Action _generatedCodeToAssemblyCompilationStart; + private static readonly Action _generatedCodeToAssemblyCompilationEnd; + private static readonly Action _malformedPageDirective; + private static readonly Action _viewCompilerLocatedCompiledView; + private static readonly Action _viewCompilerNoCompiledViewsFound; + private static readonly Action _viewCompilerLocatedCompiledViewForPath; + private static readonly Action _viewCompilerRecompilingCompiledView; + private static readonly Action _viewCompilerCouldNotFindFileToCompileForPath; + private static readonly Action _viewCompilerFoundFileToCompileForPath; + private static readonly Action _viewCompilerInvalidatingCompiledFile; + + private static readonly Action _viewLookupCacheMiss; + private static readonly Action _viewLookupCacheHit; + private static readonly Action _precompiledViewFound; + + static MvcRazorLoggerExtensions() + { + _viewCompilerLocatedCompiledView = LoggerMessage.Define( + LogLevel.Debug, + 3, + "Initializing Razor view compiler with compiled view: '{ViewName}'."); + + _viewCompilerNoCompiledViewsFound = LoggerMessage.Define( + LogLevel.Debug, + 4, + "Initializing Razor view compiler with no compiled views."); + + _viewCompilerLocatedCompiledViewForPath = LoggerMessage.Define( + LogLevel.Trace, + 5, + "Located compiled view for view at path '{Path}'."); + + _viewCompilerLocatedCompiledViewForPath = LoggerMessage.Define( + LogLevel.Trace, + 5, + "Located compiled view for view at path '{Path}'."); + + _viewCompilerRecompilingCompiledView = LoggerMessage.Define( + LogLevel.Trace, + 6, + "Invalidating compiled view for view at path '{Path}'."); + + _viewCompilerCouldNotFindFileToCompileForPath = LoggerMessage.Define( + LogLevel.Trace, + 7, + "Could not find a file for view at path '{Path}'."); + + _viewCompilerFoundFileToCompileForPath = LoggerMessage.Define( + LogLevel.Trace, + 8, + "Found file at path '{Path}'."); + + _viewCompilerInvalidatingCompiledFile = LoggerMessage.Define( + LogLevel.Trace, + 9, + "Invalidating compiled view at path '{Path}' with a file since the checksum did not match."); + + _viewLookupCacheMiss = LoggerMessage.Define( + LogLevel.Debug, + 1, + "View lookup cache miss for view '{ViewName}' in controller '{ControllerName}'."); + + _viewLookupCacheHit = LoggerMessage.Define( + LogLevel.Debug, + 2, + "View lookup cache hit for view '{ViewName}' in controller '{ControllerName}'."); + + _precompiledViewFound = LoggerMessage.Define( + LogLevel.Debug, + 3, + "Using precompiled view for '{RelativePath}'."); + + _generatedCodeToAssemblyCompilationStart = LoggerMessage.Define( + LogLevel.Debug, + 1, + "Compilation of the generated code for the Razor file at '{FilePath}' started."); + + _generatedCodeToAssemblyCompilationEnd = LoggerMessage.Define( + LogLevel.Debug, + 2, + "Compilation of the generated code for the Razor file at '{FilePath}' completed in {ElapsedMilliseconds}ms."); + + _malformedPageDirective = LoggerMessage.Define( + LogLevel.Warning, + new EventId(104, "MalformedPageDirective"), + "The page directive at '{FilePath}' is malformed. Please fix the following issues: {Diagnostics}"); + } + + public static void ViewCompilerLocatedCompiledView(this ILogger logger, string view) + { + _viewCompilerLocatedCompiledView(logger, view, null); + } + + public static void ViewCompilerNoCompiledViewsFound(this ILogger logger) + { + _viewCompilerNoCompiledViewsFound(logger, null); + } + + public static void ViewCompilerLocatedCompiledViewForPath(this ILogger logger, string path) + { + _viewCompilerLocatedCompiledViewForPath(logger, path, null); + } + + public static void ViewCompilerCouldNotFindFileAtPath(this ILogger logger, string path) + { + _viewCompilerCouldNotFindFileToCompileForPath(logger, path, null); + } + + public static void ViewCompilerFoundFileToCompile(this ILogger logger, string path) + { + _viewCompilerFoundFileToCompileForPath(logger, path, null); + } + + public static void ViewCompilerInvalidingCompiledFile(this ILogger logger, string path) + { + _viewCompilerInvalidatingCompiledFile(logger, path, null); + } + + public static void ViewLookupCacheMiss(this ILogger logger, string viewName, string controllerName) + { + _viewLookupCacheMiss(logger, viewName, controllerName, null); + } + + public static void ViewLookupCacheHit(this ILogger logger, string viewName, string controllerName) + { + _viewLookupCacheHit(logger, viewName, controllerName, null); + } + + public static void PrecompiledViewFound(this ILogger logger, string relativePath) + { + _precompiledViewFound(logger, relativePath, null); + } + + public static void GeneratedCodeToAssemblyCompilationStart(this ILogger logger, string filePath) + { + _generatedCodeToAssemblyCompilationStart(logger, filePath, null); + } + + public static void GeneratedCodeToAssemblyCompilationEnd(this ILogger logger, string filePath, long startTimestamp) + { + // Don't log if logging wasn't enabled at start of request as time will be wildly wrong. + if (startTimestamp != 0) + { + var currentTimestamp = Stopwatch.GetTimestamp(); + var elapsed = new TimeSpan((long)(TimestampToTicks * (currentTimestamp - startTimestamp))); + _generatedCodeToAssemblyCompilationEnd(logger, filePath, elapsed.TotalMilliseconds, null); + } + } + + public static void MalformedPageDirective(this ILogger logger, string filePath, IList diagnostics) + { + if (logger.IsEnabled(LogLevel.Warning)) + { + var messages = new string[diagnostics.Count]; + for (var i = 0; i < diagnostics.Count; i++) + { + messages[i] = diagnostics[i].GetMessage(); + } + + _malformedPageDirective(logger, filePath, messages, null); + } + } + } +} \ No newline at end of file diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Resources.resx b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Resources.resx new file mode 100644 index 0000000000..48eb29a54d --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Resources.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + One or more compilation failures occurred: + + + One or more compilation references may be missing. If you're seeing this in a published application, set '{0}' to true in your project file to ensure files in the refs directory are published. + + + '{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering. + + + Generated Code + + + The debug type specified in the dependency context could be parsed. The debug type value '{0}' is not supported. + + \ No newline at end of file diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RuntimeCompilationFileProvider.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RuntimeCompilationFileProvider.cs new file mode 100644 index 0000000000..ea7372bd1a --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RuntimeCompilationFileProvider.cs @@ -0,0 +1,57 @@ +// 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 Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + internal class RuntimeCompilationFileProvider + { + private readonly MvcRazorRuntimeCompilationOptions _options; + private IFileProvider _compositeFileProvider; + + public RuntimeCompilationFileProvider(IOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _options = options.Value; + } + + public IFileProvider FileProvider + { + get + { + if (_compositeFileProvider == null) + { + _compositeFileProvider = GetCompositeFileProvider(_options); + } + + return _compositeFileProvider; + } + } + + private static IFileProvider GetCompositeFileProvider(MvcRazorRuntimeCompilationOptions options) + { + var fileProviders = options.FileProviders; + if (fileProviders.Count == 0) + { + var message = Resources.FormatFileProvidersAreRequired( + typeof(MvcRazorRuntimeCompilationOptions).FullName, + nameof(MvcRazorRuntimeCompilationOptions.FileProviders), + typeof(IFileProvider).FullName); + throw new InvalidOperationException(message); + } + else if (fileProviders.Count == 1) + { + return fileProviders[0]; + } + + return new CompositeFileProvider(fileProviders); + } + } +} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RuntimeViewCompiler.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RuntimeViewCompiler.cs new file mode 100644 index 0000000000..4f417df6eb --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RuntimeViewCompiler.cs @@ -0,0 +1,434 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Razor.Hosting; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + internal class RuntimeViewCompiler : IViewCompiler + { + private readonly object _cacheLock = new object(); + private readonly Dictionary _precompiledViews; + private readonly ConcurrentDictionary _normalizedPathCache; + private readonly IFileProvider _fileProvider; + private readonly RazorProjectEngine _projectEngine; + private readonly IMemoryCache _cache; + private readonly ILogger _logger; + private readonly CSharpCompiler _csharpCompiler; + + public RuntimeViewCompiler( + IFileProvider fileProvider, + RazorProjectEngine projectEngine, + CSharpCompiler csharpCompiler, + IList precompiledViews, + ILogger logger) + { + if (fileProvider == null) + { + throw new ArgumentNullException(nameof(fileProvider)); + } + + if (projectEngine == null) + { + throw new ArgumentNullException(nameof(projectEngine)); + } + + if (csharpCompiler == null) + { + throw new ArgumentNullException(nameof(csharpCompiler)); + } + + if (precompiledViews == null) + { + throw new ArgumentNullException(nameof(precompiledViews)); + } + + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + _fileProvider = fileProvider; + _projectEngine = projectEngine; + _csharpCompiler = csharpCompiler; + _logger = logger; + + + _normalizedPathCache = new ConcurrentDictionary(StringComparer.Ordinal); + + // This is our L0 cache, and is a durable store. Views migrate into the cache as they are requested + // from either the set of known precompiled views, or by being compiled. + _cache = new MemoryCache(new MemoryCacheOptions()); + + // We need to validate that the all of the precompiled views are unique by path (case-insensitive). + // We do this because there's no good way to canonicalize paths on windows, and it will create + // problems when deploying to linux. Rather than deal with these issues, we just don't support + // views that differ only by case. + _precompiledViews = new Dictionary( + precompiledViews.Count, + StringComparer.OrdinalIgnoreCase); + + foreach (var precompiledView in precompiledViews) + { + logger.ViewCompilerLocatedCompiledView(precompiledView.RelativePath); + + if (!_precompiledViews.ContainsKey(precompiledView.RelativePath)) + { + // View ordering has precedence semantics, a view with a higher precedence was + // already added to the list. + _precompiledViews.Add(precompiledView.RelativePath, precompiledView); + } + } + + if (_precompiledViews.Count == 0) + { + logger.ViewCompilerNoCompiledViewsFound(); + } + } + + public Task CompileAsync(string relativePath) + { + if (relativePath == null) + { + throw new ArgumentNullException(nameof(relativePath)); + } + + // Attempt to lookup the cache entry using the passed in path. This will succeed if the path is already + // normalized and a cache entry exists. + if (_cache.TryGetValue(relativePath, out Task cachedResult)) + { + return cachedResult; + } + + var normalizedPath = GetNormalizedPath(relativePath); + if (_cache.TryGetValue(normalizedPath, out cachedResult)) + { + return cachedResult; + } + + // Entry does not exist. Attempt to create one. + cachedResult = OnCacheMiss(normalizedPath); + return cachedResult; + } + + private Task OnCacheMiss(string normalizedPath) + { + ViewCompilerWorkItem item; + TaskCompletionSource taskSource; + MemoryCacheEntryOptions cacheEntryOptions; + + // Safe races cannot be allowed when compiling Razor pages. To ensure only one compilation request succeeds + // per file, we'll lock the creation of a cache entry. Creating the cache entry should be very quick. The + // actual work for compiling files happens outside the critical section. + lock (_cacheLock) + { + // Double-checked locking to handle a possible race. + if (_cache.TryGetValue(normalizedPath, out Task result)) + { + return result; + } + + if (_precompiledViews.TryGetValue(normalizedPath, out var precompiledView)) + { + _logger.ViewCompilerLocatedCompiledViewForPath(normalizedPath); + item = CreatePrecompiledWorkItem(normalizedPath, precompiledView); + } + else + { + item = CreateRuntimeCompilationWorkItem(normalizedPath); + } + + // At this point, we've decided what to do - but we should create the cache entry and + // release the lock first. + cacheEntryOptions = new MemoryCacheEntryOptions(); + + Debug.Assert(item.ExpirationTokens != null); + for (var i = 0; i < item.ExpirationTokens.Count; i++) + { + cacheEntryOptions.ExpirationTokens.Add(item.ExpirationTokens[i]); + } + + taskSource = new TaskCompletionSource(creationOptions: TaskCreationOptions.RunContinuationsAsynchronously); + if (item.SupportsCompilation) + { + // We'll compile in just a sec, be patient. + } + else + { + // If we can't compile, we should have already created the descriptor + Debug.Assert(item.Descriptor != null); + taskSource.SetResult(item.Descriptor); + } + + _cache.Set(normalizedPath, taskSource.Task, cacheEntryOptions); + } + + // Now the lock has been released so we can do more expensive processing. + if (item.SupportsCompilation) + { + Debug.Assert(taskSource != null); + + if (item.Descriptor?.Item != null && + ChecksumValidator.IsItemValid(_projectEngine.FileSystem, item.Descriptor.Item)) + { + // If the item has checksums to validate, we should also have a precompiled view. + Debug.Assert(item.Descriptor != null); + + taskSource.SetResult(item.Descriptor); + return taskSource.Task; + } + + _logger.ViewCompilerInvalidingCompiledFile(item.NormalizedPath); + try + { + var descriptor = CompileAndEmit(normalizedPath); + descriptor.ExpirationTokens = cacheEntryOptions.ExpirationTokens; + taskSource.SetResult(descriptor); + } + catch (Exception ex) + { + taskSource.SetException(ex); + } + } + + return taskSource.Task; + } + + private ViewCompilerWorkItem CreatePrecompiledWorkItem(string normalizedPath, CompiledViewDescriptor precompiledView) + { + // We have a precompiled view - but we're not sure that we can use it yet. + // + // We need to determine first if we have enough information to 'recompile' this view. If that's the case + // we'll create change tokens for all of the files. + // + // Then we'll attempt to validate if any of those files have different content than the original sources + // based on checksums. + if (precompiledView.Item == null || !ChecksumValidator.IsRecompilationSupported(precompiledView.Item)) + { + return new ViewCompilerWorkItem() + { + // If we don't have a checksum for the primary source file we can't recompile. + SupportsCompilation = false, + + ExpirationTokens = Array.Empty(), // Never expire because we can't recompile. + Descriptor = precompiledView, // This will be used as-is. + }; + } + + var item = new ViewCompilerWorkItem() + { + SupportsCompilation = true, + + Descriptor = precompiledView, // This might be used, if the checksums match. + + // Used to validate and recompile + NormalizedPath = normalizedPath, + + ExpirationTokens = GetExpirationTokens(precompiledView), + }; + + // We also need to create a new descriptor, because the original one doesn't have expiration tokens on + // it. These will be used by the view location cache, which is like an L1 cache for views (this class is + // the L2 cache). + item.Descriptor = new CompiledViewDescriptor() + { + ExpirationTokens = item.ExpirationTokens, + Item = precompiledView.Item, + RelativePath = precompiledView.RelativePath, + }; + + return item; + } + + private ViewCompilerWorkItem CreateRuntimeCompilationWorkItem(string normalizedPath) + { + IList expirationTokens = new List + { + _fileProvider.Watch(normalizedPath), + }; + + var projectItem = _projectEngine.FileSystem.GetItem(normalizedPath); + if (!projectItem.Exists) + { + _logger.ViewCompilerCouldNotFindFileAtPath(normalizedPath); + + // If the file doesn't exist, we can't do compilation right now - we still want to cache + // the fact that we tried. This will allow us to re-trigger compilation if the view file + // is added. + return new ViewCompilerWorkItem() + { + // We don't have enough information to compile + SupportsCompilation = false, + + Descriptor = new CompiledViewDescriptor() + { + RelativePath = normalizedPath, + ExpirationTokens = expirationTokens, + }, + + // We can try again if the file gets created. + ExpirationTokens = expirationTokens, + }; + } + + _logger.ViewCompilerFoundFileToCompile(normalizedPath); + + GetChangeTokensFromImports(expirationTokens, projectItem); + + return new ViewCompilerWorkItem() + { + SupportsCompilation = true, + + NormalizedPath = normalizedPath, + ExpirationTokens = expirationTokens, + }; + } + + private IList GetExpirationTokens(CompiledViewDescriptor precompiledView) + { + var checksums = precompiledView.Item.GetChecksumMetadata(); + var expirationTokens = new List(checksums.Count); + + for (var i = 0; i < checksums.Count; i++) + { + // We rely on Razor to provide the right set of checksums. Trust the compiler, it has to do a good job, + // so it probably will. + expirationTokens.Add(_fileProvider.Watch(checksums[i].Identifier)); + } + + return expirationTokens; + } + + private void GetChangeTokensFromImports(IList expirationTokens, RazorProjectItem projectItem) + { + // OK this means we can do compilation. For now let's just identify the other files we need to watch + // so we can create the cache entry. Compilation will happen after we release the lock. + var importFeature = _projectEngine.ProjectFeatures.OfType().ToArray(); + foreach (var feature in importFeature) + { + foreach (var file in feature.GetImports(projectItem)) + { + if (file.FilePath != null) + { + expirationTokens.Add(_fileProvider.Watch(file.FilePath)); + } + } + } + } + + protected virtual CompiledViewDescriptor CompileAndEmit(string relativePath) + { + var projectItem = _projectEngine.FileSystem.GetItem(relativePath); + var codeDocument = _projectEngine.Process(projectItem); + var cSharpDocument = codeDocument.GetCSharpDocument(); + + if (cSharpDocument.Diagnostics.Count > 0) + { + throw CompilationFailedExceptionFactory.Create( + codeDocument, + cSharpDocument.Diagnostics); + } + + var assembly = CompileAndEmit(codeDocument, cSharpDocument.GeneratedCode); + + // Anything we compile from source will use Razor 2.1 and so should have the new metadata. + var loader = new RazorCompiledItemLoader(); + var item = loader.LoadItems(assembly).SingleOrDefault(); + return new CompiledViewDescriptor(item); + } + + internal Assembly CompileAndEmit(RazorCodeDocument codeDocument, string generatedCode) + { + _logger.GeneratedCodeToAssemblyCompilationStart(codeDocument.Source.FilePath); + + var startTimestamp = _logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : 0; + + var assemblyName = Path.GetRandomFileName(); + var compilation = CreateCompilation(generatedCode, assemblyName); + + var emitOptions = _csharpCompiler.EmitOptions; + var emitPdbFile = _csharpCompiler.EmitPdb && emitOptions.DebugInformationFormat != DebugInformationFormat.Embedded; + + using (var assemblyStream = new MemoryStream()) + using (var pdbStream = emitPdbFile ? new MemoryStream() : null) + { + var result = compilation.Emit( + assemblyStream, + pdbStream, + options: emitOptions); + + if (!result.Success) + { + throw CompilationFailedExceptionFactory.Create( + codeDocument, + generatedCode, + assemblyName, + result.Diagnostics); + } + + assemblyStream.Seek(0, SeekOrigin.Begin); + pdbStream?.Seek(0, SeekOrigin.Begin); + + var assembly = Assembly.Load(assemblyStream.ToArray(), pdbStream?.ToArray()); + _logger.GeneratedCodeToAssemblyCompilationEnd(codeDocument.Source.FilePath, startTimestamp); + + return assembly; + } + } + + private CSharpCompilation CreateCompilation(string compilationContent, string assemblyName) + { + var sourceText = SourceText.From(compilationContent, Encoding.UTF8); + var syntaxTree = _csharpCompiler.CreateSyntaxTree(sourceText).WithFilePath(assemblyName); + return _csharpCompiler + .CreateCompilation(assemblyName) + .AddSyntaxTrees(syntaxTree); + } + + private string GetNormalizedPath(string relativePath) + { + Debug.Assert(relativePath != null); + if (relativePath.Length == 0) + { + return relativePath; + } + + if (!_normalizedPathCache.TryGetValue(relativePath, out var normalizedPath)) + { + normalizedPath = ViewPath.NormalizePath(relativePath); + _normalizedPathCache[relativePath] = normalizedPath; + } + + return normalizedPath; + } + + private class ViewCompilerWorkItem + { + public bool SupportsCompilation { get; set; } + + public string NormalizedPath { get; set; } + + public IList ExpirationTokens { get; set; } + + public CompiledViewDescriptor Descriptor { get; set; } + } + } +} diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RuntimeViewCompilerProvider.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RuntimeViewCompilerProvider.cs new file mode 100644 index 0000000000..4202da6bd7 --- /dev/null +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/RuntimeViewCompilerProvider.cs @@ -0,0 +1,64 @@ +// 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.Threading; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + internal class RuntimeViewCompilerProvider : IViewCompilerProvider + { + private readonly RazorProjectEngine _razorProjectEngine; + private readonly ApplicationPartManager _applicationPartManager; + private readonly CSharpCompiler _csharpCompiler; + private readonly RuntimeCompilationFileProvider _fileProvider; + private readonly ILogger _logger; + private readonly Func _createCompiler; + + private object _initializeLock = new object(); + private bool _initialized; + private IViewCompiler _compiler; + + public RuntimeViewCompilerProvider( + ApplicationPartManager applicationPartManager, + RazorProjectEngine razorProjectEngine, + RuntimeCompilationFileProvider fileProvider, + CSharpCompiler csharpCompiler, + ILoggerFactory loggerFactory) + { + _applicationPartManager = applicationPartManager; + _razorProjectEngine = razorProjectEngine; + _csharpCompiler = csharpCompiler; + _fileProvider = fileProvider; + + _logger = loggerFactory.CreateLogger(); + _createCompiler = CreateCompiler; + } + + public IViewCompiler GetCompiler() + { + return LazyInitializer.EnsureInitialized( + ref _compiler, + ref _initialized, + ref _initializeLock, + _createCompiler); + } + + private IViewCompiler CreateCompiler() + { + var feature = new ViewsFeature(); + _applicationPartManager.PopulateFeature(feature); + + return new RuntimeViewCompiler( + _fileProvider.FileProvider, + _razorProjectEngine, + _csharpCompiler, + feature.ViewDescriptors, + _logger); + } + } +} \ No newline at end of file diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/RazorViewCompiler.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/DefaultViewCompiler.cs similarity index 97% rename from src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/RazorViewCompiler.cs rename to src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/DefaultViewCompiler.cs index 7afc53edab..1819015a16 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/RazorViewCompiler.cs +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/DefaultViewCompiler.cs @@ -14,13 +14,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation /// /// Caches the result of runtime compilation of Razor files for the duration of the application lifetime. /// - internal class RazorViewCompiler : IViewCompiler + internal class DefaultViewCompiler : IViewCompiler { private readonly Dictionary> _compiledViews; private readonly ConcurrentDictionary _normalizedPathCache; private readonly ILogger _logger; - public RazorViewCompiler( + public DefaultViewCompiler( IList compiledViews, ILogger logger) { diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/RazorViewCompilerProvider.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/DefaultViewCompilerProvider.cs similarity index 67% rename from src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/RazorViewCompilerProvider.cs rename to src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/DefaultViewCompilerProvider.cs index 64965e6f72..b92077ad69 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/RazorViewCompilerProvider.cs +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/DefaultViewCompilerProvider.cs @@ -6,18 +6,18 @@ using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Mvc.Razor.Compilation { - internal class RazorViewCompilerProvider : IViewCompilerProvider + internal class DefaultViewCompilerProvider : IViewCompilerProvider { - private readonly RazorViewCompiler _compiler; + private readonly DefaultViewCompiler _compiler; - public RazorViewCompilerProvider( + public DefaultViewCompilerProvider( ApplicationPartManager applicationPartManager, ILoggerFactory loggerFactory) { var feature = new ViewsFeature(); applicationPartManager.PopulateFeature(feature); - _compiler = new RazorViewCompiler(feature.ViewDescriptors, loggerFactory.CreateLogger()); + _compiler = new DefaultViewCompiler(feature.ViewDescriptors, loggerFactory.CreateLogger()); } public IViewCompiler GetCompiler() => _compiler; diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs index 46139a28a0..7b3ee93a3a 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs @@ -141,7 +141,7 @@ namespace Microsoft.Extensions.DependencyInjection ServiceDescriptor.Transient, RazorViewEngineOptionsSetup>()); services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(); // In the default scenario the following services are singleton by virtue of being initialized as part of // creating the singleton RazorViewEngine instance. diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Properties/AssemblyInfo.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Properties/AssemblyInfo.cs index 5bd88d4693..db40b79a34 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Properties/AssemblyInfo.cs +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor/Properties/AssemblyInfo.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.RazorPages.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.TagHelpers.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteMetadata.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteMetadata.cs index 5af62882b7..46d86127cc 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteMetadata.cs +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteMetadata.cs @@ -3,17 +3,31 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels { - // This is used to store the uncombined parts of the final page route + /// + /// Metadata used to construct an endpoint route to the page. + /// // Note: This type name is referenced by name in AuthorizationMiddleware, do not change this without addressing https://github.com/aspnet/AspNetCore/issues/7011 - internal class PageRouteMetadata + public sealed class PageRouteMetadata { + /// + /// Initializes a new instance of . + /// + /// The page route. + /// The route template specified by the page. public PageRouteMetadata(string pageRoute, string routeTemplate) { PageRoute = pageRoute; RouteTemplate = routeTemplate; } + /// + /// Gets the page route. + /// public string PageRoute { get; } + + /// + /// Gets the route template specified by the page. + /// public string RouteTemplate { get; } } } diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModelFactory.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModelFactory.cs index b2650063b7..1838d0ad9c 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModelFactory.cs +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModelFactory.cs @@ -7,18 +7,27 @@ using System.IO; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.ApplicationModels { internal class PageRouteModelFactory { + private static readonly Action _unsupportedAreaPath; + private static readonly string IndexFileName = "Index" + RazorViewEngine.ViewExtension; private readonly RazorPagesOptions _options; private readonly ILogger _logger; private readonly string _normalizedRootDirectory; private readonly string _normalizedAreaRootDirectory; + static PageRouteModelFactory() + { + _unsupportedAreaPath = LoggerMessage.Define( + LogLevel.Warning, + new EventId(1, "UnsupportedAreaPath"), + "The page at '{FilePath}' is located under the area root directory '/Areas/' but does not follow the path format '/Areas/AreaName/Pages/Directory/FileName.cshtml"); + } + public PageRouteModelFactory( RazorPagesOptions options, ILogger logger) @@ -96,7 +105,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels areaRootEndIndex >= relativePath.Length - 1 || // There's at least one token after the area root. !relativePath.StartsWith(_normalizedAreaRootDirectory, StringComparison.OrdinalIgnoreCase)) // The path must start with area root. { - _logger.UnsupportedAreaPath(relativePath); + _unsupportedAreaPath(_logger, relativePath, null); return false; } @@ -104,7 +113,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels var areaEndIndex = relativePath.IndexOf('/', startIndex: areaRootEndIndex + 1); if (areaEndIndex == -1 || areaEndIndex == relativePath.Length) { - _logger.UnsupportedAreaPath(relativePath); + _unsupportedAreaPath(_logger, relativePath, null); return false; } @@ -112,7 +121,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels // Ensure the next token is the "Pages" directory if (string.Compare(relativePath, areaEndIndex, AreaPagesRoot, 0, AreaPagesRoot.Length, StringComparison.OrdinalIgnoreCase) != 0) { - _logger.UnsupportedAreaPath(relativePath); + _unsupportedAreaPath(_logger, relativePath, null); return false; } diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs index 23a449e731..61ab2a7a07 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure diff --git a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/PageLoggerExtensions.cs b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/PageLoggerExtensions.cs index 825dc0c190..9c965f3244 100644 --- a/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/PageLoggerExtensions.cs +++ b/src/Mvc/src/Microsoft.AspNetCore.Mvc.RazorPages/PageLoggerExtensions.cs @@ -18,8 +18,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages private static readonly Action _handlerMethodExecuted; private static readonly Action _implicitHandlerMethodExecuted; private static readonly Action _pageFilterShortCircuit; - private static readonly Action _malformedPageDirective; - private static readonly Action _unsupportedAreaPath; private static readonly Action _notMostEffectiveFilter; private static readonly Action _beforeExecutingMethodOnFilter; private static readonly Action _afterExecutingMethodOnFilter; @@ -53,11 +51,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages new EventId(3, "PageFilterShortCircuited"), "Request was short circuited at page filter '{PageFilter}'."); - _malformedPageDirective = LoggerMessage.Define( - LogLevel.Warning, - new EventId(104, "MalformedPageDirective"), - "The page directive at '{FilePath}' is malformed. Please fix the following issues: {Diagnostics}"); - _notMostEffectiveFilter = LoggerMessage.Define( LogLevel.Debug, new EventId(1, "NotMostEffectiveFilter"), @@ -72,11 +65,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages LogLevel.Trace, new EventId(2, "AfterExecutingMethodOnFilter"), "{FilterType}: After executing {Method} on filter {Filter}."); - - _unsupportedAreaPath = LoggerMessage.Define( - LogLevel.Warning, - new EventId(1, "UnsupportedAreaPath"), - "The page at '{FilePath}' is located under the area root directory '/Areas/' but does not follow the path format '/Areas/AreaName/Pages/Directory/FileName.cshtml"); } public static void ExecutingHandlerMethod(this ILogger logger, PageContext context, HandlerMethodDescriptor handler, object[] arguments) @@ -153,13 +141,5 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages { _notMostEffectiveFilter(logger, policyType, null); } - - public static void UnsupportedAreaPath(this ILogger logger, string filePath) - { - if (logger.IsEnabled(LogLevel.Warning)) - { - _unsupportedAreaPath(logger, filePath, null); - } - } } } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorBuildTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorBuildTest.cs index e90c761c83..ba90b35947 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorBuildTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorBuildTest.cs @@ -1,6 +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 System; +using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -17,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public HttpClient Client { get; } - [Fact(Skip = "https://github.com/aspnet/Mvc/issues/8753")] + [Fact] public async Task Rzc_LocalPageWithDifferentContent_IsUsed() { // Act @@ -29,7 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("Hello from runtime-compiled rzc page!", responseBody.Trim()); } - [Fact(Skip = "https://github.com/aspnet/Mvc/issues/8753")] + [Fact] public async Task Rzc_LocalViewWithDifferentContent_IsUsed() { // Act @@ -53,5 +55,89 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("Hello from buildtime-compiled rzc view!", responseBody.Trim()); } + + [Fact] + public async Task RazorViews_AreUpdatedOnChange() + { + // Arrange + var expected1 = "Original content"; + var path = "/Views/UpdateableViews/Index.cshtml"; + + // Act - 1 + var body = await Client.GetStringAsync("/UpdateableViews"); + + // Assert - 1 + Assert.Equal(expected1, body.Trim(), ignoreLineEndingDifferences: true); + + // Act - 2 + await UpdateFile(path, "@GetType().Assembly"); + body = await Client.GetStringAsync("/UpdateableViews"); + + // Assert - 2 + var actual2 = body.Trim(); + Assert.NotEqual(expected1, actual2); + + // Act - 3 + // With all things being the same, expect a cached compilation + body = await Client.GetStringAsync("/UpdateableViews"); + + // Assert - 3 + Assert.Equal(actual2, body.Trim(), ignoreLineEndingDifferences: true); + + // Act - 4 + // Trigger a change in ViewImports + await UpdateFile("/Views/UpdateableViews/_ViewImports.cshtml", "new content"); + body = await Client.GetStringAsync("/UpdateableViews"); + + // Assert - 4 + Assert.NotEqual(actual2, body.Trim()); + } + + [Fact] + public async Task RazorPages_AreUpdatedOnChange() + { + // Arrange + var expected1 = "Original content"; + + // Act - 1 + var body = await Client.GetStringAsync("/UpdateablePage"); + + // Assert - 1 + Assert.Equal(expected1, body.Trim(), ignoreLineEndingDifferences: true); + + // Act - 2 + await UpdateRazorPages(); + await UpdateFile("/Pages/UpdateablePage.cshtml", "@page" + Environment.NewLine + "@GetType().Assembly"); + body = await Client.GetStringAsync("/UpdateablePage"); + + // Assert - 2 + var actual2 = body.Trim(); + Assert.NotEqual(expected1, actual2); + + // Act - 3 + // With all things being unchanged, we should get the cached page. + body = await Client.GetStringAsync("/UpdateablePage"); + + // Assert - 3 + Assert.Equal(actual2, body.Trim(), ignoreLineEndingDifferences: true); + } + + private async Task UpdateFile(string path, string content) + { + var updateContent = new FormUrlEncodedContent(new Dictionary + { + { "path", path }, + { "content", content }, + }); + + var response = await Client.PostAsync($"/UpdateableViews/Update", updateContent); + response.EnsureSuccessStatusCode(); + } + + private async Task UpdateRazorPages() + { + var response = await Client.PostAsync($"/UpdateableViews/UpdateRazorPages", new StringContent(string.Empty)); + response.EnsureSuccessStatusCode(); + } } } diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ViewEngineTests.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ViewEngineTests.cs index 84283fe77a..e2c9f20430 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ViewEngineTests.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ViewEngineTests.cs @@ -252,30 +252,6 @@ ViewWithNestedLayout-Content Assert.Equal(expected, body.Trim(), ignoreLineEndingDifferences: true); } - [Fact(Skip = "https://github.com/aspnet/Mvc/issues/8754")] - public Task RazorViewEngine_RendersViewsFromEmbeddedFileProvider_WhenLookedupByName() - => RazorViewEngine_RendersIndexViewsFromEmbeddedFileProvider("/EmbeddedViews/LookupByName"); - - [Fact(Skip = "https://github.com/aspnet/Mvc/issues/8754")] - public Task RazorViewEngine_RendersViewsFromEmbeddedFileProvider_WhenLookedupByPath() - => RazorViewEngine_RendersIndexViewsFromEmbeddedFileProvider("/EmbeddedViews/LookupByPath"); - - private async Task RazorViewEngine_RendersIndexViewsFromEmbeddedFileProvider(string requestPath) - { - // Arrange - var expected = -@"Hello from EmbeddedShared/_Partial -Hello from Shared/_EmbeddedPartial -Tag Helper Link -"; - - // Act - var body = await Client.GetStringAsync(requestPath); - - // Assert - Assert.Equal(expected, body.Trim(), ignoreLineEndingDifferences: true); - } - [Fact] public async Task LayoutValueIsPassedBetweenNestedViewStarts() { diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/CSharpCompilerTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/CSharpCompilerTest.cs new file mode 100644 index 0000000000..3f87554209 --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/CSharpCompilerTest.cs @@ -0,0 +1,319 @@ +// 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 Microsoft.AspNetCore.Hosting; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.Text; +using Moq; +using Xunit; +using DependencyContextCompilationOptions = Microsoft.Extensions.DependencyModel.CompilationOptions; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + public class CSharpCompilerTest + { + private readonly RazorReferenceManager ReferenceManager = new TestRazorReferenceManager(); + + [Theory] + [InlineData(null)] + [InlineData("")] + public void GetCompilationOptions_ReturnsDefaultOptionsIfApplicationNameIsNullOrEmpty(string name) + { + // Arrange + var hostingEnvironment = Mock.Of(e => e.ApplicationName == name); + var compiler = new CSharpCompiler(ReferenceManager, hostingEnvironment); + + // Act + var options = compiler.GetDependencyContextCompilationOptions(); + + // Assert + Assert.Same(DependencyContextCompilationOptions.Default, options); + } + + [Fact] + public void GetCompilationOptions_ReturnsDefaultOptionsIfApplicationDoesNotHaveDependencyContext() + { + // Arrange + var hostingEnvironment = Mock.Of(); + var compiler = new CSharpCompiler(ReferenceManager, hostingEnvironment); + + // Act + var options = compiler.GetDependencyContextCompilationOptions(); + + // Assert + Assert.Same(DependencyContextCompilationOptions.Default, options); + } + + [Theory] + [InlineData("Development", OptimizationLevel.Debug)] + [InlineData("Staging", OptimizationLevel.Release)] + [InlineData("Production", OptimizationLevel.Release)] + public void Constructor_SetsOptimizationLevelBasedOnEnvironment( + string environment, + OptimizationLevel expected) + { + // Arrange + var options = new RazorViewEngineOptions(); + var hostingEnvironment = new Mock(); + hostingEnvironment.SetupGet(e => e.EnvironmentName) + .Returns(environment); + var compiler = new CSharpCompiler(ReferenceManager, hostingEnvironment.Object); + + // Act & Assert + var compilationOptions = compiler.CSharpCompilationOptions; + Assert.Equal(expected, compilationOptions.OptimizationLevel); + } + + [Theory] + [InlineData("Development", "DEBUG")] + [InlineData("Staging", "RELEASE")] + [InlineData("Production", "RELEASE")] + public void EnsureOptions_SetsPreprocessorSymbols(string environment, string expectedConfiguration) + { + // Arrange + var options = new RazorViewEngineOptions(); + var hostingEnvironment = new Mock(); + hostingEnvironment.SetupGet(e => e.EnvironmentName) + .Returns(environment); + var compiler = new CSharpCompiler(ReferenceManager, hostingEnvironment.Object); + + // Act & Assert + var parseOptions = compiler.ParseOptions; + Assert.Equal(new[] { expectedConfiguration }, parseOptions.PreprocessorSymbolNames); + } + + [Fact] + public void EnsureOptions_ConfiguresDefaultCompilationOptions() + { + // Arrange + var hostingEnvironment = Mock.Of(h => h.EnvironmentName == "Development"); + var compiler = new CSharpCompiler(ReferenceManager, hostingEnvironment); + + // Act & Assert + var compilationOptions = compiler.CSharpCompilationOptions; + Assert.False(compilationOptions.AllowUnsafe); + Assert.Equal(ReportDiagnostic.Default, compilationOptions.GeneralDiagnosticOption); + Assert.Equal(OptimizationLevel.Debug, compilationOptions.OptimizationLevel); + Assert.Collection(compilationOptions.SpecificDiagnosticOptions.OrderBy(d => d.Key), + item => + { + Assert.Equal("CS1701", item.Key); + Assert.Equal(ReportDiagnostic.Suppress, item.Value); + }, + item => + { + Assert.Equal("CS1702", item.Key); + Assert.Equal(ReportDiagnostic.Suppress, item.Value); + }, + item => + { + Assert.Equal("CS1705", item.Key); + Assert.Equal(ReportDiagnostic.Suppress, item.Value); + }); + } + + [Fact] + public void EnsureOptions_ConfiguresDefaultParseOptions() + { + // Arrange + var hostingEnvironment = Mock.Of(h => h.EnvironmentName == "Development"); + var compiler = new CSharpCompiler(ReferenceManager, hostingEnvironment); + + // Act & Assert + var parseOptions = compiler.ParseOptions; + Assert.Equal(LanguageVersion.CSharp7, parseOptions.LanguageVersion); + Assert.Equal(new[] { "DEBUG" }, parseOptions.PreprocessorSymbolNames); + } + + [Fact] + public void Constructor_ConfiguresPreprocessorSymbolNames() + { + // Arrange + var hostingEnvironment = Mock.Of(); + var dependencyContextOptions = GetDependencyContextCompilationOptions("SOME_TEST_DEFINE"); + + var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions); + + // Act & Assert + var parseOptions = compiler.ParseOptions; + Assert.Contains("SOME_TEST_DEFINE", parseOptions.PreprocessorSymbolNames); + } + + [Fact] + public void Constructor_ConfiguresLanguageVersion() + { + // Arrange + var dependencyContextOptions = GetDependencyContextCompilationOptions(languageVersion: "7.1"); + var hostingEnvironment = Mock.Of(); + + var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions); + + // Act & Assert + var compilationOptions = compiler.ParseOptions; + Assert.Equal(LanguageVersion.CSharp7_1, compilationOptions.LanguageVersion); + } + + + [Fact] + public void EmitOptions_ReadsDebugTypeFromDependencyContext() + { + // Arrange + var dependencyContextOptions = GetDependencyContextCompilationOptions(debugType: "portable"); + var hostingEnvironment = Mock.Of(); + + var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions); + + // Act & Assert + var emitOptions = compiler.EmitOptions; + Assert.Equal(DebugInformationFormat.PortablePdb, emitOptions.DebugInformationFormat); + Assert.True(compiler.EmitPdb); + } + + [Fact] + public void EmitOptions_SetsDebugInformationFormatToPortable_WhenDebugTypeIsEmbedded() + { + // Arrange + var dependencyContextOptions = GetDependencyContextCompilationOptions(debugType: "embedded"); + var hostingEnvironment = Mock.Of(); + + var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions); + + // Act & Assert + var emitOptions = compiler.EmitOptions; + Assert.Equal(DebugInformationFormat.PortablePdb, emitOptions.DebugInformationFormat); + Assert.True(compiler.EmitPdb); + } + + [Fact] + public void EmitOptions_DoesNotSetEmitPdb_IfDebugTypeIsNone() + { + // Arrange + var dependencyContextOptions = GetDependencyContextCompilationOptions(debugType: "none"); + var hostingEnvironment = Mock.Of(); + + var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions); + + // Act & Assert + Assert.False(compiler.EmitPdb); + } + + [Fact] + public void Constructor_ConfiguresAllowUnsafe() + { + // Arrange + var dependencyContextOptions = GetDependencyContextCompilationOptions(allowUnsafe: true); + var hostingEnvironment = Mock.Of(); + + var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions); + + // Act & Assert + var compilationOptions = compiler.CSharpCompilationOptions; + Assert.True(compilationOptions.AllowUnsafe); + } + + [Fact] + public void Constructor_SetsDiagnosticOption() + { + // Arrange + var dependencyContextOptions = GetDependencyContextCompilationOptions(warningsAsErrors: true); + var hostingEnvironment = Mock.Of(); + + var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions); + + // Act & Assert + var compilationOptions = compiler.CSharpCompilationOptions; + Assert.Equal(ReportDiagnostic.Error, compilationOptions.GeneralDiagnosticOption); + } + + [Fact] + public void Constructor_SetsOptimizationLevel() + { + // Arrange + var dependencyContextOptions = GetDependencyContextCompilationOptions(optimize: true); + var hostingEnvironment = Mock.Of(); + + var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions); + + // Act & Assert + var compilationOptions = compiler.CSharpCompilationOptions; + Assert.Equal(OptimizationLevel.Release, compilationOptions.OptimizationLevel); + } + + [Fact] + public void Constructor_SetsDefines() + { + // Arrange + var dependencyContextOptions = GetDependencyContextCompilationOptions("MyDefine"); + var hostingEnvironment = Mock.Of(); + var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions); + + // Act & Assert + var parseOptions = compiler.ParseOptions; + Assert.Equal(new[] { "MyDefine", "RELEASE" }, parseOptions.PreprocessorSymbolNames); + } + + [Fact] + public void Compile_UsesApplicationsCompilationSettings_ForParsingAndCompilation() + { + // Arrange + var content = "public class Test {}"; + var define = "MY_CUSTOM_DEFINE"; + var dependencyContextOptions = GetDependencyContextCompilationOptions(define); + var hostingEnvironment = Mock.Of(); + var compiler = new TestCSharpCompiler(ReferenceManager, hostingEnvironment, dependencyContextOptions); + + // Act + var syntaxTree = compiler.CreateSyntaxTree(SourceText.From(content)); + + // Assert + Assert.Contains(define, syntaxTree.Options.PreprocessorSymbolNames); + } + + private static DependencyContextCompilationOptions GetDependencyContextCompilationOptions( + string define = null, + string languageVersion = null, + string platform = null, + bool? allowUnsafe = null, + bool? warningsAsErrors = null, + bool? optimize = null, + string keyFile = null, + bool? delaySign = null, + bool? publicSign = null, + string debugType = null) + { + return new DependencyContextCompilationOptions( + new[] { define }, + languageVersion, + platform, + allowUnsafe, + warningsAsErrors, + optimize, + keyFile, + delaySign, + publicSign, + debugType, + emitEntryPoint: null, + generateXmlDocumentation: null); + } + + private class TestCSharpCompiler : CSharpCompiler + { + private readonly DependencyContextCompilationOptions _options; + + public TestCSharpCompiler( + RazorReferenceManager referenceManager, + IHostingEnvironment hostingEnvironment, + DependencyContextCompilationOptions options) + : base(referenceManager, hostingEnvironment) + { + _options = options; + } + + protected internal override DependencyContextCompilationOptions GetDependencyContextCompilationOptions() + => _options; + } + } +} diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/ChecksumValidatorTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/ChecksumValidatorTest.cs new file mode 100644 index 0000000000..4a37fee2bf --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/ChecksumValidatorTest.cs @@ -0,0 +1,190 @@ +// 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.Hosting; +using Microsoft.AspNetCore.Razor.Language; +using Xunit; +using static Microsoft.AspNetCore.Razor.Hosting.TestRazorCompiledItem; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + public class ChecksumValidatorTest + { + private VirtualRazorProjectFileSystem ProjectFileSystem { get; } = new VirtualRazorProjectFileSystem(); + + [Fact] + public void IsRecompilationSupported_NoChecksumAttributes_ReturnsFalse() + { + // Arrange + var item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Views/Home/Index.cstml", new object[] { }); + + // Act + var result = ChecksumValidator.IsRecompilationSupported(item); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsRecompilationSupported_NoPrimaryChecksumAttribute_ReturnsFalse() + { + // Arrange + var item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Views/Home/Index.cstml", new object[] + { + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some import"), "/Views/Home/_ViewImports.cstml"), + }); + + // Act + var result = ChecksumValidator.IsRecompilationSupported(item); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsRecompilationSupported_HasPrimaryChecksumAttribute_ReturnsTrue() + { + // Arrange + var item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Views/Home/Index.cstml", new object[] + { + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some import"), "/Views/Home/_ViewImports.cstml"), + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), "/Views/Home/Index.cstml"), + }); + + // Act + var result = ChecksumValidator.IsRecompilationSupported(item); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsItemValid_NoChecksumAttributes_ReturnsTrue() + { + // Arrange + var item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Views/Home/Index.cstml", new object[] { }); + + // Act + var result = ChecksumValidator.IsItemValid(ProjectFileSystem, item); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsItemValid_NoPrimaryChecksumAttribute_ReturnsTrue() + { + // Arrange + var item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Views/Home/Index.cstml", new object[] + { + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some import"), "/Views/Home/_ViewImports.cstml"), + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), "/Views/Home/About.cstml"), + }); + + // Act + var result = ChecksumValidator.IsItemValid(ProjectFileSystem, item); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsItemValid_PrimaryFileDoesNotExist_ReturnsTrue() + { + // Arrange + var item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Views/Home/Index.cstml", new object[] + { + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some import"), "/Views/Home/_ViewImports.cstml"), + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), "/Views/Home/Index.cstml"), + }); + + ProjectFileSystem.Add(new TestRazorProjectItem("/Views/Home/_ViewImports.cstml", "dkdkfkdf")); // This will be ignored + + // Act + var result = ChecksumValidator.IsItemValid(ProjectFileSystem, item); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsItemValid_PrimaryFileExistsButDoesNotMatch_ReturnsFalse() + { + // Arrange + var item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Views/Home/Index.cstml", new object[] + { + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some import"), "/Views/Home/_ViewImports.cstml"), + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), "/Views/Home/Index.cstml"), + }); + + ProjectFileSystem.Add(new TestRazorProjectItem("/Views/Home/Index.cstml", "other content")); + + // Act + var result = ChecksumValidator.IsItemValid(ProjectFileSystem, item); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsItemValid_ImportFileDoesNotExist_ReturnsFalse() + { + // Arrange + var item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Views/Home/Index.cstml", new object[] + { + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some import"), "/Views/Home/_ViewImports.cstml"), + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), "/Views/Home/Index.cstml"), + }); + + ProjectFileSystem.Add(new TestRazorProjectItem("/Views/Home/Index.cstml", "some content")); + + // Act + var result = ChecksumValidator.IsItemValid(ProjectFileSystem, item); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsItemValid_ImportFileExistsButDoesNotMatch_ReturnsFalse() + { + // Arrange + var item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Views/Home/Index.cstml", new object[] + { + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some import"), "/Views/Home/_ViewImports.cstml"), + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), "/Views/Home/Index.cstml"), + }); + + ProjectFileSystem.Add(new TestRazorProjectItem("/Views/Home/Index.cstml", "some content")); + ProjectFileSystem.Add(new TestRazorProjectItem("/Views/Home/_ViewImports.cstml", "some other import")); + + // Act + var result = ChecksumValidator.IsItemValid(ProjectFileSystem, item); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsItemValid_AllFilesMatch_ReturnsTrue() + { + // Arrange + var item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Views/Home/Index.cstml", new object[] + { + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some other import"), "/Views/_ViewImports.cstml"), + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some import"), "/Views/Home/_ViewImports.cstml"), + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), "/Views/Home/Index.cstml"), + }); + + ProjectFileSystem.Add(new TestRazorProjectItem("/Views/Home/Index.cstml", "some content")); + ProjectFileSystem.Add(new TestRazorProjectItem("/Views/Home/_ViewImports.cstml", "some import")); + ProjectFileSystem.Add(new TestRazorProjectItem("/Views/_ViewImports.cstml", "some other import")); + + // Act + var result = ChecksumValidator.IsItemValid(ProjectFileSystem, item); + + // Assert + Assert.True(result); + } + } +} diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/CompilerFailedExceptionFactoryTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/CompilerFailedExceptionFactoryTest.cs new file mode 100644 index 0000000000..6bba1c26e1 --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/CompilerFailedExceptionFactoryTest.cs @@ -0,0 +1,355 @@ +// 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.IO; +using System.Linq; +using System.Text; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + public class CompilerFailedExceptionFactoryTest + { + [Fact] + public void GetCompilationFailedResult_ReadsRazorErrorsFromPage() + { + // Arrange + var viewPath = "/Views/Home/Index.cshtml"; + + var fileSystem = new VirtualRazorProjectFileSystem(); + var projectItem = new TestRazorProjectItem(viewPath, ""); + + var razorEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem).Engine; + var codeDocument = GetCodeDocument(projectItem); + + // Act + razorEngine.Process(codeDocument); + var csharpDocument = codeDocument.GetCSharpDocument(); + var compilationResult = CompilationFailedExceptionFactory.Create(codeDocument, csharpDocument.Diagnostics); + + // Assert + var failure = Assert.Single(compilationResult.CompilationFailures); + Assert.Equal(viewPath, failure.SourceFilePath); + Assert.Collection(failure.Messages, + message => Assert.StartsWith( + @"Unterminated string literal.", + message.Message), + message => Assert.StartsWith( + @"The explicit expression block is missing a closing "")"" character.", + message.Message)); + } + + [Fact] + public void GetCompilationFailedResult_WithMissingReferences() + { + // Arrange + var expected = "One or more compilation references may be missing. If you're seeing this in a published application, set 'CopyRefAssembliesToPublishDirectory' to true in your project file to ensure files in the refs directory are published."; + var compilation = CSharpCompilation.Create("Test", options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + var syntaxTree = CSharpSyntaxTree.ParseText("@class Test { public string Test { get; set; } }"); + compilation = compilation.AddSyntaxTrees(syntaxTree); + var emitResult = compilation.Emit(new MemoryStream()); + + // Act + var exception = CompilationFailedExceptionFactory.Create( + RazorCodeDocument.Create(RazorSourceDocument.Create("Test", "Index.cshtml"), Enumerable.Empty()), + syntaxTree.ToString(), + "Test", + emitResult.Diagnostics); + + // Assert + Assert.Collection( + exception.CompilationFailures, + failure => Assert.Equal(expected, failure.FailureSummary)); + } + + [Fact] + public void GetCompilationFailedResult_UsesPhysicalPath() + { + // Arrange + var viewPath = "/Views/Home/Index.cshtml"; + var physicalPath = @"x:\myapp\views\home\index.cshtml"; + + var projectItem = new TestRazorProjectItem(viewPath, "", physicalPath: physicalPath); + + var codeDocument = GetCodeDocument(projectItem); + var csharpDocument = codeDocument.GetCSharpDocument(); + + // Act + var compilationResult = CompilationFailedExceptionFactory.Create(codeDocument, csharpDocument.Diagnostics); + + // Assert + var failure = Assert.Single(compilationResult.CompilationFailures); + Assert.Equal(physicalPath, failure.SourceFilePath); + } + + [Fact] + public void GetCompilationFailedResult_ReadsContentFromSourceDocuments() + { + // Arrange + var viewPath = "/Views/Home/Index.cshtml"; + var fileContent = +@" +@if (User.IsAdmin) +{ + +} +"; + + var projectItem = new TestRazorProjectItem(viewPath, fileContent); + var codeDocument = GetCodeDocument(projectItem); + var csharpDocument = codeDocument.GetCSharpDocument(); + + // Act + var compilationResult = CompilationFailedExceptionFactory.Create(codeDocument, csharpDocument.Diagnostics); + + // Assert + var failure = Assert.Single(compilationResult.CompilationFailures); + Assert.Equal(fileContent, failure.SourceFileContent); + } + + [Fact] + public void GetCompilationFailedResult_ReadsContentFromImports() + { + // Arrange + var viewPath = "/Views/Home/Index.cshtml"; + var importsPath = "/Views/_MyImports.cshtml"; + var fileContent = "@ "; + var importsContent = "@(abc"; + + var projectItem = new TestRazorProjectItem(viewPath, fileContent); + var importsItem = new TestRazorProjectItem(importsPath, importsContent); + var codeDocument = GetCodeDocument(projectItem, importsItem); + var csharpDocument = codeDocument.GetCSharpDocument(); + + // Act + var compilationResult = CompilationFailedExceptionFactory.Create(codeDocument, csharpDocument.Diagnostics); + + // Assert + Assert.Collection( + compilationResult.CompilationFailures, + failure => + { + Assert.Equal(viewPath, failure.SourceFilePath); + Assert.Collection(failure.Messages, + message => + { + Assert.Equal(@"A space or line break was encountered after the ""@"" character. Only valid identifiers, keywords, comments, ""("" and ""{"" are valid at the start of a code block and they must occur immediately following ""@"" with no space in between.", + message.Message); + }); + }, + failure => + { + Assert.Equal(importsPath, failure.SourceFilePath); + Assert.Collection(failure.Messages, + message => + { + Assert.Equal(@"The explicit expression block is missing a closing "")"" character. Make sure you have a matching "")"" character for all the ""("" characters within this block, and that none of the "")"" characters are being interpreted as markup.", + message.Message); + }); + }); + } + + [Fact] + public void GetCompilationFailedResult_GroupsMessages() + { + // Arrange + var viewPath = "views/index.razor"; + var viewImportsPath = "views/global.import.cshtml"; + var codeDocument = RazorCodeDocument.Create( + Create(viewPath, "View Content"), + new[] { Create(viewImportsPath, "Global Import Content") }); + var diagnostics = new[] + { + GetRazorDiagnostic("message-1", new SourceLocation(1, 2, 17), length: 1), + GetRazorDiagnostic("message-2", new SourceLocation(viewPath, 1, 4, 6), length: 7), + GetRazorDiagnostic("message-3", SourceLocation.Undefined, length: -1), + GetRazorDiagnostic("message-4", new SourceLocation(viewImportsPath, 1, 3, 8), length: 4), + }; + + // Act + var result = CompilationFailedExceptionFactory.Create(codeDocument, diagnostics); + + // Assert + Assert.Collection(result.CompilationFailures, + failure => + { + Assert.Equal(viewPath, failure.SourceFilePath); + Assert.Equal("View Content", failure.SourceFileContent); + Assert.Collection(failure.Messages, + message => + { + Assert.Equal(diagnostics[0].GetMessage(), message.Message); + Assert.Equal(viewPath, message.SourceFilePath); + Assert.Equal(3, message.StartLine); + Assert.Equal(17, message.StartColumn); + Assert.Equal(3, message.EndLine); + Assert.Equal(18, message.EndColumn); + }, + message => + { + Assert.Equal(diagnostics[1].GetMessage(), message.Message); + Assert.Equal(viewPath, message.SourceFilePath); + Assert.Equal(5, message.StartLine); + Assert.Equal(6, message.StartColumn); + Assert.Equal(5, message.EndLine); + Assert.Equal(13, message.EndColumn); + }, + message => + { + Assert.Equal(diagnostics[2].GetMessage(), message.Message); + Assert.Equal(viewPath, message.SourceFilePath); + Assert.Equal(0, message.StartLine); + Assert.Equal(-1, message.StartColumn); + Assert.Equal(0, message.EndLine); + Assert.Equal(-2, message.EndColumn); + }); + }, + failure => + { + Assert.Equal(viewImportsPath, failure.SourceFilePath); + Assert.Equal("Global Import Content", failure.SourceFileContent); + Assert.Collection(failure.Messages, + message => + { + Assert.Equal(diagnostics[3].GetMessage(), message.Message); + Assert.Equal(viewImportsPath, message.SourceFilePath); + Assert.Equal(4, message.StartLine); + Assert.Equal(8, message.StartColumn); + Assert.Equal(4, message.EndLine); + Assert.Equal(12, message.EndColumn); + }); + }); + } + + [Fact] + public void GetCompilationFailedResult_ReturnsCompilationResult_WithGroupedMessages() + { + // Arrange + var viewPath = "Views/Home/Index"; + var generatedCodeFileName = "Generated Code"; + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("view-content", viewPath)); + var assemblyName = "random-assembly-name"; + + var diagnostics = new[] + { + Diagnostic.Create( + GetRoslynDiagnostic("message-1"), + Location.Create( + viewPath, + new TextSpan(10, 5), + new LinePositionSpan(new LinePosition(10, 1), new LinePosition(10, 2)))), + Diagnostic.Create( + GetRoslynDiagnostic("message-2"), + Location.Create( + assemblyName, + new TextSpan(1, 6), + new LinePositionSpan(new LinePosition(1, 2), new LinePosition(3, 4)))), + Diagnostic.Create( + GetRoslynDiagnostic("message-3"), + Location.Create( + viewPath, + new TextSpan(40, 50), + new LinePositionSpan(new LinePosition(30, 5), new LinePosition(40, 12)))), + }; + + // Act + var compilationResult = CompilationFailedExceptionFactory.Create( + codeDocument, + "compilation-content", + assemblyName, + diagnostics); + + // Assert + Assert.Collection(compilationResult.CompilationFailures, + failure => + { + Assert.Equal(viewPath, failure.SourceFilePath); + Assert.Equal("view-content", failure.SourceFileContent); + Assert.Collection(failure.Messages, + message => + { + Assert.Equal("message-1", message.Message); + Assert.Equal(viewPath, message.SourceFilePath); + Assert.Equal(11, message.StartLine); + Assert.Equal(2, message.StartColumn); + Assert.Equal(11, message.EndLine); + Assert.Equal(3, message.EndColumn); + }, + message => + { + Assert.Equal("message-3", message.Message); + Assert.Equal(viewPath, message.SourceFilePath); + Assert.Equal(31, message.StartLine); + Assert.Equal(6, message.StartColumn); + Assert.Equal(41, message.EndLine); + Assert.Equal(13, message.EndColumn); + }); + }, + failure => + { + Assert.Equal(generatedCodeFileName, failure.SourceFilePath); + Assert.Equal("compilation-content", failure.SourceFileContent); + Assert.Collection(failure.Messages, + message => + { + Assert.Equal("message-2", message.Message); + Assert.Equal(assemblyName, message.SourceFilePath); + Assert.Equal(2, message.StartLine); + Assert.Equal(3, message.StartColumn); + Assert.Equal(4, message.EndLine); + Assert.Equal(5, message.EndColumn); + }); + }); + } + + private static RazorSourceDocument Create(string path, string template) + { + var stream = new MemoryStream(Encoding.UTF8.GetBytes(template)); + return RazorSourceDocument.ReadFrom(stream, path); + } + + private static RazorDiagnostic GetRazorDiagnostic(string message, SourceLocation sourceLocation, int length) + { + var diagnosticDescriptor = new RazorDiagnosticDescriptor("test-id", () => message, RazorDiagnosticSeverity.Error); + var sourceSpan = new SourceSpan(sourceLocation, length); + + return RazorDiagnostic.Create(diagnosticDescriptor, sourceSpan); + } + + private static DiagnosticDescriptor GetRoslynDiagnostic(string messageFormat) + { + return new DiagnosticDescriptor( + id: "someid", + title: "sometitle", + messageFormat: messageFormat, + category: "some-category", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + } + + private static RazorCodeDocument GetCodeDocument(TestRazorProjectItem projectItem, TestRazorProjectItem imports = null) + { + var sourceDocument = RazorSourceDocument.ReadFrom(projectItem); + var fileSystem = new VirtualRazorProjectFileSystem(); + fileSystem.Add(projectItem); + + var codeDocument = RazorCodeDocument.Create(sourceDocument); + + if (imports != null) + { + fileSystem.Add(imports); + codeDocument = RazorCodeDocument.Create(sourceDocument, new[] { RazorSourceDocument.ReadFrom(imports) }); + } + + var razorEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem).Engine; + + razorEngine.Process(codeDocument); + return codeDocument; + } + + } +} diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/DependencyInjection/RazorRuntimeCompilationMvcCoreBuilderExtensionsTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/DependencyInjection/RazorRuntimeCompilationMvcCoreBuilderExtensionsTest.cs new file mode 100644 index 0000000000..f74cfb8bf0 --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/DependencyInjection/RazorRuntimeCompilationMvcCoreBuilderExtensionsTest.cs @@ -0,0 +1,27 @@ +// 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.Mvc.Razor.RuntimeCompilation; +using Xunit; + +namespace Microsoft.Extensions.DependencyInjection +{ + public class RazorRuntimeCompilationMvcCoreBuilderExtensionsTest + { + [Fact] + public void AddServices_ReplacesRazorViewCompiler() + { + // Arrange + var services = new ServiceCollection() + .AddSingleton(); + + // Act + RazorRuntimeCompilationMvcCoreBuilderExtensions.AddServices(services); + + // Assert + var serviceDescriptor = Assert.Single(services, service => service.ServiceType == typeof(IViewCompilerProvider)); + Assert.Equal(typeof(RuntimeViewCompilerProvider), serviceDescriptor.ImplementationType); + } + } +} diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/FileProviderRazorProjectFileSystemTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/FileProviderRazorProjectFileSystemTest.cs new file mode 100644 index 0000000000..94abf8068a --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/FileProviderRazorProjectFileSystemTest.cs @@ -0,0 +1,260 @@ +// 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.IO; +using System.Linq; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + public class FileProviderRazorProjectFileSystemTest + { + [Fact] + public void EnumerateFiles_ReturnsEmptySequenceIfNoCshtmlFilesArePresent() + { + // Arrange + var fileProvider = new TestFileProvider("BasePath"); + var file1 = fileProvider.AddFile("/File1.txt", "content"); + var file2 = fileProvider.AddFile("/File2.js", "content"); + fileProvider.AddDirectoryContent("/", new IFileInfo[] { file1, file2 }); + + var fileSystem = GetRazorProjectFileSystem(fileProvider); + + // Act + var razorFiles = fileSystem.EnumerateItems("/"); + + // Assert + Assert.Empty(razorFiles); + } + + [Fact] + public void EnumerateFiles_ReturnsCshtmlFiles() + { + // Arrange + var fileProvider = new TestFileProvider("BasePath"); + var file1 = fileProvider.AddFile("/File1.cshtml", "content"); + var file2 = fileProvider.AddFile("/File2.js", "content"); + var file3 = fileProvider.AddFile("/File3.cshtml", "content"); + fileProvider.AddDirectoryContent("/", new IFileInfo[] { file1, file2, file3 }); + + var fileSystem = GetRazorProjectFileSystem(fileProvider); + + // Act + var razorFiles = fileSystem.EnumerateItems("/"); + + // Assert + Assert.Collection( + razorFiles.OrderBy(f => f.FilePath), + file => + { + Assert.Equal("/File1.cshtml", file.FilePath); + Assert.Equal("/", file.BasePath); + Assert.Equal(Path.Combine("BasePath", "File1.cshtml"), file.PhysicalPath); + Assert.Equal("File1.cshtml", file.RelativePhysicalPath); + }, + file => + { + Assert.Equal("/File3.cshtml", file.FilePath); + Assert.Equal("/", file.BasePath); + Assert.Equal(Path.Combine("BasePath", "File3.cshtml"), file.PhysicalPath); + Assert.Equal("File3.cshtml", file.RelativePhysicalPath); + }); + } + + [Fact] + public void EnumerateFiles_IteratesOverAllCshtmlUnderRoot() + { + // Arrange + var fileProvider = new TestFileProvider("BasePath"); + var directory1 = new TestDirectoryFileInfo + { + Name = "Level1-Dir1", + }; + var file1 = fileProvider.AddFile("File1.cshtml", "content"); + var directory2 = new TestDirectoryFileInfo + { + Name = "Level1-Dir2", + }; + fileProvider.AddDirectoryContent("/", new IFileInfo[] { directory1, file1, directory2 }); + + var file2 = fileProvider.AddFile("/Level1-Dir1/File2.cshtml", "content"); + var file3 = fileProvider.AddFile("/Level1-Dir1/File3.cshtml", "content"); + var file4 = fileProvider.AddFile("/Level1-Dir1/File4.txt", "content"); + var directory3 = new TestDirectoryFileInfo + { + Name = "Level2-Dir1" + }; + fileProvider.AddDirectoryContent("/Level1-Dir1", new IFileInfo[] { file2, directory3, file3, file4 }); + var file5 = fileProvider.AddFile(Path.Combine("Level1-Dir2", "File5.cshtml"), "content"); + fileProvider.AddDirectoryContent("/Level1-Dir2", new IFileInfo[] { file5 }); + fileProvider.AddDirectoryContent("/Level1/Level2", new IFileInfo[0]); + + var fileSystem = GetRazorProjectFileSystem(fileProvider); + + // Act + var razorFiles = fileSystem.EnumerateItems("/"); + + // Assert + Assert.Collection(razorFiles.OrderBy(f => f.FilePath), + file => + { + Assert.Equal("/File1.cshtml", file.FilePath); + Assert.Equal("/", file.BasePath); + Assert.Equal(Path.Combine("BasePath", "File1.cshtml"), file.PhysicalPath); + Assert.Equal("File1.cshtml", file.RelativePhysicalPath); + }, + file => + { + Assert.Equal("/Level1-Dir1/File2.cshtml", file.FilePath); + Assert.Equal("/", file.BasePath); + Assert.Equal(Path.Combine("BasePath", "Level1-Dir1", "File2.cshtml"), file.PhysicalPath); + Assert.Equal(Path.Combine("Level1-Dir1", "File2.cshtml"), file.RelativePhysicalPath); + }, + file => + { + Assert.Equal("/Level1-Dir1/File3.cshtml", file.FilePath); + Assert.Equal("/", file.BasePath); + Assert.Equal(Path.Combine("BasePath", "Level1-Dir1", "File3.cshtml"), file.PhysicalPath); + Assert.Equal(Path.Combine("Level1-Dir1", "File3.cshtml"), file.RelativePhysicalPath); + }, + file => + { + Assert.Equal("/Level1-Dir2/File5.cshtml", file.FilePath); + Assert.Equal("/", file.BasePath); + Assert.Equal(Path.Combine("BasePath", "Level1-Dir2", "File5.cshtml"), file.PhysicalPath); + Assert.Equal(Path.Combine("Level1-Dir2", "File5.cshtml"), file.RelativePhysicalPath); + }); + } + + [Fact] + public void EnumerateFiles_IteratesOverAllCshtmlUnderPath() + { + // Arrange + var fileProvider = new TestFileProvider("BasePath"); + var directory1 = new TestDirectoryFileInfo + { + Name = "Level1-Dir1", + }; + var file1 = fileProvider.AddFile("/File1.cshtml", "content"); + var directory2 = new TestDirectoryFileInfo + { + Name = "Level1-Dir2", + }; + fileProvider.AddDirectoryContent("/", new IFileInfo[] { directory1, file1, directory2 }); + + var file2 = fileProvider.AddFile("/Level1-Dir1/File2.cshtml", "content"); + var file3 = fileProvider.AddFile("/Level1-Dir1/File3.cshtml", "content"); + var file4 = fileProvider.AddFile("/Level1-Dir1/File4.txt", "content"); + var directory3 = new TestDirectoryFileInfo + { + Name = "Level2-Dir1" + }; + fileProvider.AddDirectoryContent("/Level1-Dir1", new IFileInfo[] { file2, directory3, file3, file4 }); + var file5 = fileProvider.AddFile(Path.Combine("Level1-Dir2", "File5.cshtml"), "content"); + fileProvider.AddDirectoryContent("/Level1-Dir2", new IFileInfo[] { file5 }); + fileProvider.AddDirectoryContent("/Level1/Level2", new IFileInfo[0]); + + var fileSystem = GetRazorProjectFileSystem(fileProvider); + + // Act + var razorFiles = fileSystem.EnumerateItems("/Level1-Dir1"); + + // Assert + Assert.Collection(razorFiles.OrderBy(f => f.FilePath), + file => + { + Assert.Equal("/File2.cshtml", file.FilePath); + Assert.Equal("/Level1-Dir1", file.BasePath); + Assert.Equal(Path.Combine("BasePath", "Level1-Dir1", "File2.cshtml"), file.PhysicalPath); + Assert.Equal(Path.Combine("Level1-Dir1", "File2.cshtml"), file.RelativePhysicalPath); + }, + file => + { + Assert.Equal("/File3.cshtml", file.FilePath); + Assert.Equal("/Level1-Dir1", file.BasePath); + Assert.Equal(Path.Combine("BasePath", "Level1-Dir1", "File3.cshtml"), file.PhysicalPath); + Assert.Equal(Path.Combine("Level1-Dir1", "File3.cshtml"), file.RelativePhysicalPath); + }); + } + + [Fact] + public void GetItem_ReturnsFileFromDisk() + { + var fileProvider = new TestFileProvider("BasePath"); + var file1 = fileProvider.AddFile("/File1.cshtml", "content"); + var file2 = fileProvider.AddFile("/File2.js", "content"); + var file3 = fileProvider.AddFile("/File3.cshtml", "content"); + fileProvider.AddDirectoryContent("/", new IFileInfo[] { file1, file2, file3 }); + + var fileSystem = GetRazorProjectFileSystem(fileProvider); + + // Act + var item = fileSystem.GetItem("/File3.cshtml"); + + // Assert + Assert.True(item.Exists); + Assert.Equal("/File3.cshtml", item.FilePath); + Assert.Equal(string.Empty, item.BasePath); + Assert.Equal(Path.Combine("BasePath", "File3.cshtml"), item.PhysicalPath); + Assert.Equal("File3.cshtml", item.RelativePhysicalPath); + } + + [Fact] + public void GetItem_PhysicalPathDoesNotStartWithContentRoot_ReturnsNull() + { + var fileProvider = new TestFileProvider("BasePath2"); + var file1 = fileProvider.AddFile("/File1.cshtml", "content"); + var file2 = fileProvider.AddFile("/File2.js", "content"); + var file3 = fileProvider.AddFile("/File3.cshtml", "content"); + fileProvider.AddDirectoryContent("/", new IFileInfo[] { file1, file2, file3 }); + + var fileSystem = GetRazorProjectFileSystem(fileProvider); + + // Act + var item = fileSystem.GetItem("/File3.cshtml"); + + // Assert + Assert.True(item.Exists); + Assert.Equal("/File3.cshtml", item.FilePath); + Assert.Equal(string.Empty, item.BasePath); + Assert.Equal(Path.Combine("BasePath2", "File3.cshtml"), item.PhysicalPath); + Assert.Null(item.RelativePhysicalPath); + } + + [Fact] + public void GetItem_ReturnsNotFoundResult() + { + // Arrange + var fileProvider = new TestFileProvider("BasePath"); + var file = fileProvider.AddFile("/SomeFile.cshtml", "content"); + fileProvider.AddDirectoryContent("/", new IFileInfo[] { file }); + + var fileSystem = GetRazorProjectFileSystem(fileProvider); + + // Act + var item = fileSystem.GetItem("/NotFound.cshtml"); + + // Assert + Assert.False(item.Exists); + } + + private static FileProviderRazorProjectFileSystem GetRazorProjectFileSystem( + TestFileProvider fileProvider, + string contentRootPath = "BasePath") + { + var options = Options.Create(new MvcRazorRuntimeCompilationOptions + { + FileProviders = { fileProvider } + }); + var compilationFileProvider = new RuntimeCompilationFileProvider(options); + var fileSystem = new FileProviderRazorProjectFileSystem( + compilationFileProvider, + Mock.Of(e => e.ContentRootPath == contentRootPath)); + return fileSystem; + } + } +} \ No newline at end of file diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test.csproj b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test.csproj new file mode 100644 index 0000000000..70b0187a85 --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.0 + true + + + + + + + + diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/RazorReferenceManagerTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/RazorReferenceManagerTest.cs new file mode 100644 index 0000000000..e28e974162 --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/RazorReferenceManagerTest.cs @@ -0,0 +1,52 @@ +// 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 Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + public class RazorReferenceManagerTest + { + private static readonly string ApplicationPartReferencePath = "some-path"; + + [Fact] + public void GetCompilationReferences_CombinesApplicationPartAndOptionMetadataReferences() + { + // Arrange + var options = new MvcRazorRuntimeCompilationOptions(); + var additionalReferencePath = "additional-path"; + options.AdditionalReferencePaths.Add(additionalReferencePath); + + var applicationPartManager = GetApplicationPartManager(); + var referenceManager = new RazorReferenceManager( + applicationPartManager, + Options.Create(options)); + + var expected = new[] { ApplicationPartReferencePath, additionalReferencePath }; + + // Act + var references = referenceManager.GetReferencePaths(); + + // Assert + Assert.Equal(expected, references); + } + + private static ApplicationPartManager GetApplicationPartManager() + { + var applicationPartManager = new ApplicationPartManager(); + var part = new Mock(); + + part.As() + .Setup(p => p.GetReferencePaths()) + .Returns(new[] { ApplicationPartReferencePath }); + + applicationPartManager.ApplicationParts.Add(part.Object); + + return applicationPartManager; + } + } +} diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/RuntimeCompilationFileProviderTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/RuntimeCompilationFileProviderTest.cs new file mode 100644 index 0000000000..61b9b22290 --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/RuntimeCompilationFileProviderTest.cs @@ -0,0 +1,31 @@ +// 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 Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + public class RuntimeCompilationFileProviderTest + { + [Fact] + public void GetFileProvider_ThrowsIfNoConfiguredFileProviders() + { + // Arrange + var expected = + $"'{typeof(MvcRazorRuntimeCompilationOptions).FullName}.{nameof(MvcRazorRuntimeCompilationOptions.FileProviders)}' must " + + $"not be empty. At least one '{typeof(IFileProvider).FullName}' is required to locate a view for " + + "rendering."; + var options = Options.Create(new MvcRazorRuntimeCompilationOptions()); + + var fileProvider = new RuntimeCompilationFileProvider(options); + + // Act & Assert + var exception = Assert.Throws( + () => fileProvider.FileProvider); + Assert.Equal(expected, exception.Message); + } + } +} diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/RuntimeViewCompilerTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/RuntimeViewCompilerTest.cs new file mode 100644 index 0000000000..5ac9e61bed --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/RuntimeViewCompilerTest.cs @@ -0,0 +1,908 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; +using Microsoft.AspNetCore.Razor.Hosting; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; +using static Microsoft.AspNetCore.Razor.Hosting.TestRazorCompiledItem; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + public class RuntimeViewCompilerTest + { + [Fact] + public async Task CompileAsync_ReturnsResultWithNullAttribute_IfFileIsNotFoundInFileSystem() + { + // Arrange + var path = "/file/does-not-exist"; + var fileProvider = new TestFileProvider(); + var viewCompiler = GetViewCompiler(fileProvider); + + // Act + var result1 = await viewCompiler.CompileAsync(path); + var result2 = await viewCompiler.CompileAsync(path); + + // Assert + Assert.Same(result1, result2); + Assert.Null(result1.Item); + var token = Assert.Single(result1.ExpirationTokens); + Assert.Same(fileProvider.GetChangeToken(path), token); + } + + [Fact] + public async Task CompileAsync_ReturnsResultWithExpirationToken() + { + // Arrange + var path = "/file/does-not-exist"; + var fileProvider = new TestFileProvider(); + var viewCompiler = GetViewCompiler(fileProvider); + + // Act + var result1 = await viewCompiler.CompileAsync(path); + var result2 = await viewCompiler.CompileAsync(path); + + // Assert + Assert.Same(result1, result2); + Assert.Null(result1.Item); + Assert.Collection( + result1.ExpirationTokens, + token => Assert.Equal(fileProvider.GetChangeToken(path), token)); + } + + [Fact] + public async Task CompileAsync_AddsChangeTokensForViewStartsIfFileExists() + { + // Arrange + var path = "/file/exists/FilePath.cshtml"; + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + var viewCompiler = GetViewCompiler(fileProvider); + + // Act + var result = await viewCompiler.CompileAsync(path); + + // Assert + Assert.NotNull(result.Item); + Assert.Collection( + result.ExpirationTokens, + token => Assert.Same(fileProvider.GetChangeToken(path), token), + token => Assert.Same(fileProvider.GetChangeToken("/_ViewImports.cshtml"), token), + token => Assert.Same(fileProvider.GetChangeToken("/file/_ViewImports.cshtml"), token), + token => Assert.Same(fileProvider.GetChangeToken("/file/exists/_ViewImports.cshtml"), token)); + } + + [Theory] + [InlineData("/Areas/Finances/Views/Home/Index.cshtml")] + [InlineData(@"Areas\Finances\Views\Home\Index.cshtml")] + [InlineData(@"\Areas\Finances\Views\Home\Index.cshtml")] + [InlineData(@"\Areas\Finances\Views/Home\Index.cshtml")] + public async Task CompileAsync_NormalizesPathSeparatorForPaths(string relativePath) + { + // Arrange + var viewPath = "/Areas/Finances/Views/Home/Index.cshtml"; + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(viewPath, "some content"); + var viewCompiler = GetViewCompiler(fileProvider); + + // Act - 1 + var result1 = await viewCompiler.CompileAsync(@"Areas\Finances\Views\Home\Index.cshtml"); + + // Act - 2 + viewCompiler.Compile = _ => throw new Exception("Can't call me"); + var result2 = await viewCompiler.CompileAsync(relativePath); + + // Assert - 2 + Assert.Same(result1, result2); + } + + [Fact] + public async Task CompileAsync_InvalidatesCache_IfChangeTokenExpires() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + var fileProvider = new TestFileProvider(); + var fileNode = fileProvider.AddFile(path, "some content"); + var viewCompiler = GetViewCompiler(fileProvider); + + // Act 1 + var result1 = await viewCompiler.CompileAsync(path); + + // Assert 1 + Assert.NotNull(result1.Item); + + // Act 2 + // Simulate deleting the file + fileProvider.GetChangeToken(path).HasChanged = true; + fileProvider.DeleteFile(path); + + viewCompiler.Compile = _ => throw new Exception("Can't call me"); + var result2 = await viewCompiler.CompileAsync(path); + + // Assert 2 + Assert.NotSame(result1, result2); + Assert.Null(result2.Item); + } + + [Fact] + public async Task CompileAsync_ReturnsNewResultIfFileWasModified() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + var viewCompiler = GetViewCompiler(fileProvider); + var expected2 = new CompiledViewDescriptor(); + + // Act 1 + var result1 = await viewCompiler.CompileAsync(path); + + // Assert 1 + Assert.NotNull(result1.Item); + + // Act 2 + fileProvider.GetChangeToken(path).HasChanged = true; + viewCompiler.Compile = _ => expected2; + var result2 = await viewCompiler.CompileAsync(path); + + // Assert 2 + Assert.NotSame(result1, result2); + Assert.Same(expected2, result2); + } + + [Fact] + public async Task CompileAsync_ReturnsNewResult_IfAncestorViewImportsWereModified() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + var viewCompiler = GetViewCompiler(fileProvider); + var expected2 = new CompiledViewDescriptor(); + + // Act 1 + var result1 = await viewCompiler.CompileAsync(path); + + // Assert 1 + Assert.NotNull(result1.Item); + + // Act 2 + fileProvider.GetChangeToken("/Views/_ViewImports.cshtml").HasChanged = true; + viewCompiler.Compile = _ => expected2; + var result2 = await viewCompiler.CompileAsync(path); + + // Assert 2 + Assert.NotSame(result1, result2); + Assert.Same(expected2, result2); + } + + [Fact] + public async Task CompileAsync_ReturnsPrecompiledViews() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + var precompiledView = new CompiledViewDescriptor + { + RelativePath = path, + }; + var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView }); + + // Act + var result = await viewCompiler.CompileAsync(path); + + // Assert + Assert.Same(precompiledView, result); + + // This view doesn't have checksums so it can't be recompiled. + Assert.Null(precompiledView.ExpirationTokens); + } + + [Theory] + [InlineData("/views/home/index.cshtml")] + [InlineData("/VIEWS/HOME/INDEX.CSHTML")] + [InlineData("/viEws/HoME/inDex.cshtml")] + public async Task CompileAsync_PerformsCaseInsensitiveLookupsForPrecompiledViews(string lookupPath) + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + var precompiledView = new CompiledViewDescriptor + { + RelativePath = path, + }; + var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView }); + + // Act + var result = await viewCompiler.CompileAsync(lookupPath); + + // Assert + Assert.Same(precompiledView, result); + } + + [Fact] + public async Task CompileAsync_PerformsCaseInsensitiveLookupsForPrecompiledViews_WithNonNormalizedPaths() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + var precompiledView = new CompiledViewDescriptor + { + RelativePath = path, + }; + var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView }); + + // Act + var result = await viewCompiler.CompileAsync("Views\\Home\\Index.cshtml"); + + // Assert + Assert.Same(precompiledView, result); + } + + [Fact] + public async Task CompileAsync_PrecompiledViewWithoutChecksumForMainSource_DoesNotSupportRecompilation() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + + var precompiledView = new CompiledViewDescriptor + { + RelativePath = path, + Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[] + { + new RazorSourceChecksumAttribute("sha1", GetChecksum("some content"), "/Views/Some-Other-View"), + }), + }; + + var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView }); + + // Act - 1 + var result = await viewCompiler.CompileAsync(path); + + // Assert - 1 + Assert.Same(precompiledView.Item, result.Item); + + // Act - 2 + fileProvider.Watch(path); + fileProvider.GetChangeToken(path).HasChanged = true; + result = await viewCompiler.CompileAsync(path); + + // Assert - 2 + Assert.Same(precompiledView.Item, result.Item); + + // This view doesn't have checksums so it can't be recompiled. + Assert.Null(result.ExpirationTokens); + } + + [Fact] + public async Task CompileAsync_PrecompiledViewWithoutAnyChecksum_DoesNotSupportRecompilation() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + + var precompiledView = new CompiledViewDescriptor + { + RelativePath = path, + Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[] { }), + }; + + var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView }); + + // Act - 1 + var result = await viewCompiler.CompileAsync(path); + + // Assert - 1 + Assert.Same(precompiledView, result); + + // Act - 2 + fileProvider.Watch(path); + fileProvider.GetChangeToken(path).HasChanged = true; + result = await viewCompiler.CompileAsync(path); + + // Assert - 2 + Assert.Same(precompiledView, result); + + // This view doesn't have checksums so it can't be recompiled. + Assert.Null(result.ExpirationTokens); + } + + [Fact] + public async Task CompileAsync_PrecompiledViewWithChecksum_UsesPrecompiledViewWhenChecksumIsMatch() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + + var precompiledView = new CompiledViewDescriptor + { + RelativePath = path, + Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[] + { + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), path), + }), + }; + + var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView }); + + // Act + var result = await viewCompiler.CompileAsync(path); + + // Assert + Assert.Same(precompiledView.Item, result.Item); + + // This view has checksums so it should also have tokens + Assert.Collection( + result.ExpirationTokens, + token => Assert.Same(fileProvider.GetChangeToken(path), token)); + } + + [Fact] + public async Task CompileAsync_PrecompiledViewWithChecksum_CanRejectWhenChecksumFails() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + + var expected = new CompiledViewDescriptor(); + + var precompiledView = new CompiledViewDescriptor + { + RelativePath = path, + Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[] + { + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some other content"), path), + }), + }; + + var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView }); + viewCompiler.Compile = _ => expected; + + // Act + var result = await viewCompiler.CompileAsync(path); + + // Assert + Assert.Same(expected, result); + } + + [Fact] + public async Task CompileAsync_PrecompiledViewWithChecksum_AddsExpirationTokensForFilesInChecksumAttributes() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + + var precompiledView = new CompiledViewDescriptor + { + RelativePath = path, + Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[] + { + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), path), + }), + }; + + var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView }); + + // Act + var result = await viewCompiler.CompileAsync(path); + + // Assert + Assert.Same(precompiledView.Item, result.Item); + var token = Assert.Single(result.ExpirationTokens); + Assert.Same(fileProvider.GetChangeToken(path), token); + } + + [Fact] + public async Task CompileAsync_PrecompiledViewWithChecksum_CanRecompile() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + + var fileProvider = new TestFileProvider(); + var file = fileProvider.AddFile(path, "some content"); + var expected2 = new CompiledViewDescriptor(); + + var precompiledView = new CompiledViewDescriptor + { + RelativePath = path, + Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[] + { + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), path), + }), + }; + + var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView }); + + // Act - 1 + var result = await viewCompiler.CompileAsync(path); + + // Assert - 1 + Assert.Same(precompiledView.Item, result.Item); + Assert.NotEmpty(result.ExpirationTokens); + + // Act - 2 + file.Content = "different"; + fileProvider.GetChangeToken(path).HasChanged = true; + viewCompiler.Compile = _ => expected2; + result = await viewCompiler.CompileAsync(path); + + // Assert - 2 + Assert.Same(expected2, result); + } + + [Fact] + public async Task CompileAsync_PrecompiledViewWithChecksum_DoesNotRecompiledWithoutContentChange() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + + var precompiledView = new CompiledViewDescriptor + { + RelativePath = path, + Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[] + { + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), path), + }), + }; + + var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView }); + + // Act - 1 + var result = await viewCompiler.CompileAsync(path); + + // Assert - 1 + Assert.Same(precompiledView.Item, result.Item); + + // Act - 2 + fileProvider.GetChangeToken(path).HasChanged = true; + result = await viewCompiler.CompileAsync(path); + + // Assert - 2 + Assert.Same(precompiledView.Item, result.Item); + } + + [Fact] + public async Task CompileAsync_PrecompiledViewWithChecksum_CanReusePrecompiledViewIfContentChangesToMatch() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + + var fileProvider = new TestFileProvider(); + var file = fileProvider.AddFile(path, "different content"); + + var expected1 = new CompiledViewDescriptor(); + + var precompiledView = new CompiledViewDescriptor + { + RelativePath = path, + Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[] + { + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), path), + }), + }; + + var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView }); + viewCompiler.Compile = _ => expected1; + + // Act - 1 + var result = await viewCompiler.CompileAsync(path); + + // Assert - 1 + Assert.Same(expected1, result); + + // Act - 2 + file.Content = "some content"; + fileProvider.GetChangeToken(path).HasChanged = true; + result = await viewCompiler.CompileAsync(path); + + // Assert - 2 + Assert.Same(precompiledView.Item, result.Item); + } + + [Fact] + public async Task CompileAsync_PrecompiledViewWithChecksum_CanRecompileWhenViewImportChanges() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + var importPath = "/Views/_ViewImports.cshtml"; + + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + var importFile = fileProvider.AddFile(importPath, "some import"); + + var expected2 = new CompiledViewDescriptor(); + + var precompiledView = new CompiledViewDescriptor + { + RelativePath = path, + Item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", path, new object[] + { + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), path), + new RazorSourceChecksumAttribute("SHA1", GetChecksum("some import"), importPath), + }), + }; + + var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView }); + + // Act - 1 + var result = await viewCompiler.CompileAsync(path); + + // Assert - 1 + Assert.Same(precompiledView.Item, result.Item); + + // Act - 2 + importFile.Content = "different content"; + fileProvider.GetChangeToken(importPath).HasChanged = true; + viewCompiler.Compile = _ => expected2; + result = await viewCompiler.CompileAsync(path); + + // Assert - 2 + Assert.Same(expected2, result); + } + + [Fact] + public async Task GetOrAdd_AllowsConcurrentCompilationOfMultipleRazorPages() + { + // Arrange + var path1 = "/Views/Home/Index.cshtml"; + var path2 = "/Views/Home/About.cshtml"; + var waitDuration = TimeSpan.FromSeconds(20); + + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path1, "some content"); + fileProvider.AddFile(path2, "some content"); + + var resetEvent1 = new AutoResetEvent(initialState: false); + var resetEvent2 = new ManualResetEvent(initialState: false); + + var compilingOne = false; + var compilingTwo = false; + + var result1 = new CompiledViewDescriptor(); + var result2 = new CompiledViewDescriptor(); + + var compiler = GetViewCompiler(fileProvider); + + compiler.Compile = path => + { + if (path == path1) + { + compilingOne = true; + + // Event 2 + Assert.True(resetEvent1.WaitOne(waitDuration)); + + // Event 3 + Assert.True(resetEvent2.Set()); + + // Event 6 + Assert.True(resetEvent1.WaitOne(waitDuration)); + + Assert.True(compilingTwo); + + return result1; + } + else if (path == path2) + { + compilingTwo = true; + + // Event 4 + Assert.True(resetEvent2.WaitOne(waitDuration)); + + // Event 5 + Assert.True(resetEvent1.Set()); + + Assert.True(compilingOne); + + return result2; + } + else + { + throw new Exception(); + } + }; + + // Act + var task1 = Task.Run(() => compiler.CompileAsync(path1)); + var task2 = Task.Run(() => compiler.CompileAsync(path2)); + + // Event 1 + resetEvent1.Set(); + + await Task.WhenAll(task1, task2); + + // Assert + Assert.True(compilingOne); + Assert.True(compilingTwo); + Assert.Same(result1, task1.Result); + Assert.Same(result2, task2.Result); + } + + [Fact] + public async Task CompileAsync_DoesNotCreateMultipleCompilationResults_ForConcurrentInvocations() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + var waitDuration = TimeSpan.FromSeconds(20); + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + var resetEvent1 = new ManualResetEvent(initialState: false); + var resetEvent2 = new ManualResetEvent(initialState: false); + var compiler = GetViewCompiler(fileProvider); + + compiler.Compile = _ => + { + // Event 2 + resetEvent1.WaitOne(waitDuration); + + // Event 3 + resetEvent2.Set(); + return new CompiledViewDescriptor(); + }; + + // Act + var task1 = Task.Run(() => compiler.CompileAsync(path)); + var task2 = Task.Run(() => + { + // Event 4 + Assert.True(resetEvent2.WaitOne(waitDuration)); + return compiler.CompileAsync(path); + }); + + // Event 1 + resetEvent1.Set(); + await Task.WhenAll(task1, task2); + + // Assert + var result1 = task1.Result; + var result2 = task2.Result; + Assert.Same(result1, result2); + } + + [Fact] + public async Task GetOrAdd_CachesCompilationExceptions() + { + // Arrange + var path = "/Views/Home/Index.cshtml"; + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, "some content"); + var exception = new InvalidTimeZoneException(); + var compiler = GetViewCompiler(fileProvider); + compiler.Compile = _ => throw exception; + + // Act and Assert - 1 + var actual = await Assert.ThrowsAsync( + () => compiler.CompileAsync(path)); + Assert.Same(exception, actual); + + // Act and Assert - 2 + compiler.Compile = _ => throw new Exception("Shouldn't be called"); + + actual = await Assert.ThrowsAsync( + () => compiler.CompileAsync(path)); + Assert.Same(exception, actual); + } + + [Fact] + public void Compile_SucceedsForCSharp7() + { + // Arrange + var content = @" +public class MyTestType +{ + private string _name; + + public string Name + { + get => _name; + set => _name = value ?? throw new System.ArgumentNullException(nameof(value)); + } +}"; + var compiler = GetViewCompiler(new TestFileProvider()); + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("razor-content", "filename")); + + // Act + var result = compiler.CompileAndEmit(codeDocument, content); + + // Assert + var exportedType = Assert.Single(result.ExportedTypes); + Assert.Equal("MyTestType", exportedType.Name); + } + + [Fact] + public void Compile_ReturnsCompilationFailureWithPathsFromLinePragmas() + { + // Arrange + var viewPath = "some-relative-path"; + var fileContent = "test file content"; + var content = $@" +#line 1 ""{viewPath}"" +this should fail"; + + var compiler = GetViewCompiler(new TestFileProvider()); + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create(fileContent, viewPath)); + + // Act & Assert + var ex = Assert.Throws(() => compiler.CompileAndEmit(codeDocument, content)); + + var compilationFailure = Assert.Single(ex.CompilationFailures); + Assert.Equal(viewPath, compilationFailure.SourceFilePath); + Assert.Equal(fileContent, compilationFailure.SourceFileContent); + } + + [Fact] + public void Compile_ReturnsGeneratedCodePath_IfLinePragmaIsNotAvailable() + { + // Arrange + var viewPath = "some-relative-path"; + var fileContent = "file content"; + var content = "this should fail"; + + var compiler = GetViewCompiler(new TestFileProvider()); + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create(fileContent, viewPath)); + + // Act & Assert + var ex = Assert.Throws(() => compiler.CompileAndEmit(codeDocument, content)); + + var compilationFailure = Assert.Single(ex.CompilationFailures); + Assert.Equal("Generated Code", compilationFailure.SourceFilePath); + Assert.Equal(content, compilationFailure.SourceFileContent); + } + + [Fact] + public void CompileAndEmit_DoesNotThrowIfDebugTypeIsEmbedded() + { + // Arrange + var referenceManager = CreateReferenceManager(); + var csharpCompiler = new TestCSharpCompiler(referenceManager, Mock.Of()) + { + EmitOptionsSettable = new EmitOptions(debugInformationFormat: DebugInformationFormat.Embedded), + }; + + var compiler = GetViewCompiler(csharpCompiler: csharpCompiler); + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "some-relative-path.cshtml")); + + // Act + var result = compiler.CompileAndEmit(codeDocument, "public class Test{}"); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public void CompileAndEmit_WorksIfEmitPdbIsNotSet() + { + // Arrange + var referenceManager = CreateReferenceManager(); + var csharpCompiler = new TestCSharpCompiler(referenceManager, Mock.Of()) + { + EmitPdbSettable = false, + }; + + var compiler = GetViewCompiler(csharpCompiler: csharpCompiler); + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "some-relative-path.cshtml")); + + // Act + var result = compiler.CompileAndEmit(codeDocument, "public class Test{}"); + + // Assert + Assert.NotNull(result); + } + + private static TestRazorViewCompiler GetViewCompiler( + TestFileProvider fileProvider = null, + RazorReferenceManager referenceManager = null, + IList precompiledViews = null, + CSharpCompiler csharpCompiler = null) + { + fileProvider = fileProvider ?? new TestFileProvider(); + var options = Options.Create(new MvcRazorRuntimeCompilationOptions + { + FileProviders = { fileProvider } + }); + var compilationFileProvider = new RuntimeCompilationFileProvider(options); + + + referenceManager = referenceManager ?? CreateReferenceManager(); + precompiledViews = precompiledViews ?? Array.Empty(); + + var hostingEnvironment = Mock.Of(e => e.ContentRootPath == "BasePath"); + var fileSystem = new FileProviderRazorProjectFileSystem(compilationFileProvider, hostingEnvironment); + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder => + { + RazorExtensions.Register(builder); + }); + + csharpCompiler = csharpCompiler ?? new CSharpCompiler(referenceManager, hostingEnvironment); + + return new TestRazorViewCompiler( + fileProvider, + projectEngine, + csharpCompiler, + precompiledViews); + } + + private static RazorReferenceManager CreateReferenceManager() + { + var applicationPartManager = new ApplicationPartManager(); + var assembly = typeof(RuntimeViewCompilerTest).Assembly; + applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly)); + + return new RazorReferenceManager(applicationPartManager, Options.Create(new MvcRazorRuntimeCompilationOptions())); + } + + private class TestRazorViewCompiler : RuntimeViewCompiler + { + public TestRazorViewCompiler( + TestFileProvider fileProvider, + RazorProjectEngine projectEngine, + CSharpCompiler csharpCompiler, + IList precompiledViews, + Func compile = null) + : base(fileProvider, projectEngine, csharpCompiler, precompiledViews, NullLogger.Instance) + { + Compile = compile; + if (Compile == null) + { + Compile = path => new CompiledViewDescriptor + { + RelativePath = path, + Item = CreateForView(path), + }; + } + } + + public Func Compile { get; set; } + + protected override CompiledViewDescriptor CompileAndEmit(string relativePath) + { + return Compile(relativePath); + } + } + + private class TestCSharpCompiler : CSharpCompiler + { + public TestCSharpCompiler(RazorReferenceManager manager, IHostingEnvironment hostingEnvironment) + : base(manager, hostingEnvironment) + { + } + + public EmitOptions EmitOptionsSettable { get; set; } + + public bool EmitPdbSettable { get; set; } + + public override EmitOptions EmitOptions => EmitOptionsSettable; + + public override bool EmitPdb => EmitPdbSettable; + } + } +} \ No newline at end of file diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/DirectoryNode.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/DirectoryNode.cs new file mode 100644 index 0000000000..4cd98796c2 --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/DirectoryNode.cs @@ -0,0 +1,168 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Language +{ + // Internal for testing + [DebuggerDisplay("{Path}")] + internal class DirectoryNode + { + public DirectoryNode(string path) + { + Path = path; + } + + public string Path { get; } + + public List Directories { get; } = new List(); + + public List Files { get; } = new List(); + + public void AddFile(FileNode fileNode) + { + var filePath = fileNode.Path; + if (!filePath.StartsWith(Path, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"File {fileNode.Path} does not belong to {Path}."); + } + + // Look for the first / that appears in the path after the current directory path. + var directoryPath = GetDirectoryPath(filePath); + var directory = GetOrAddDirectory(this, directoryPath, createIfNotExists: true); + Debug.Assert(directory != null); + directory.Files.Add(fileNode); + fileNode.Directory = directory; + } + + public DirectoryNode GetDirectory(string path) + { + if (!path.StartsWith(Path, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"File {path} does not belong to {Path}."); + } + + return GetOrAddDirectory(this, path); + } + + public IEnumerable EnumerateItems() + { + foreach (var file in Files) + { + yield return file.ProjectItem; + } + + foreach (var directory in Directories) + { + foreach (var file in directory.EnumerateItems()) + { + yield return file; + } + } + } + + public RazorProjectItem GetItem(string path) + { + if (!path.StartsWith(Path, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"File {path} does not belong to {Path}."); + } + + var directoryPath = GetDirectoryPath(path); + var directory = GetOrAddDirectory(this, directoryPath); + if (directory == null) + { + return new NotFoundProjectItem("/", path); + } + + foreach (var file in directory.Files) + { + var filePath = file.Path; + var directoryLength = directory.Path.Length; + + // path, filePath -> /Views/Home/Index.cshtml + // directory.Path -> /Views/Home/ + // We only need to match the file name portion since we've already matched the directory segment. + if (string.Compare(path, directoryLength, filePath, directoryLength, path.Length - directoryLength, StringComparison.OrdinalIgnoreCase) == 0) + { + return file.ProjectItem; + } + } + + return new NotFoundProjectItem("/", path); + } + + private static string GetDirectoryPath(string path) + { + // /dir1/dir2/file.cshtml -> /dir1/dir2/ + var fileNameIndex = path.LastIndexOf('/'); + if (fileNameIndex == -1) + { + return path; + } + + return path.Substring(0, fileNameIndex + 1); + } + + private static DirectoryNode GetOrAddDirectory( + DirectoryNode directory, + string path, + bool createIfNotExists = false) + { + Debug.Assert(!string.IsNullOrEmpty(path)); + if (path[path.Length - 1] != '/') + { + path += '/'; + } + + int index; + while ((index = path.IndexOf('/', directory.Path.Length)) != -1 && index != path.Length) + { + var subDirectory = FindSubDirectory(directory, path); + + if (subDirectory == null) + { + if (createIfNotExists) + { + var directoryPath = path.Substring(0, index + 1); // + 1 to include trailing slash + subDirectory = new DirectoryNode(directoryPath); + directory.Directories.Add(subDirectory); + } + else + { + return null; + } + } + + directory = subDirectory; + } + + return directory; + } + + private static DirectoryNode FindSubDirectory(DirectoryNode parentDirectory, string path) + { + for (var i = 0; i < parentDirectory.Directories.Count; i++) + { + // ParentDirectory.Path -> /Views/Home/ + // CurrentDirectory.Path -> /Views/Home/SubDir/ + // Path -> /Views/Home/SubDir/MorePath/File.cshtml + // Each invocation of FindSubDirectory returns the immediate subdirectory along the path to the file. + + var currentDirectory = parentDirectory.Directories[i]; + var directoryPath = currentDirectory.Path; + var startIndex = parentDirectory.Path.Length; + + if (string.Compare(path, startIndex, directoryPath, startIndex, directoryPath.Length - startIndex, StringComparison.OrdinalIgnoreCase) == 0) + { + return currentDirectory; + } + } + + return null; + } + } +} diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/FileNode.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/FileNode.cs new file mode 100644 index 0000000000..583281794a --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/FileNode.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.Diagnostics; + +namespace Microsoft.AspNetCore.Razor.Language +{ + // Internal for testing + [DebuggerDisplay("{Path}")] + internal class FileNode + { + public FileNode(string path, RazorProjectItem projectItem) + { + Path = path; + ProjectItem = projectItem; + } + + public DirectoryNode Directory { get; set; } + + public string Path { get; } + + public RazorProjectItem ProjectItem { get; set; } + } +} \ No newline at end of file diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/NotFoundProjectItem.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/NotFoundProjectItem.cs new file mode 100644 index 0000000000..84ea7f01cf --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/NotFoundProjectItem.cs @@ -0,0 +1,27 @@ +// 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.IO; + +namespace Microsoft.AspNetCore.Razor.Language +{ + internal class NotFoundProjectItem : RazorProjectItem + { + public NotFoundProjectItem(string basePath, string path) + { + BasePath = basePath; + FilePath = path; + } + + public override string BasePath { get; } + + public override string FilePath { get; } + + public override bool Exists => false; + + public override string PhysicalPath => throw new NotSupportedException(); + + public override Stream Read() => throw new NotSupportedException(); + } +} diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/TestRazorProjectItem.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/TestRazorProjectItem.cs new file mode 100644 index 0000000000..c30b761464 --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/TestRazorProjectItem.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 System.IO; +using System.Text; + +namespace Microsoft.AspNetCore.Razor.Language +{ + public class TestRazorProjectItem : RazorProjectItem + { + public TestRazorProjectItem( + string filePath, + string content = "Default content", + string physicalPath = null, + string relativePhysicalPath = null, + string basePath = "/") + { + FilePath = filePath; + PhysicalPath = physicalPath; + RelativePhysicalPath = relativePhysicalPath; + BasePath = basePath; + Content = content; + } + + public override string BasePath { get; } + + public override string FilePath { get; } + + public override string PhysicalPath { get; } + + public override string RelativePhysicalPath { get; } + + public override bool Exists => true; + + public string Content { get; set; } + + public override Stream Read() + { + var stream = new MemoryStream(Encoding.UTF8.GetBytes(Content)); + + return stream; + } + } +} \ No newline at end of file diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/TestRazorReferenceManager.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/TestRazorReferenceManager.cs new file mode 100644 index 0000000000..91a44bcfeb --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/TestRazorReferenceManager.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; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation +{ + internal class TestRazorReferenceManager : RazorReferenceManager + { + public TestRazorReferenceManager() + : base( + new ApplicationPartManager(), + Options.Create(new MvcRazorRuntimeCompilationOptions())) + { + CompilationReferences = Array.Empty(); + } + + public override IReadOnlyList CompilationReferences { get; } + } +} diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/VirtualRazorProjectFileSystem.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/VirtualRazorProjectFileSystem.cs new file mode 100644 index 0000000000..fe662d3b3f --- /dev/null +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test/TestInfrastructure/VirtualRazorProjectFileSystem.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 System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation; + +namespace Microsoft.AspNetCore.Razor.Language +{ + internal class VirtualRazorProjectFileSystem : RazorProjectFileSystem + { + private readonly DirectoryNode _root = new DirectoryNode("/"); + + public override IEnumerable EnumerateItems(string basePath) + { + basePath = NormalizeAndEnsureValidPath(basePath); + var directory = _root.GetDirectory(basePath); + return directory?.EnumerateItems() ?? Enumerable.Empty(); + } + + public override RazorProjectItem GetItem(string path) + { + path = NormalizeAndEnsureValidPath(path); + return _root.GetItem(path) ?? new NotFoundProjectItem(string.Empty, path); + } + + public void Add(RazorProjectItem projectItem) + { + if (projectItem == null) + { + throw new ArgumentNullException(nameof(projectItem)); + } + + var filePath = NormalizeAndEnsureValidPath(projectItem.FilePath); + _root.AddFile(new FileNode(filePath, projectItem)); + } + } +} \ No newline at end of file diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/RazorViewCompilerTest.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/DefaultViewCompilerTest.cs similarity index 96% rename from src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/RazorViewCompilerTest.cs rename to src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/DefaultViewCompilerTest.cs index 790efd95dd..003bd6ddab 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/RazorViewCompilerTest.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/DefaultViewCompilerTest.cs @@ -9,7 +9,7 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.Razor.Compilation { - public class RazorViewCompilerTest + public class DefaultViewCompilerTest { [Fact] public async Task CompileAsync_ReturnsResultWithNullAttribute_IfFileIsNotFoundInFileSystem() @@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation return viewCompiler; } - private class TestRazorViewCompiler : RazorViewCompiler + private class TestRazorViewCompiler : DefaultViewCompiler { public TestRazorViewCompiler(IList compiledViews) : base(compiledViews, NullLogger.Instance) diff --git a/src/Mvc/test/WebSites/RazorWebSite/Controllers/UpdateableFileProviderController.cs b/src/Mvc/test/WebSites/RazorBuildWebSite/Controllers/UpdateableViewsController.cs similarity index 79% rename from src/Mvc/test/WebSites/RazorWebSite/Controllers/UpdateableFileProviderController.cs rename to src/Mvc/test/WebSites/RazorBuildWebSite/Controllers/UpdateableViewsController.cs index ed5a12a6f2..609e8ab7bf 100644 --- a/src/Mvc/test/WebSites/RazorWebSite/Controllers/UpdateableFileProviderController.cs +++ b/src/Mvc/test/WebSites/RazorBuildWebSite/Controllers/UpdateableViewsController.cs @@ -3,11 +3,11 @@ using Microsoft.AspNetCore.Mvc; -namespace RazorWebSite +namespace RazorBuildWebSite { - public class UpdateableFileProviderController : Controller + public class UpdateableViewsController : Controller { - public IActionResult Index() => View("/Views/UpdateableIndex/Index.cshtml"); + public IActionResult Index() => View(); [HttpPost] public IActionResult Update([FromServices] UpdateableFileProvider fileProvider, string path, string content) diff --git a/src/Mvc/test/WebSites/RazorBuildWebSite/RazorBuildWebSite.csproj b/src/Mvc/test/WebSites/RazorBuildWebSite/RazorBuildWebSite.csproj index f7fcee6d8a..76177e1789 100644 --- a/src/Mvc/test/WebSites/RazorBuildWebSite/RazorBuildWebSite.csproj +++ b/src/Mvc/test/WebSites/RazorBuildWebSite/RazorBuildWebSite.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -10,6 +10,7 @@ + diff --git a/src/Mvc/test/WebSites/RazorBuildWebSite/Startup.cs b/src/Mvc/test/WebSites/RazorBuildWebSite/Startup.cs index 3d4858bd55..5d5123ee9d 100644 --- a/src/Mvc/test/WebSites/RazorBuildWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/RazorBuildWebSite/Startup.cs @@ -13,7 +13,11 @@ namespace RazorBuildWebSite { public void ConfigureServices(IServiceCollection services) { + var fileProvider = new UpdateableFileProvider(); + services.AddSingleton(fileProvider); + services.AddMvc() + .AddRazorRuntimeCompilation(options => options.FileProviders.Add(fileProvider)) .SetCompatibilityVersion(CompatibilityVersion.Latest); } diff --git a/src/Mvc/test/WebSites/RazorWebSite/Services/UpdateableFileProvider.cs b/src/Mvc/test/WebSites/RazorBuildWebSite/UpdateableFileProvider.cs similarity index 92% rename from src/Mvc/test/WebSites/RazorWebSite/Services/UpdateableFileProvider.cs rename to src/Mvc/test/WebSites/RazorBuildWebSite/UpdateableFileProvider.cs index 5613ffda9a..42968e9fa0 100644 --- a/src/Mvc/test/WebSites/RazorWebSite/Services/UpdateableFileProvider.cs +++ b/src/Mvc/test/WebSites/RazorBuildWebSite/UpdateableFileProvider.cs @@ -10,7 +10,7 @@ using System.Threading; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; -namespace RazorWebSite +namespace RazorBuildWebSite { public class UpdateableFileProvider : IFileProvider { @@ -19,15 +19,11 @@ namespace RazorWebSite private readonly Dictionary _content = new Dictionary() { { - "/Views/UpdateableIndex/_ViewImports.cshtml", + "/Views/UpdateableViews/_ViewImports.cshtml", new TestFileInfo(string.Empty) }, { - "/Views/UpdateableIndex/Index.cshtml", - new TestFileInfo(@"@Html.Partial(""../UpdateableShared/_Partial.cshtml"")") - }, - { - "/Views/UpdateableShared/_Partial.cshtml", + "/Views/UpdateableViews/Index.cshtml", new TestFileInfo("Original content") }, { diff --git a/src/Mvc/test/WebSites/RazorBuildWebSite/readme.md b/src/Mvc/test/WebSites/RazorBuildWebSite/readme.md index 292a669614..07d41e6944 100644 --- a/src/Mvc/test/WebSites/RazorBuildWebSite/readme.md +++ b/src/Mvc/test/WebSites/RazorBuildWebSite/readme.md @@ -1,4 +1,4 @@ RazorBuildWebSite === -This web site tests how the Razor view engine interacts with pre-built Razor assemblies. \ No newline at end of file +This web site tests how the Razor view engine interacts with pre-built and runtime compiled Razor assemblies. \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RazorWebSite/Controllers/EmbeddedViewsController.cs b/src/Mvc/test/WebSites/RazorWebSite/Controllers/EmbeddedViewsController.cs deleted file mode 100644 index d170e5cf57..0000000000 --- a/src/Mvc/test/WebSites/RazorWebSite/Controllers/EmbeddedViewsController.cs +++ /dev/null @@ -1,18 +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 Microsoft.AspNetCore.Mvc; - -namespace RazorWebSite.Controllers -{ - public class EmbeddedViewsController : Controller - { - public IActionResult Index() => null; - - public IActionResult LookupByName() => View("Index"); - - public IActionResult LookupByPath() => View("/Views/EmbeddedViews/Index.cshtml"); - - public IActionResult RelativeNonPath() => View(); - } -} diff --git a/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedShared/_Layout.cshtml b/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedShared/_Layout.cshtml deleted file mode 100644 index e16c087dfd..0000000000 --- a/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedShared/_Layout.cshtml +++ /dev/null @@ -1 +0,0 @@ -@RenderBody() \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedShared/_Partial.cshtml b/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedShared/_Partial.cshtml deleted file mode 100644 index b34fab2eec..0000000000 --- a/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedShared/_Partial.cshtml +++ /dev/null @@ -1 +0,0 @@ -Hello from EmbeddedShared/_Partial \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/EmbeddedPartial.cshtml b/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/EmbeddedPartial.cshtml deleted file mode 100644 index cebe57816a..0000000000 --- a/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/EmbeddedPartial.cshtml +++ /dev/null @@ -1 +0,0 @@ -Hello from EmbeddedHome\EmbeddedPartial \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/Index.cshtml b/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/Index.cshtml deleted file mode 100644 index 0084534e52..0000000000 --- a/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/Index.cshtml +++ /dev/null @@ -1,3 +0,0 @@ -@(await Html.PartialAsync("../EmbeddedShared/_Partial.cshtml")) -@(await Html.PartialAsync("_EmbeddedPartial")) -Tag Helper Link diff --git a/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/RelativeNonPath.cshtml b/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/RelativeNonPath.cshtml deleted file mode 100644 index 03303e5f91..0000000000 --- a/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/RelativeNonPath.cshtml +++ /dev/null @@ -1,2 +0,0 @@ -@{ Layout = "../EmbeddedShared/_Layout"; } -@(await Html.PartialAsync("./EmbeddedPartial")) \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/_ViewImports.cshtml b/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/_ViewImports.cshtml deleted file mode 100644 index 9018c7897f..0000000000 --- a/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/_ViewImports.cshtml +++ /dev/null @@ -1 +0,0 @@ -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/_ViewStart.cshtml b/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/_ViewStart.cshtml deleted file mode 100644 index e4209d9008..0000000000 --- a/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/EmbeddedViews/_ViewStart.cshtml +++ /dev/null @@ -1 +0,0 @@ -@{ Layout = "/Views/EmbeddedShared/_Layout.cshtml"; } diff --git a/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/Shared/_EmbeddedPartial.cshtml b/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/Shared/_EmbeddedPartial.cshtml deleted file mode 100644 index ef09462726..0000000000 --- a/src/Mvc/test/WebSites/RazorWebSite/EmbeddedResources/Views/Shared/_EmbeddedPartial.cshtml +++ /dev/null @@ -1 +0,0 @@ -Hello from Shared/_EmbeddedPartial \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RazorWebSite/RazorWebSite.csproj b/src/Mvc/test/WebSites/RazorWebSite/RazorWebSite.csproj index 17402ce6bc..edf7a035d9 100644 --- a/src/Mvc/test/WebSites/RazorWebSite/RazorWebSite.csproj +++ b/src/Mvc/test/WebSites/RazorWebSite/RazorWebSite.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -6,10 +6,6 @@ true - - - - diff --git a/src/Mvc/test/WebSites/RazorWebSite/Startup.cs b/src/Mvc/test/WebSites/RazorWebSite/Startup.cs index 1be982ca9c..f49df81d55 100644 --- a/src/Mvc/test/WebSites/RazorWebSite/Startup.cs +++ b/src/Mvc/test/WebSites/RazorWebSite/Startup.cs @@ -3,14 +3,12 @@ using System.Collections.Generic; using System.Globalization; -using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; namespace RazorWebSite { @@ -18,8 +16,6 @@ namespace RazorWebSite { public void ConfigureServices(IServiceCollection services) { - var updateableFileProvider = new UpdateableFileProvider(); - services.AddSingleton(updateableFileProvider); services.AddSingleton(); services.AddSingleton();