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.Blazor.Mono" Version="$(MicrosoftAspNetCoreBlazorMonoPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.AspNetCore.Mvc.Razor.Extensions" Version="$(MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.AspNetCore.Mvc.Razor.Extensions" Version="$(MicrosoftAspNetCoreMvcRazorExtensionsPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="$(MicrosoftAspNetCoreRazorLanguagePackageVersion)" />
|
<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.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.Azure.KeyVault" Version="$(MicrosoftAzureKeyVaultPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.Azure.KeyVault" Version="$(MicrosoftAzureKeyVaultPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.Build.Framework" Version="$(MicrosoftBuildFrameworkPackageVersion)" />
|
<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.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.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.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.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.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" />
|
<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
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio 15
|
# Visual Studio Version 16
|
||||||
VisualStudioVersion = 15.0.28307.136
|
VisualStudioVersion = 16.0.28509.92
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|
@ -274,6 +274,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataPr
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenericHostWebSite", "test\WebSites\GenericHostWebSite\GenericHostWebSite.csproj", "{D9BE3E50-5CE8-4D8D-BA19-AA219D009752}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenericHostWebSite", "test\WebSites\GenericHostWebSite\GenericHostWebSite.csproj", "{D9BE3E50-5CE8-4D8D-BA19-AA219D009752}"
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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|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.ActiveCfg = Release|Any CPU
|
||||||
{D9BE3E50-5CE8-4D8D-BA19-AA219D009752}.Release|x86.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -1710,6 +1752,9 @@ Global
|
||||||
{848E2620-EAF9-4BFD-8810-4AF71E09A8FB} = {9328599D-A7AF-43BC-BE08-7503DF9B8CE6}
|
{848E2620-EAF9-4BFD-8810-4AF71E09A8FB} = {9328599D-A7AF-43BC-BE08-7503DF9B8CE6}
|
||||||
{C75C6E51-4FFD-4902-8739-9109E51875B4} = {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}
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {63D344F6-F86D-40E6-85B9-0AABBE338C4A}
|
SolutionGuid = {63D344F6-F86D-40E6-85B9-0AABBE338C4A}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.AspNetCore.Mvc" />
|
<Reference Include="Microsoft.AspNetCore.Mvc" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
|
<Reference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
|
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />
|
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ namespace MvcSandbox
|
||||||
{
|
{
|
||||||
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
|
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.
|
// 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>
|
/// <summary>
|
||||||
/// Caches the result of runtime compilation of Razor files for the duration of the application lifetime.
|
/// Caches the result of runtime compilation of Razor files for the duration of the application lifetime.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class RazorViewCompiler : IViewCompiler
|
internal class DefaultViewCompiler : IViewCompiler
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, Task<CompiledViewDescriptor>> _compiledViews;
|
private readonly Dictionary<string, Task<CompiledViewDescriptor>> _compiledViews;
|
||||||
private readonly ConcurrentDictionary<string, string> _normalizedPathCache;
|
private readonly ConcurrentDictionary<string, string> _normalizedPathCache;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public RazorViewCompiler(
|
public DefaultViewCompiler(
|
||||||
IList<CompiledViewDescriptor> compiledViews,
|
IList<CompiledViewDescriptor> compiledViews,
|
||||||
ILogger logger)
|
ILogger logger)
|
||||||
{
|
{
|
||||||
|
|
@ -6,18 +6,18 @@ using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
|
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,
|
ApplicationPartManager applicationPartManager,
|
||||||
ILoggerFactory loggerFactory)
|
ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
var feature = new ViewsFeature();
|
var feature = new ViewsFeature();
|
||||||
applicationPartManager.PopulateFeature(feature);
|
applicationPartManager.PopulateFeature(feature);
|
||||||
|
|
||||||
_compiler = new RazorViewCompiler(feature.ViewDescriptors, loggerFactory.CreateLogger<RazorViewCompiler>());
|
_compiler = new DefaultViewCompiler(feature.ViewDescriptors, loggerFactory.CreateLogger<DefaultViewCompiler>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public IViewCompiler GetCompiler() => _compiler;
|
public IViewCompiler GetCompiler() => _compiler;
|
||||||
|
|
@ -141,7 +141,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
ServiceDescriptor.Transient<IConfigureOptions<RazorViewEngineOptions>, RazorViewEngineOptionsSetup>());
|
ServiceDescriptor.Transient<IConfigureOptions<RazorViewEngineOptions>, RazorViewEngineOptionsSetup>());
|
||||||
|
|
||||||
services.TryAddSingleton<IRazorViewEngine, RazorViewEngine>();
|
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
|
// In the default scenario the following services are singleton by virtue of being initialized as part of
|
||||||
// creating the singleton RazorViewEngine instance.
|
// 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.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.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.RazorPages.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.TagHelpers.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.TagHelpers.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,31 @@
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
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
|
// 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)
|
public PageRouteMetadata(string pageRoute, string routeTemplate)
|
||||||
{
|
{
|
||||||
PageRoute = pageRoute;
|
PageRoute = pageRoute;
|
||||||
RouteTemplate = routeTemplate;
|
RouteTemplate = routeTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the page route.
|
||||||
|
/// </summary>
|
||||||
public string PageRoute { get; }
|
public string PageRoute { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the route template specified by the page.
|
||||||
|
/// </summary>
|
||||||
public string RouteTemplate { get; }
|
public string RouteTemplate { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,27 @@ using System.IO;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor;
|
using Microsoft.AspNetCore.Mvc.Razor;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
{
|
{
|
||||||
internal class PageRouteModelFactory
|
internal class PageRouteModelFactory
|
||||||
{
|
{
|
||||||
|
private static readonly Action<ILogger, string, Exception> _unsupportedAreaPath;
|
||||||
|
|
||||||
private static readonly string IndexFileName = "Index" + RazorViewEngine.ViewExtension;
|
private static readonly string IndexFileName = "Index" + RazorViewEngine.ViewExtension;
|
||||||
private readonly RazorPagesOptions _options;
|
private readonly RazorPagesOptions _options;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly string _normalizedRootDirectory;
|
private readonly string _normalizedRootDirectory;
|
||||||
private readonly string _normalizedAreaRootDirectory;
|
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(
|
public PageRouteModelFactory(
|
||||||
RazorPagesOptions options,
|
RazorPagesOptions options,
|
||||||
ILogger logger)
|
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.
|
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.
|
!relativePath.StartsWith(_normalizedAreaRootDirectory, StringComparison.OrdinalIgnoreCase)) // The path must start with area root.
|
||||||
{
|
{
|
||||||
_logger.UnsupportedAreaPath(relativePath);
|
_unsupportedAreaPath(_logger, relativePath, null);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,7 +113,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
var areaEndIndex = relativePath.IndexOf('/', startIndex: areaRootEndIndex + 1);
|
var areaEndIndex = relativePath.IndexOf('/', startIndex: areaRootEndIndex + 1);
|
||||||
if (areaEndIndex == -1 || areaEndIndex == relativePath.Length)
|
if (areaEndIndex == -1 || areaEndIndex == relativePath.Length)
|
||||||
{
|
{
|
||||||
_logger.UnsupportedAreaPath(relativePath);
|
_unsupportedAreaPath(_logger, relativePath, null);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,7 +121,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
// Ensure the next token is the "Pages" directory
|
// Ensure the next token is the "Pages" directory
|
||||||
if (string.Compare(relativePath, areaEndIndex, AreaPagesRoot, 0, AreaPagesRoot.Length, StringComparison.OrdinalIgnoreCase) != 0)
|
if (string.Compare(relativePath, areaEndIndex, AreaPagesRoot, 0, AreaPagesRoot.Length, StringComparison.OrdinalIgnoreCase) != 0)
|
||||||
{
|
{
|
||||||
_logger.UnsupportedAreaPath(relativePath);
|
_unsupportedAreaPath(_logger, relativePath, null);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
using Microsoft.AspNetCore.Routing;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
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, string, Exception> _handlerMethodExecuted;
|
||||||
private static readonly Action<ILogger, string, Exception> _implicitHandlerMethodExecuted;
|
private static readonly Action<ILogger, string, Exception> _implicitHandlerMethodExecuted;
|
||||||
private static readonly Action<ILogger, object, Exception> _pageFilterShortCircuit;
|
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, Type, Exception> _notMostEffectiveFilter;
|
||||||
private static readonly Action<ILogger, string, string, string, Exception> _beforeExecutingMethodOnFilter;
|
private static readonly Action<ILogger, string, string, string, Exception> _beforeExecutingMethodOnFilter;
|
||||||
private static readonly Action<ILogger, string, string, string, Exception> _afterExecutingMethodOnFilter;
|
private static readonly Action<ILogger, string, string, string, Exception> _afterExecutingMethodOnFilter;
|
||||||
|
|
@ -53,11 +51,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
||||||
new EventId(3, "PageFilterShortCircuited"),
|
new EventId(3, "PageFilterShortCircuited"),
|
||||||
"Request was short circuited at page filter '{PageFilter}'.");
|
"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>(
|
_notMostEffectiveFilter = LoggerMessage.Define<Type>(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
new EventId(1, "NotMostEffectiveFilter"),
|
new EventId(1, "NotMostEffectiveFilter"),
|
||||||
|
|
@ -72,11 +65,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
||||||
LogLevel.Trace,
|
LogLevel.Trace,
|
||||||
new EventId(2, "AfterExecutingMethodOnFilter"),
|
new EventId(2, "AfterExecutingMethodOnFilter"),
|
||||||
"{FilterType}: After executing {Method} on filter {Filter}.");
|
"{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)
|
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);
|
_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.
|
// 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.
|
// 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;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -17,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
|
|
||||||
public HttpClient Client { get; }
|
public HttpClient Client { get; }
|
||||||
|
|
||||||
[Fact(Skip = "https://github.com/aspnet/Mvc/issues/8753")]
|
[Fact]
|
||||||
public async Task Rzc_LocalPageWithDifferentContent_IsUsed()
|
public async Task Rzc_LocalPageWithDifferentContent_IsUsed()
|
||||||
{
|
{
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -29,7 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
Assert.Equal("Hello from runtime-compiled rzc page!", responseBody.Trim());
|
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()
|
public async Task Rzc_LocalViewWithDifferentContent_IsUsed()
|
||||||
{
|
{
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -53,5 +55,89 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
Assert.Equal("Hello from buildtime-compiled rzc view!", responseBody.Trim());
|
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);
|
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]
|
[Fact]
|
||||||
public async Task LayoutValueIsPassedBetweenNestedViewStarts()
|
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
|
namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
|
||||||
{
|
{
|
||||||
public class RazorViewCompilerTest
|
public class DefaultViewCompilerTest
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CompileAsync_ReturnsResultWithNullAttribute_IfFileIsNotFoundInFileSystem()
|
public async Task CompileAsync_ReturnsResultWithNullAttribute_IfFileIsNotFoundInFileSystem()
|
||||||
|
|
@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
|
||||||
return viewCompiler;
|
return viewCompiler;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestRazorViewCompiler : RazorViewCompiler
|
private class TestRazorViewCompiler : DefaultViewCompiler
|
||||||
{
|
{
|
||||||
public TestRazorViewCompiler(IList<CompiledViewDescriptor> compiledViews) :
|
public TestRazorViewCompiler(IList<CompiledViewDescriptor> compiledViews) :
|
||||||
base(compiledViews, NullLogger.Instance)
|
base(compiledViews, NullLogger.Instance)
|
||||||
|
|
@ -3,11 +3,11 @@
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
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]
|
[HttpPost]
|
||||||
public IActionResult Update([FromServices] UpdateableFileProvider fileProvider, string path, string content)
|
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>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.AspNetCore.Mvc" />
|
<Reference Include="Microsoft.AspNetCore.Mvc" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" />
|
||||||
|
|
||||||
<!-- Faking like we had Razor-on-Build (RZC) -->
|
<!-- Faking like we had Razor-on-Build (RZC) -->
|
||||||
<ProjectReference Include="..\RazorBuildWebSite.Views\RazorBuildWebSite.Views.csproj" />
|
<ProjectReference Include="..\RazorBuildWebSite.Views\RazorBuildWebSite.Views.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,11 @@ namespace RazorBuildWebSite
|
||||||
{
|
{
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
|
var fileProvider = new UpdateableFileProvider();
|
||||||
|
services.AddSingleton(fileProvider);
|
||||||
|
|
||||||
services.AddMvc()
|
services.AddMvc()
|
||||||
|
.AddRazorRuntimeCompilation(options => options.FileProviders.Add(fileProvider))
|
||||||
.SetCompatibilityVersion(CompatibilityVersion.Latest);
|
.SetCompatibilityVersion(CompatibilityVersion.Latest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ using System.Threading;
|
||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.Extensions.FileProviders;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace RazorWebSite
|
namespace RazorBuildWebSite
|
||||||
{
|
{
|
||||||
public class UpdateableFileProvider : IFileProvider
|
public class UpdateableFileProvider : IFileProvider
|
||||||
{
|
{
|
||||||
|
|
@ -19,15 +19,11 @@ namespace RazorWebSite
|
||||||
private readonly Dictionary<string, TestFileInfo> _content = new Dictionary<string, TestFileInfo>()
|
private readonly Dictionary<string, TestFileInfo> _content = new Dictionary<string, TestFileInfo>()
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
"/Views/UpdateableIndex/_ViewImports.cshtml",
|
"/Views/UpdateableViews/_ViewImports.cshtml",
|
||||||
new TestFileInfo(string.Empty)
|
new TestFileInfo(string.Empty)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"/Views/UpdateableIndex/Index.cshtml",
|
"/Views/UpdateableViews/Index.cshtml",
|
||||||
new TestFileInfo(@"@Html.Partial(""../UpdateableShared/_Partial.cshtml"")")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"/Views/UpdateableShared/_Partial.cshtml",
|
|
||||||
new TestFileInfo("Original content")
|
new TestFileInfo("Original content")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
RazorBuildWebSite
|
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>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
|
|
@ -6,10 +6,6 @@
|
||||||
<IsTestAssetProject>true</IsTestAssetProject>
|
<IsTestAssetProject>true</IsTestAssetProject>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Include="EmbeddedResources\**" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.AspNetCore.Mvc" />
|
<Reference Include="Microsoft.AspNetCore.Mvc" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,12 @@
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Reflection;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Localization;
|
using Microsoft.AspNetCore.Localization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor;
|
using Microsoft.AspNetCore.Mvc.Razor;
|
||||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.FileProviders;
|
|
||||||
|
|
||||||
namespace RazorWebSite
|
namespace RazorWebSite
|
||||||
{
|
{
|
||||||
|
|
@ -18,8 +16,6 @@ namespace RazorWebSite
|
||||||
{
|
{
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
var updateableFileProvider = new UpdateableFileProvider();
|
|
||||||
services.AddSingleton(updateableFileProvider);
|
|
||||||
services.AddSingleton<ITagHelperComponent, TestHeadTagHelperComponent>();
|
services.AddSingleton<ITagHelperComponent, TestHeadTagHelperComponent>();
|
||||||
services.AddSingleton<ITagHelperComponent, TestBodyTagHelperComponent>();
|
services.AddSingleton<ITagHelperComponent, TestBodyTagHelperComponent>();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue