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();