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:
parent
1ecc8dd9c6
commit
0f072a9565
|
|
@ -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)" />
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")]
|
||||
100
src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Properties/Resources.Designer.cs
generated
Normal file
100
src/Mvc/src/Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation/Properties/Resources.Designer.cs
generated
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
@ -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;
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
},
|
||||
{
|
||||
|
|
@ -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.
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<embdedded-layout>@RenderBody()</embdedded-layout>
|
||||
|
|
@ -1 +0,0 @@
|
|||
Hello from EmbeddedShared/_Partial
|
||||
|
|
@ -1 +0,0 @@
|
|||
Hello from EmbeddedHome\EmbeddedPartial
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
@{ Layout = "../EmbeddedShared/_Layout"; }
|
||||
@(await Html.PartialAsync("./EmbeddedPartial"))
|
||||
|
|
@ -1 +0,0 @@
|
|||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
|
|
@ -1 +0,0 @@
|
|||
@{ Layout = "/Views/EmbeddedShared/_Layout.cshtml"; }
|
||||
|
|
@ -1 +0,0 @@
|
|||
Hello from Shared/_EmbeddedPartial
|
||||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue