Reintroduce a package for Razor runtime compilation (#6653)

* Reintroduce a package for Razor runtime compilation

Fixes https://github.com/aspnet/AspNetCore/issues/4947
This commit is contained in:
Pranav K 2019-01-29 09:34:43 -08:00 committed by GitHub
parent 1ecc8dd9c6
commit 0f072a9565
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 5018 additions and 115 deletions

View File

@ -35,6 +35,8 @@ and are generated based on the last package release.
<LatestPackageReference Include="Microsoft.AspNetCore.Blazor.Mono" Version="$(MicrosoftAspNetCoreBlazorMonoPackageVersion)" />
<LatestPackageReference Include="Microsoft.AspNetCore.Mvc.Razor.Extensions" Version="$(MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion)" />
<LatestPackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="$(MicrosoftAspNetCoreRazorLanguagePackageVersion)" />
<LatestPackageReference Include="Microsoft.AspNetCore.Mvc.Razor.Extensions" Version="$(MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion)" />
<LatestPackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="$(MicrosoftAspNetCoreRazorLanguagePackageVersion)" />
<LatestPackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
<LatestPackageReference Include="Microsoft.Azure.KeyVault" Version="$(MicrosoftAzureKeyVaultPackageVersion)" />
<LatestPackageReference Include="Microsoft.Build.Framework" Version="$(MicrosoftBuildFrameworkPackageVersion)" />

View File

@ -98,6 +98,7 @@
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Formatters.Xml" ProjectPath="$(RepositoryRoot)src\Mvc\src\Microsoft.AspNetCore.Mvc.Formatters.Xml\Microsoft.AspNetCore.Mvc.Formatters.Xml.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Localization" ProjectPath="$(RepositoryRoot)src\Mvc\src\Microsoft.AspNetCore.Mvc.Localization\Microsoft.AspNetCore.Mvc.Localization.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" ProjectPath="$(RepositoryRoot)src\Mvc\src\Microsoft.AspNetCore.Mvc.NewtonsoftJson\Microsoft.AspNetCore.Mvc.NewtonsoftJson.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" ProjectPath="$(RepositoryRoot)src\Mvc\src\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.RazorPages" ProjectPath="$(RepositoryRoot)src\Mvc\src\Microsoft.AspNetCore.Mvc.RazorPages\Microsoft.AspNetCore.Mvc.RazorPages.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Razor" ProjectPath="$(RepositoryRoot)src\Mvc\src\Microsoft.AspNetCore.Mvc.Razor\Microsoft.AspNetCore.Mvc.Razor.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.TagHelpers" ProjectPath="$(RepositoryRoot)src\Mvc\src\Microsoft.AspNetCore.Mvc.TagHelpers\Microsoft.AspNetCore.Mvc.TagHelpers.csproj" />

View File

@ -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}

View File

@ -7,6 +7,7 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc" />
<Reference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
<Reference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" />
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />

View File

@ -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.

View File

@ -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<string, ReportDiagnostic>
{
{"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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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<CompilationFailure> compilationFailures)
: base(FormatMessage(compilationFailures))
{
if (compilationFailures == null)
{
throw new ArgumentNullException(nameof(compilationFailures));
}
CompilationFailures = compilationFailures;
}
public IEnumerable<CompilationFailure> CompilationFailures { get; }
private static string FormatMessage(IEnumerable<CompilationFailure> compilationFailures)
{
return Resources.CompilationFailed + Environment.NewLine +
string.Join(
Environment.NewLine,
compilationFailures.SelectMany(f => f.Messages).Select(message => message.FormattedMessage));
}
}
}

View File

@ -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<RazorDiagnostic> 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<CompilationFailure>();
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<Diagnostic> diagnostics)
{
var diagnosticGroups = diagnostics
.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error)
.GroupBy(diagnostic => GetFilePath(codeDocument, diagnostic), StringComparer.Ordinal);
var failures = new List<CompilationFailure>();
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;
}
}
}

View File

@ -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<MvcRazorRuntimeCompilationOptions>
{
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);
}
}
}

View File

@ -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
{
/// <summary>
/// Configures <see cref="IMvcBuilder" /> to support runtime compilation of Razor views and Razor Pages.
/// </summary>
/// <param name="builder">The <see cref="IMvcBuilder" />.</param>
/// <returns>The <see cref="IMvcBuilder"/>.</returns>
public static IMvcBuilder AddRazorRuntimeCompilation(this IMvcBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
RazorRuntimeCompilationMvcCoreBuilderExtensions.AddServices(builder.Services);
return builder;
}
/// <summary>
/// Configures <see cref="IMvcBuilder" /> to support runtime compilation of Razor views and Razor Pages.
/// </summary>
/// <param name="builder">The <see cref="IMvcBuilder" />.</param>
/// <param name="setupAction">An action to configure the <see cref="MvcRazorRuntimeCompilationOptions"/>.</param>
/// <returns>The <see cref="IMvcBuilder"/>.</returns>
public static IMvcBuilder AddRazorRuntimeCompilation(this IMvcBuilder builder, Action<MvcRazorRuntimeCompilationOptions> 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;
}
}
}

View File

@ -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
{
/// <summary>
/// Configures <see cref="IMvcCoreBuilder" /> to support runtime compilation of Razor views and Razor Pages.
/// </summary>
/// <param name="builder">The <see cref="IMvcCoreBuilder" />.</param>
/// <returns>The <see cref="IMvcCoreBuilder"/>.</returns>
public static IMvcCoreBuilder AddRazorRuntimeCompilation(this IMvcCoreBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
AddServices(builder.Services);
return builder;
}
/// <summary>
/// Configures <see cref="IMvcCoreBuilder" /> to support runtime compilation of Razor views and Razor Pages.
/// </summary>
/// <param name="builder">The <see cref="IMvcCoreBuilder" />.</param>
/// <param name="setupAction">An action to configure the <see cref="MvcRazorRuntimeCompilationOptions"/>.</param>
/// <returns>The <see cref="IMvcCoreBuilder"/>.</returns>
public static IMvcCoreBuilder AddRazorRuntimeCompilation(this IMvcCoreBuilder builder, Action<MvcRazorRuntimeCompilationOptions> 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<IConfigureOptions<MvcRazorRuntimeCompilationOptions>, 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<IViewCompilerProvider, RuntimeViewCompilerProvider>();
services.TryAddSingleton<RuntimeCompilationFileProvider>();
services.TryAddSingleton<RazorReferenceManager>();
services.TryAddSingleton<CSharpCompiler>();
services.TryAddSingleton<RazorProjectFileSystem, FileProviderRazorProjectFileSystem>();
services.TryAddSingleton(s =>
{
var fileSystem = s.GetRequiredService<RazorProjectFileSystem>();
var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder =>
{
RazorExtensions.Register(builder);
// Roslyn + TagHelpers infrastructure
var referenceManager = s.GetRequiredService<RazorReferenceManager>();
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<IPageRouteModelProvider, RazorProjectPageRouteModelProvider>());
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IActionDescriptorChangeProvider, PageActionDescriptorChangeProvider>());
}
}
}

View File

@ -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<RazorProjectItem> EnumerateItems(string path)
{
path = NormalizeAndEnsureValidPath(path);
return EnumerateFiles(FileProvider.GetDirectoryContents(path), path, prefix: string.Empty);
}
private IEnumerable<RazorProjectItem> 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;
}
}
}

View File

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

View File

@ -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;
}
/// <remarks>
/// Invoking <see cref="RazorReferenceManager.CompilationReferences"/> ensures that compilation
/// references are lazily evaluated.
/// </remarks>
public IReadOnlyList<MetadataReference> References => _referenceManager.CompilationReferences;
public RazorEngine Engine { get; set; }
}
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Runtime compilation support for Razor views and Razor Pages in ASP.NET Core MVC.</Description>
<TargetFramework>netcoreapp3.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;aspnetcoremvc;razor</PackageTags>
<IsShippingPackage>true</IsShippingPackage>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc.Razor.Extensions" />
<Reference Include="Microsoft.AspNetCore.Mvc.RazorPages" />
<Reference Include="Microsoft.AspNetCore.Razor.Runtime" />
<Reference Include="Microsoft.CodeAnalysis.Razor" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Microsoft.AspNetCore.Mvc.Razor\ViewPath.cs" />
<Compile Include="..\Microsoft.AspNetCore.Mvc.RazorPages\ApplicationModels\PageRouteModelFactory.cs" />
</ItemGroup>
</Project>

View File

@ -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
{
/// <summary>
/// Gets the <see cref="IFileProvider" /> instances used to locate Razor files.
/// </summary>
/// <remarks>
/// At startup, this collection is initialized to include an instance of
/// <see cref="IHostingEnvironment.ContentRootFileProvider"/> that is rooted at the application root.
/// </remarks>
public IList<IFileProvider> FileProviders { get; } = new List<IFileProvider>();
/// <summary>
/// Gets paths to additional references used during runtime compilation of Razor files.
/// </summary>
/// <remarks>
/// By default, the runtime compiler <see cref="ICompilationReferencesProvider"/> to gather references
/// uses to compile a Razor file. This API allows providing additional references to the compiler.
/// </remarks>
public IList<string> AdditionalReferencePaths { get; } = new List<string>();
}
}

View File

@ -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> 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<IImportProjectFeature>().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();
}
}
}

View File

@ -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<RazorProjectItem> EnumerateItems(string basePath)
{
return Enumerable.Empty<RazorProjectItem>();
}
public override IEnumerable<RazorProjectItem> FindHierarchicalItems(string basePath, string path, string fileName)
{
return Enumerable.Empty<RazorProjectItem>();
}
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;
}
/// <inheritdoc />
public override string BasePath { get; }
/// <inheritdoc />
public override string FilePath { get; }
/// <inheritdoc />
public override bool Exists => false;
/// <inheritdoc />
public override string PhysicalPath => throw new NotSupportedException();
/// <inheritdoc />
public override Stream Read() => throw new NotSupportedException();
}
}
}
}

View File

@ -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")]

View File

@ -0,0 +1,100 @@
// <auto-generated />
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);
/// <summary>
/// One or more compilation failures occurred:
/// </summary>
internal static string CompilationFailed
{
get => GetString("CompilationFailed");
}
/// <summary>
/// One or more compilation failures occurred:
/// </summary>
internal static string FormatCompilationFailed()
=> GetString("CompilationFailed");
/// <summary>
/// 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.
/// </summary>
internal static string Compilation_MissingReferences
{
get => GetString("Compilation_MissingReferences");
}
/// <summary>
/// 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.
/// </summary>
internal static string FormatCompilation_MissingReferences(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("Compilation_MissingReferences"), p0);
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering.
/// </summary>
internal static string FileProvidersAreRequired
{
get => GetString("FileProvidersAreRequired");
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering.
/// </summary>
internal static string FormatFileProvidersAreRequired(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("FileProvidersAreRequired"), p0, p1, p2);
/// <summary>
/// Generated Code
/// </summary>
internal static string GeneratedCodeFileName
{
get => GetString("GeneratedCodeFileName");
}
/// <summary>
/// Generated Code
/// </summary>
internal static string FormatGeneratedCodeFileName()
=> GetString("GeneratedCodeFileName");
/// <summary>
/// The debug type specified in the dependency context could be parsed. The debug type value '{0}' is not supported.
/// </summary>
internal static string UnsupportedDebugInformationFormat
{
get => GetString("UnsupportedDebugInformationFormat");
}
/// <summary>
/// The debug type specified in the dependency context could be parsed. The debug type value '{0}' is not supported.
/// </summary>
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;
}
}
}

View File

@ -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<RazorProjectPageRouteModelProvider> _logger;
public RazorProjectPageRouteModelProvider(
RazorProjectFileSystem razorFileSystem,
IOptions<RazorPagesOptions> pagesOptionsAccessor,
ILoggerFactory loggerFactory)
{
_razorFileSystem = razorFileSystem;
_pagesOptions = pagesOptionsAccessor.Value;
_logger = loggerFactory.CreateLogger<RazorProjectPageRouteModelProvider>();
_routeModelFactory = new PageRouteModelFactory(_pagesOptions, _logger);
}
/// <remarks>
/// Ordered to execute after <see cref="CompiledPageRouteModelProvider"/>.
/// </remarks>
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);
}
}
}
}
}

View File

@ -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<MetadataReference> _compilationReferences;
public RazorReferenceManager(
ApplicationPartManager partManager,
IOptions<MvcRazorRuntimeCompilationOptions> options)
{
_partManager = partManager;
_options = options.Value;
}
public virtual IReadOnlyList<MetadataReference> CompilationReferences
{
get
{
return LazyInitializer.EnsureInitialized(
ref _compilationReferences,
ref _compilationReferencesInitialized,
ref _compilationReferencesLock,
GetCompilationReferences);
}
}
private IReadOnlyList<MetadataReference> GetCompilationReferences()
{
var referencePaths = GetReferencePaths();
return referencePaths
.Select(CreateMetadataReference)
.ToList();
}
// For unit testing
internal IEnumerable<string> GetReferencePaths()
{
var referencesFromApplicationParts = _partManager
.ApplicationParts
.OfType<ICompilationReferencesProvider>()
.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);
}
}
}
}

View File

@ -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<ILogger, string, Exception> _generatedCodeToAssemblyCompilationStart;
private static readonly Action<ILogger, string, double, Exception> _generatedCodeToAssemblyCompilationEnd;
private static readonly Action<ILogger, string, string[], Exception> _malformedPageDirective;
private static readonly Action<ILogger, string, Exception> _viewCompilerLocatedCompiledView;
private static readonly Action<ILogger, Exception> _viewCompilerNoCompiledViewsFound;
private static readonly Action<ILogger, string, Exception> _viewCompilerLocatedCompiledViewForPath;
private static readonly Action<ILogger, string, Exception> _viewCompilerRecompilingCompiledView;
private static readonly Action<ILogger, string, Exception> _viewCompilerCouldNotFindFileToCompileForPath;
private static readonly Action<ILogger, string, Exception> _viewCompilerFoundFileToCompileForPath;
private static readonly Action<ILogger, string, Exception> _viewCompilerInvalidatingCompiledFile;
private static readonly Action<ILogger, string, string, Exception> _viewLookupCacheMiss;
private static readonly Action<ILogger, string, string, Exception> _viewLookupCacheHit;
private static readonly Action<ILogger, string, Exception> _precompiledViewFound;
static MvcRazorLoggerExtensions()
{
_viewCompilerLocatedCompiledView = LoggerMessage.Define<string>(
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<string>(
LogLevel.Trace,
5,
"Located compiled view for view at path '{Path}'.");
_viewCompilerLocatedCompiledViewForPath = LoggerMessage.Define<string>(
LogLevel.Trace,
5,
"Located compiled view for view at path '{Path}'.");
_viewCompilerRecompilingCompiledView = LoggerMessage.Define<string>(
LogLevel.Trace,
6,
"Invalidating compiled view for view at path '{Path}'.");
_viewCompilerCouldNotFindFileToCompileForPath = LoggerMessage.Define<string>(
LogLevel.Trace,
7,
"Could not find a file for view at path '{Path}'.");
_viewCompilerFoundFileToCompileForPath = LoggerMessage.Define<string>(
LogLevel.Trace,
8,
"Found file at path '{Path}'.");
_viewCompilerInvalidatingCompiledFile = LoggerMessage.Define<string>(
LogLevel.Trace,
9,
"Invalidating compiled view at path '{Path}' with a file since the checksum did not match.");
_viewLookupCacheMiss = LoggerMessage.Define<string, string>(
LogLevel.Debug,
1,
"View lookup cache miss for view '{ViewName}' in controller '{ControllerName}'.");
_viewLookupCacheHit = LoggerMessage.Define<string, string>(
LogLevel.Debug,
2,
"View lookup cache hit for view '{ViewName}' in controller '{ControllerName}'.");
_precompiledViewFound = LoggerMessage.Define<string>(
LogLevel.Debug,
3,
"Using precompiled view for '{RelativePath}'.");
_generatedCodeToAssemblyCompilationStart = LoggerMessage.Define<string>(
LogLevel.Debug,
1,
"Compilation of the generated code for the Razor file at '{FilePath}' started.");
_generatedCodeToAssemblyCompilationEnd = LoggerMessage.Define<string, double>(
LogLevel.Debug,
2,
"Compilation of the generated code for the Razor file at '{FilePath}' completed in {ElapsedMilliseconds}ms.");
_malformedPageDirective = LoggerMessage.Define<string, string[]>(
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<RazorDiagnostic> 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);
}
}
}
}

View File

@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="CompilationFailed" xml:space="preserve">
<value>One or more compilation failures occurred:</value>
</data>
<data name="Compilation_MissingReferences" xml:space="preserve">
<value>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.</value>
</data>
<data name="FileProvidersAreRequired" xml:space="preserve">
<value>'{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering.</value>
</data>
<data name="GeneratedCodeFileName" xml:space="preserve">
<value>Generated Code</value>
</data>
<data name="UnsupportedDebugInformationFormat" xml:space="preserve">
<value>The debug type specified in the dependency context could be parsed. The debug type value '{0}' is not supported.</value>
</data>
</root>

View File

@ -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<MvcRazorRuntimeCompilationOptions> 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);
}
}
}

View File

@ -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<string, CompiledViewDescriptor> _precompiledViews;
private readonly ConcurrentDictionary<string, string> _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<CompiledViewDescriptor> 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<string, string>(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<string, CompiledViewDescriptor>(
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<CompiledViewDescriptor> 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<CompiledViewDescriptor> 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<CompiledViewDescriptor> OnCacheMiss(string normalizedPath)
{
ViewCompilerWorkItem item;
TaskCompletionSource<CompiledViewDescriptor> 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<CompiledViewDescriptor> 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<CompiledViewDescriptor>(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<IChangeToken>(), // 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<IChangeToken> expirationTokens = new List<IChangeToken>
{
_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<IChangeToken> GetExpirationTokens(CompiledViewDescriptor precompiledView)
{
var checksums = precompiledView.Item.GetChecksumMetadata();
var expirationTokens = new List<IChangeToken>(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<IChangeToken> 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<IImportProjectFeature>().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<IChangeToken> ExpirationTokens { get; set; }
public CompiledViewDescriptor Descriptor { get; set; }
}
}
}

View File

@ -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<RuntimeViewCompiler> _logger;
private readonly Func<IViewCompiler> _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<RuntimeViewCompiler>();
_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);
}
}
}

View File

@ -14,13 +14,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
/// <summary>
/// Caches the result of runtime compilation of Razor files for the duration of the application lifetime.
/// </summary>
internal class RazorViewCompiler : IViewCompiler
internal class DefaultViewCompiler : IViewCompiler
{
private readonly Dictionary<string, Task<CompiledViewDescriptor>> _compiledViews;
private readonly ConcurrentDictionary<string, string> _normalizedPathCache;
private readonly ILogger _logger;
public RazorViewCompiler(
public DefaultViewCompiler(
IList<CompiledViewDescriptor> compiledViews,
ILogger logger)
{

View File

@ -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<RazorViewCompiler>());
_compiler = new DefaultViewCompiler(feature.ViewDescriptors, loggerFactory.CreateLogger<DefaultViewCompiler>());
}
public IViewCompiler GetCompiler() => _compiler;

View File

@ -141,7 +141,7 @@ namespace Microsoft.Extensions.DependencyInjection
ServiceDescriptor.Transient<IConfigureOptions<RazorViewEngineOptions>, RazorViewEngineOptionsSetup>());
services.TryAddSingleton<IRazorViewEngine, RazorViewEngine>();
services.TryAddSingleton<IViewCompilerProvider, RazorViewCompilerProvider>();
services.TryAddSingleton<IViewCompilerProvider, DefaultViewCompilerProvider>();
// In the default scenario the following services are singleton by virtue of being initialized as part of
// creating the singleton RazorViewEngine instance.

View File

@ -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")]

View File

@ -3,17 +3,31 @@
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
// This is used to store the uncombined parts of the final page route
/// <summary>
/// Metadata used to construct an endpoint route to the page.
/// </summary>
// 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
{
/// <summary>
/// Initializes a new instance of <see cref="PageRouteMetadata"/>.
/// </summary>
/// <param name="pageRoute">The page route.</param>
/// <param name="routeTemplate">The route template specified by the page.</param>
public PageRouteMetadata(string pageRoute, string routeTemplate)
{
PageRoute = pageRoute;
RouteTemplate = routeTemplate;
}
/// <summary>
/// Gets the page route.
/// </summary>
public string PageRoute { get; }
/// <summary>
/// Gets the route template specified by the page.
/// </summary>
public string RouteTemplate { get; }
}
}

View File

@ -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<ILogger, string, Exception> _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<string>(
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;
}

View File

@ -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

View File

@ -18,8 +18,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
private static readonly Action<ILogger, string, string, Exception> _handlerMethodExecuted;
private static readonly Action<ILogger, string, Exception> _implicitHandlerMethodExecuted;
private static readonly Action<ILogger, object, Exception> _pageFilterShortCircuit;
private static readonly Action<ILogger, string, string[], Exception> _malformedPageDirective;
private static readonly Action<ILogger, string, Exception> _unsupportedAreaPath;
private static readonly Action<ILogger, Type, Exception> _notMostEffectiveFilter;
private static readonly Action<ILogger, string, string, string, Exception> _beforeExecutingMethodOnFilter;
private static readonly Action<ILogger, string, string, string, Exception> _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<string, string[]>(
LogLevel.Warning,
new EventId(104, "MalformedPageDirective"),
"The page directive at '{FilePath}' is malformed. Please fix the following issues: {Diagnostics}");
_notMostEffectiveFilter = LoggerMessage.Define<Type>(
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<string>(
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);
}
}
}
}

View File

@ -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<string, string>
{
{ "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();
}
}
}

View File

@ -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 =
@"<embdedded-layout>Hello from EmbeddedShared/_Partial
Hello from Shared/_EmbeddedPartial
<a href=""/EmbeddedViews"">Tag Helper Link</a>
</embdedded-layout>";
// Act
var body = await Client.GetStringAsync(requestPath);
// Assert
Assert.Equal(expected, body.Trim(), ignoreLineEndingDifferences: true);
}
[Fact]
public async Task LayoutValueIsPassedBetweenNestedViewStarts()
{

View File

@ -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<IHostingEnvironment>(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<IHostingEnvironment>();
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<IHostingEnvironment>();
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<IHostingEnvironment>();
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<IHostingEnvironment>(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<IHostingEnvironment>(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<IHostingEnvironment>();
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<IHostingEnvironment>();
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<IHostingEnvironment>();
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<IHostingEnvironment>();
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<IHostingEnvironment>();
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<IHostingEnvironment>();
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<IHostingEnvironment>();
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<IHostingEnvironment>();
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<IHostingEnvironment>();
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<IHostingEnvironment>();
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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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, "<span name=\"@(User.Id\">");
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<RazorSourceDocument>()),
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, "<span name=\"@(User.Id\">", 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)
{
<span>
}
</span>";
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;
}
}
}

View File

@ -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<IViewCompilerProvider, DefaultViewCompilerProvider>();
// Act
RazorRuntimeCompilationMvcCoreBuilderExtensions.AddServices(services);
// Assert
var serviceDescriptor = Assert.Single(services, service => service.ServiceType == typeof(IViewCompilerProvider));
Assert.Equal(typeof(RuntimeViewCompilerProvider), serviceDescriptor.ImplementationType);
}
}
}

View File

@ -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<IHostingEnvironment>(e => e.ContentRootPath == contentRootPath));
return fileSystem;
}
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" />
<ProjectReference Include="..\Microsoft.AspNetCore.Mvc.Views.TestCommon\Microsoft.AspNetCore.Mvc.Views.TestCommon.csproj" />
</ItemGroup>
</Project>

View File

@ -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<ApplicationPart>();
part.As<ICompilationReferencesProvider>()
.Setup(p => p.GetReferencePaths())
.Returns(new[] { ApplicationPartReferencePath });
applicationPartManager.ApplicationParts.Add(part.Object);
return applicationPartManager;
}
}
}

View File

@ -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<InvalidOperationException>(
() => fileProvider.FileProvider);
Assert.Equal(expected, exception.Message);
}
}
}

View File

@ -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<InvalidTimeZoneException>(
() => compiler.CompileAsync(path));
Assert.Same(exception, actual);
// Act and Assert - 2
compiler.Compile = _ => throw new Exception("Shouldn't be called");
actual = await Assert.ThrowsAsync<InvalidTimeZoneException>(
() => 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<CompilationFailedException>(() => 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<CompilationFailedException>(() => 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<IHostingEnvironment>())
{
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<IHostingEnvironment>())
{
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<CompiledViewDescriptor> 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<CompiledViewDescriptor>();
var hostingEnvironment = Mock.Of<IHostingEnvironment>(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<CompiledViewDescriptor> precompiledViews,
Func<string, CompiledViewDescriptor> 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<string, CompiledViewDescriptor> 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;
}
}
}

View File

@ -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<DirectoryNode> Directories { get; } = new List<DirectoryNode>();
public List<FileNode> Files { get; } = new List<FileNode>();
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<RazorProjectItem> 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;
}
}
}

View File

@ -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; }
}
}

View File

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

View File

@ -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;
}
}
}

View File

@ -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<MetadataReference>();
}
public override IReadOnlyList<MetadataReference> CompilationReferences { get; }
}
}

View File

@ -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<RazorProjectItem> EnumerateItems(string basePath)
{
basePath = NormalizeAndEnsureValidPath(basePath);
var directory = _root.GetDirectory(basePath);
return directory?.EnumerateItems() ?? Enumerable.Empty<RazorProjectItem>();
}
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));
}
}
}

View File

@ -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<CompiledViewDescriptor> compiledViews) :
base(compiledViews, NullLogger.Instance)

View File

@ -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)

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
@ -10,6 +10,7 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc" />
<Reference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" />
<!-- Faking like we had Razor-on-Build (RZC) -->
<ProjectReference Include="..\RazorBuildWebSite.Views\RazorBuildWebSite.Views.csproj" />

View File

@ -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);
}

View File

@ -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<string, TestFileInfo> _content = new Dictionary<string, TestFileInfo>()
{
{
"/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")
},
{

View File

@ -1,4 +1,4 @@
RazorBuildWebSite
===
This web site tests how the Razor view engine interacts with pre-built Razor assemblies.
This web site tests how the Razor view engine interacts with pre-built and runtime compiled Razor assemblies.

View File

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

View File

@ -1 +0,0 @@
<embdedded-layout>@RenderBody()</embdedded-layout>

View File

@ -1 +0,0 @@
Hello from EmbeddedShared/_Partial

View File

@ -1 +0,0 @@
Hello from EmbeddedHome\EmbeddedPartial

View File

@ -1,3 +0,0 @@
@(await Html.PartialAsync("../EmbeddedShared/_Partial.cshtml"))
@(await Html.PartialAsync("_EmbeddedPartial"))
<a asp-controller="EmbeddedViews" asp-action="Index">Tag Helper Link</a>

View File

@ -1,2 +0,0 @@
@{ Layout = "../EmbeddedShared/_Layout"; }
@(await Html.PartialAsync("./EmbeddedPartial"))

View File

@ -1 +0,0 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -1 +0,0 @@
@{ Layout = "/Views/EmbeddedShared/_Layout.cshtml"; }

View File

@ -1 +0,0 @@
Hello from Shared/_EmbeddedPartial

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
@ -6,10 +6,6 @@
<IsTestAssetProject>true</IsTestAssetProject>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="EmbeddedResources\**" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc" />

View File

@ -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<ITagHelperComponent, TestHeadTagHelperComponent>();
services.AddSingleton<ITagHelperComponent, TestBodyTagHelperComponent>();