Add support for recompilation

This commit is contained in:
Ryan Nowak 2018-01-06 16:08:52 -08:00 committed by Pranav K
parent 73bd09dc1c
commit d58d0f917f
38 changed files with 1870 additions and 327 deletions

45
Mvc.sln
View File

@ -155,6 +155,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{FDC66952-A3EA-4074-899E-C29816BF7C1F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RazorBuildWebSite", "test\WebSites\RazorBuildWebSite\RazorBuildWebSite.csproj", "{BF8A3392-C3D2-4813-855A-E906564600E1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RazorBuildWebSite.PrecompiledViews", "test\WebSites\RazorBuildWebSite.PrecompiledViews\RazorBuildWebSite.PrecompiledViews.csproj", "{856D7E25-E033-477D-9ABD-0B50CF428C80}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RazorBuildWebSite.Views", "test\WebSites\RazorBuildWebSite.Views\RazorBuildWebSite.Views.csproj", "{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -791,6 +797,42 @@ Global
{7500B228-1769-4CFB-A571-3DFAC6678A06}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{7500B228-1769-4CFB-A571-3DFAC6678A06}.Release|x86.ActiveCfg = Release|Any CPU
{7500B228-1769-4CFB-A571-3DFAC6678A06}.Release|x86.Build.0 = Release|Any CPU
{BF8A3392-C3D2-4813-855A-E906564600E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BF8A3392-C3D2-4813-855A-E906564600E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BF8A3392-C3D2-4813-855A-E906564600E1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{BF8A3392-C3D2-4813-855A-E906564600E1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{BF8A3392-C3D2-4813-855A-E906564600E1}.Debug|x86.ActiveCfg = Debug|Any CPU
{BF8A3392-C3D2-4813-855A-E906564600E1}.Debug|x86.Build.0 = Debug|Any CPU
{BF8A3392-C3D2-4813-855A-E906564600E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BF8A3392-C3D2-4813-855A-E906564600E1}.Release|Any CPU.Build.0 = Release|Any CPU
{BF8A3392-C3D2-4813-855A-E906564600E1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{BF8A3392-C3D2-4813-855A-E906564600E1}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{BF8A3392-C3D2-4813-855A-E906564600E1}.Release|x86.ActiveCfg = Release|Any CPU
{BF8A3392-C3D2-4813-855A-E906564600E1}.Release|x86.Build.0 = Release|Any CPU
{856D7E25-E033-477D-9ABD-0B50CF428C80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{856D7E25-E033-477D-9ABD-0B50CF428C80}.Debug|Any CPU.Build.0 = Debug|Any CPU
{856D7E25-E033-477D-9ABD-0B50CF428C80}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{856D7E25-E033-477D-9ABD-0B50CF428C80}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{856D7E25-E033-477D-9ABD-0B50CF428C80}.Debug|x86.ActiveCfg = Debug|Any CPU
{856D7E25-E033-477D-9ABD-0B50CF428C80}.Debug|x86.Build.0 = Debug|Any CPU
{856D7E25-E033-477D-9ABD-0B50CF428C80}.Release|Any CPU.ActiveCfg = Release|Any CPU
{856D7E25-E033-477D-9ABD-0B50CF428C80}.Release|Any CPU.Build.0 = Release|Any CPU
{856D7E25-E033-477D-9ABD-0B50CF428C80}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{856D7E25-E033-477D-9ABD-0B50CF428C80}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{856D7E25-E033-477D-9ABD-0B50CF428C80}.Release|x86.ActiveCfg = Release|Any CPU
{856D7E25-E033-477D-9ABD-0B50CF428C80}.Release|x86.Build.0 = Release|Any CPU
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}.Debug|x86.ActiveCfg = Debug|Any CPU
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}.Debug|x86.Build.0 = Debug|Any CPU
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}.Release|Any CPU.Build.0 = Release|Any CPU
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}.Release|x86.ActiveCfg = Release|Any CPU
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -853,6 +895,9 @@ Global
{4BA6EC9A-B6D9-41F2-BFDA-D82B22D80352} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{F16CEE0D-A28E-43BD-802F-99BAFE4BA7CE} = {FDC66952-A3EA-4074-899E-C29816BF7C1F}
{7500B228-1769-4CFB-A571-3DFAC6678A06} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
{BF8A3392-C3D2-4813-855A-E906564600E1} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{856D7E25-E033-477D-9ABD-0B50CF428C80} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{8916DDCA-EC2A-4193-B9F3-78CAA1A96D5A} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {63D344F6-F86D-40E6-85B9-0AABBE338C4A}

View File

@ -1,13 +1,60 @@
// 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.Razor.Internal;
using Microsoft.AspNetCore.Razor.Hosting;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
{
/// <summary>
/// Represents a compiled Razor View or Page.
/// </summary>
public class CompiledViewDescriptor
{
/// <summary>
/// Creates a new <see cref="CompiledViewDescriptor"/>.
/// </summary>
public CompiledViewDescriptor()
{
}
/// <summary>
/// Creates a new <see cref="CompiledViewDescriptor"/>. At least one of <paramref name="attribute"/> or
/// <paramref name="item"/> must be non-<c>null</c>.
/// </summary>
/// <param name="item">The <see cref="RazorCompiledItem"/>.</param>
/// <param name="attribute">The <see cref="RazorViewAttribute"/>.</param>
public CompiledViewDescriptor(RazorCompiledItem item, RazorViewAttribute attribute)
{
if (item == null && attribute == null)
{
// We require at least one of these to be specified.
throw new ArgumentException(Resources.FormatCompiledViewDescriptor_NoData(nameof(item), nameof(attribute)));
}
Item = item;
//
// For now we expect that MVC views and pages will still have either:
// [RazorView(...)] or
// [RazorPage(...)].
//
// In theory we could look at the 'Item.Kind' to determine what kind of thing we're dealing
// with, but for compat reasons we're basing it on ViewAttribute since that's what 2.0 had.
ViewAttribute = attribute;
// We don't have access to the file provider here so we can't check if the files
// even exist or what their checksums are. For now leave this empty, it will be updated
// later.
ExpirationTokens = Array.Empty<IChangeToken>();
RelativePath = ViewPath.NormalizePath(item?.Identifier ?? attribute.Path);
IsPrecompiled = true;
}
/// <summary>
/// The normalized application relative path of the view.
/// </summary>
@ -27,5 +74,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
/// Gets a value that determines if the view is precompiled.
/// </summary>
public bool IsPrecompiled { get; set; }
/// <summary>
/// Gets the <see cref="RazorCompiledItem"/> descriptor for this view.
/// </summary>
public RazorCompiledItem Item { get; set; }
/// <summary>
/// Gets the type of the compiled item.
/// </summary>
public Type Type => Item?.Type ?? ViewAttribute?.ViewType;
}
}

View File

@ -7,8 +7,7 @@ using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.Extensions.Primitives;
using Microsoft.AspNetCore.Razor.Hosting;
namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
{
@ -19,28 +18,81 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
{
public static readonly string PrecompiledViewsAssemblySuffix = ".PrecompiledViews";
public static readonly IReadOnlyList<string> ViewAssemblySuffixes = new string[]
{
PrecompiledViewsAssemblySuffix,
".Views",
};
/// <inheritdoc />
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewsFeature feature)
{
foreach (var assemblyPart in parts.OfType<AssemblyPart>())
{
var viewAttributes = GetViewAttributes(assemblyPart);
foreach (var attribute in viewAttributes)
{
var relativePath = ViewPath.NormalizePath(attribute.Path);
var viewDescriptor = new CompiledViewDescriptor
{
ExpirationTokens = Array.Empty<IChangeToken>(),
RelativePath = relativePath,
ViewAttribute = attribute,
IsPrecompiled = true,
};
var attributes = GetViewAttributes(assemblyPart);
var items = LoadItems(assemblyPart);
feature.ViewDescriptors.Add(viewDescriptor);
var merged = Merge(items, attributes);
foreach (var entry in merged)
{
feature.ViewDescriptors.Add(new CompiledViewDescriptor(entry.item, entry.attribute));
}
}
}
private ICollection<(RazorCompiledItem item, RazorViewAttribute attribute)> Merge(
IReadOnlyList<RazorCompiledItem> items,
IEnumerable<RazorViewAttribute> attributes)
{
// This code is a intentionally defensive. We assume that it's possible to have duplicates
// of attributes, and also items that have a single kind of metadata, but not the other.
var dictionary = new Dictionary<string, (RazorCompiledItem item, RazorViewAttribute attribute)>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < items.Count; i++)
{
var item = items[i];
if (!dictionary.TryGetValue(item.Identifier, out var entry))
{
dictionary.Add(item.Identifier, (item, null));
}
else if (entry.item == null)
{
dictionary[item.Identifier] = (item, entry.attribute);
}
}
foreach (var attribute in attributes)
{
if (!dictionary.TryGetValue(attribute.Path, out var entry))
{
dictionary.Add(attribute.Path, (null, attribute));
}
else if (entry.attribute == null)
{
dictionary[attribute.Path] = (entry.item, attribute);
}
}
return dictionary.Values;
}
protected virtual IReadOnlyList<RazorCompiledItem> LoadItems(AssemblyPart assemblyPart)
{
if (assemblyPart == null)
{
throw new ArgumentNullException(nameof(assemblyPart));
}
var viewAssembly = GetViewAssembly(assemblyPart);
if (viewAssembly != null)
{
var loader = new RazorCompiledItemLoader();
return loader.LoadItems(viewAssembly);
}
return Array.Empty<RazorCompiledItem>();
}
/// <summary>
/// Gets the sequence of <see cref="RazorViewAttribute"/> instances associated with the specified <paramref name="assemblyPart"/>.
/// </summary>
@ -53,7 +105,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
throw new ArgumentNullException(nameof(assemblyPart));
}
var featureAssembly = GetFeatureAssembly(assemblyPart);
var featureAssembly = GetViewAssembly(assemblyPart);
if (featureAssembly != null)
{
return featureAssembly.GetCustomAttributes<RazorViewAttribute>();
@ -62,29 +114,28 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
return Enumerable.Empty<RazorViewAttribute>();
}
private static Assembly GetFeatureAssembly(AssemblyPart assemblyPart)
private Assembly GetViewAssembly(AssemblyPart assemblyPart)
{
if (assemblyPart.Assembly.IsDynamic || string.IsNullOrEmpty(assemblyPart.Assembly.Location))
{
return null;
}
var precompiledAssemblyFileName = assemblyPart.Assembly.GetName().Name
+ PrecompiledViewsAssemblySuffix
+ ".dll";
var precompiledAssemblyFilePath = Path.Combine(
Path.GetDirectoryName(assemblyPart.Assembly.Location),
precompiledAssemblyFileName);
if (File.Exists(precompiledAssemblyFilePath))
for (var i = 0; i < ViewAssemblySuffixes.Count; i++)
{
try
var fileName = assemblyPart.Assembly.GetName().Name + ViewAssemblySuffixes[i] + ".dll";
var filePath = Path.Combine(Path.GetDirectoryName(assemblyPart.Assembly.Location), fileName);
if (File.Exists(filePath))
{
return Assembly.LoadFile(precompiledAssemblyFilePath);
}
catch (FileLoadException)
{
// Don't throw if assembly cannot be loaded. This can happen if the file is not a managed assembly.
try
{
return Assembly.LoadFile(filePath);
}
catch (FileLoadException)
{
// Don't throw if assembly cannot be loaded. This can happen if the file is not a managed assembly.
}
}
}

View File

@ -0,0 +1,122 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using Microsoft.AspNetCore.Razor.Hosting;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{
public 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(RazorProject project, RazorCompiledItem item)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
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 = project.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 = project.GetItem(checksum.Identifier);
if (!importItem.Exists)
{
// Import file doesn't exist - assume invalid.
return false;
}
sourceDocument = RazorSourceDocument.ReadFrom(importItem);
if (!string.Equals(sourceDocument.GetChecksumAlgorithm(), checksum.ChecksumAlgorithm) ||
!ChecksumsEqual(checksum.Checksum, sourceDocument.GetChecksum()))
{
// Import file exists, but checksums not equal.
return false;
}
}
return true;
}
private static bool ChecksumsEqual(string checksum, byte[] bytes)
{
if (bytes.Length * 2 != checksum.Length)
{
return false;
}
for (var i = 0; i < bytes.Length; i++)
{
var text = bytes[i].ToString("x2");
if (checksum[i * 2] != text[0] || checksum[i * 2 + 1] != text[1])
{
return false;
}
}
return true;
}
}
}

View File

@ -43,12 +43,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var compileTask = Compiler.CompileAsync(relativePath);
var viewDescriptor = compileTask.GetAwaiter().GetResult();
if (viewDescriptor.ViewAttribute != null)
{
var compiledType = viewDescriptor.ViewAttribute.ViewType;
var newExpression = Expression.New(compiledType);
var pathProperty = compiledType.GetTypeInfo().GetProperty(nameof(IRazorPage.Path));
var viewType = viewDescriptor.Type;
if (viewType != null)
{
var newExpression = Expression.New(viewType);
var pathProperty = viewType.GetTypeInfo().GetProperty(nameof(IRazorPage.Path));
// Generate: page.Path = relativePath;
// Use the normalized path specified from the result.

View File

@ -6,16 +6,19 @@ 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.Text;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{
@ -25,8 +28,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
public class RazorViewCompiler : IViewCompiler
{
private readonly object _cacheLock = new object();
private readonly Dictionary<string, Task<CompiledViewDescriptor>> _precompiledViewLookup;
private readonly ConcurrentDictionary<string, string> _normalizedPathLookup;
private readonly Dictionary<string, CompiledViewDescriptor> _precompiledViews;
private readonly ConcurrentDictionary<string, string> _normalizedPathCache;
private readonly IFileProvider _fileProvider;
private readonly RazorTemplateEngine _templateEngine;
private readonly Action<RoslynCompilationContext> _compilationCallback;
@ -78,26 +81,33 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
_compilationCallback = compilationCallback;
_logger = logger;
_normalizedPathLookup = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
_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());
_precompiledViewLookup = new Dictionary<string, Task<CompiledViewDescriptor>>(
// We need to validate that the all of the precompiled views are unique by path (case-insenstive).
// 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)
{
if (_precompiledViewLookup.TryGetValue(precompiledView.RelativePath, out var otherValue))
if (_precompiledViews.TryGetValue(precompiledView.RelativePath, out var otherValue))
{
var message = string.Join(
Environment.NewLine,
Resources.RazorViewCompiler_ViewPathsDifferOnlyInCase,
otherValue.Result.RelativePath,
otherValue.RelativePath,
precompiledView.RelativePath);
throw new InvalidOperationException(message);
}
_precompiledViewLookup.Add(precompiledView.RelativePath, Task.FromResult(precompiledView));
_precompiledViews.Add(precompiledView.RelativePath, precompiledView);
}
}
@ -109,103 +119,192 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
throw new ArgumentNullException(nameof(relativePath));
}
// Lookup precompiled views first.
// 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.
string normalizedPath = null;
Task<CompiledViewDescriptor> cachedResult;
if (_precompiledViewLookup.Count > 0)
{
if (_precompiledViewLookup.TryGetValue(relativePath, out cachedResult))
{
return cachedResult;
}
normalizedPath = GetNormalizedPath(relativePath);
if (_precompiledViewLookup.TryGetValue(normalizedPath, out cachedResult))
{
return cachedResult;
}
}
if (_cache.TryGetValue(relativePath, out cachedResult))
{
return cachedResult;
}
normalizedPath = normalizedPath ?? GetNormalizedPath(relativePath);
var normalizedPath = GetNormalizedPath(relativePath);
if (_cache.TryGetValue(normalizedPath, out cachedResult))
{
return cachedResult;
}
// Entry does not exist. Attempt to create one.
cachedResult = CreateCacheEntry(normalizedPath);
cachedResult = OnCacheMiss(normalizedPath);
return cachedResult;
}
private Task<CompiledViewDescriptor> CreateCacheEntry(string normalizedPath)
private Task<CompiledViewDescriptor> OnCacheMiss(string normalizedPath)
{
TaskCompletionSource<CompiledViewDescriptor> compilationTaskSource = null;
ViewCompilerWorkItem item;
TaskCompletionSource<CompiledViewDescriptor> taskSource;
MemoryCacheEntryOptions cacheEntryOptions;
Task<CompiledViewDescriptor> cacheEntry;
// 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)
{
if (_cache.TryGetValue(normalizedPath, out cacheEntry))
// Double-checked locking to handle a possible race.
if (_cache.TryGetValue(normalizedPath, out Task<CompiledViewDescriptor> result))
{
return cacheEntry;
return result;
}
cacheEntryOptions = new MemoryCacheEntryOptions();
cacheEntryOptions.ExpirationTokens.Add(_fileProvider.Watch(normalizedPath));
var projectItem = _templateEngine.Project.GetItem(normalizedPath);
if (!projectItem.Exists)
if (_precompiledViews.TryGetValue(normalizedPath, out var precompiledView))
{
cacheEntry = Task.FromResult(new CompiledViewDescriptor
{
RelativePath = normalizedPath,
ExpirationTokens = cacheEntryOptions.ExpirationTokens,
});
item = CreatePrecompiledWorkItem(normalizedPath, precompiledView);
}
else
{
// A file exists and needs to be compiled.
compilationTaskSource = new TaskCompletionSource<CompiledViewDescriptor>();
foreach (var importItem in _templateEngine.GetImportItems(projectItem))
{
cacheEntryOptions.ExpirationTokens.Add(_fileProvider.Watch(importItem.FilePath));
}
cacheEntry = compilationTaskSource.Task;
item = CreateRuntimeCompilationWorkItem(normalizedPath);
}
cacheEntry = _cache.Set(normalizedPath, cacheEntry, cacheEntryOptions);
// 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>();
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);
}
if (compilationTaskSource != null)
// Now the lock has been released so we can do more expensive processing.
if (item.SupportsCompilation)
{
// Indicates that a file was found and needs to be compiled.
Debug.Assert(cacheEntryOptions != null);
Debug.Assert(taskSource != null);
if (item.Descriptor?.Item != null &&
ChecksumValidator.IsItemValid(_templateEngine.Project, 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;
}
try
{
var descriptor = CompileAndEmit(normalizedPath);
descriptor.ExpirationTokens = cacheEntryOptions.ExpirationTokens;
compilationTaskSource.SetResult(descriptor);
taskSource.SetResult(descriptor);
}
catch (Exception ex)
{
compilationTaskSource.SetException(ex);
taskSource.SetException(ex);
}
}
return cacheEntry;
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 = new List<IChangeToken>(),
};
var checksums = precompiledView.Item.GetChecksumMetadata();
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.
item.ExpirationTokens.Add(_fileProvider.Watch(checksums[i].Identifier));
}
return item;
}
private ViewCompilerWorkItem CreateRuntimeCompilationWorkItem(string normalizedPath)
{
var expirationTokens = new List<IChangeToken>()
{
_fileProvider.Watch(normalizedPath),
};
var projectItem = _templateEngine.Project.GetItem(normalizedPath);
if (!projectItem.Exists)
{
// 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 retrigger 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,
};
}
// 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.
foreach (var importItem in _templateEngine.GetImportItems(projectItem))
{
expirationTokens.Add(_fileProvider.Watch(importItem.FilePath));
}
return new ViewCompilerWorkItem()
{
SupportsCompilation = true,
NormalizedPath = normalizedPath,
ExpirationTokens = expirationTokens,
};
}
protected virtual CompiledViewDescriptor CompileAndEmit(string relativePath)
@ -220,13 +319,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
cSharpDocument.Diagnostics);
}
var generatedAssembly = CompileAndEmit(codeDocument, cSharpDocument.GeneratedCode);
var viewAttribute = generatedAssembly.GetCustomAttribute<RazorViewAttribute>();
return new CompiledViewDescriptor
{
ViewAttribute = viewAttribute,
RelativePath = relativePath,
};
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();
var attribute = assembly.GetCustomAttribute<RazorViewAttribute>();
return new CompiledViewDescriptor(item, attribute);
}
internal Assembly CompileAndEmit(RazorCodeDocument codeDocument, string generatedCode)
@ -288,13 +388,24 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
return relativePath;
}
if (!_normalizedPathLookup.TryGetValue(relativePath, out var normalizedPath))
if (!_normalizedPathCache.TryGetValue(relativePath, out var normalizedPath))
{
normalizedPath = ViewPath.NormalizePath(relativePath);
_normalizedPathLookup[relativePath] = normalizedPath;
_normalizedPathCache[relativePath] = normalizedPath;
}
return normalizedPath;
}
private class ViewCompilerWorkItem
{
public bool SupportsCompilation { get; set; }
public string NormalizedPath { get; set; }
public IList<IChangeToken> ExpirationTokens { get; set; }
public CompiledViewDescriptor Descriptor { get; set; }
}
}
}

View File

@ -378,6 +378,20 @@ namespace Microsoft.AspNetCore.Mvc.Razor
internal static string FormatUnsupportedDebugInformationFormat(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("UnsupportedDebugInformationFormat"), p0);
/// <summary>
/// At least one of the '{0}' or '{1}' values must be non-null.
/// </summary>
internal static string CompiledViewDescriptor_NoData
{
get => GetString("CompiledViewDescriptor_NoData");
}
/// <summary>
/// At least one of the '{0}' or '{1}' values must be non-null.
/// </summary>
internal static string FormatCompiledViewDescriptor_NoData(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("CompiledViewDescriptor_NoData"), p0, p1);
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -197,4 +197,7 @@
<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>
<data name="CompiledViewDescriptor_NoData" xml:space="preserve">
<value>At least one of the '{0}' or '{1}' values must be non-null.</value>
</data>
</root>

View File

@ -8,7 +8,9 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -16,19 +18,40 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class CompiledPageRouteModelProvider : IPageRouteModelProvider
{
private readonly object _cacheLock = new object();
private readonly ApplicationPartManager _applicationManager;
private readonly RazorPagesOptions _pagesOptions;
private readonly RazorTemplateEngine _templateEngine;
private readonly ILogger<CompiledPageRouteModelProvider> _logger;
private List<PageRouteModel> _cachedModels;
public CompiledPageRouteModelProvider(
ApplicationPartManager applicationManager,
IOptions<RazorPagesOptions> pagesOptionsAccessor,
RazorTemplateEngine templateEngine,
ILoggerFactory loggerFactory)
{
if (applicationManager == null)
{
throw new ArgumentNullException(nameof(applicationManager));
}
if (pagesOptionsAccessor == null)
{
throw new ArgumentNullException(nameof(pagesOptionsAccessor));
}
if (templateEngine == null)
{
throw new ArgumentNullException(nameof(templateEngine));
}
if (loggerFactory == null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}
_applicationManager = applicationManager;
_pagesOptions = pagesOptionsAccessor.Value;
_templateEngine = templateEngine;
_logger = loggerFactory.CreateLogger<CompiledPageRouteModelProvider>();
}
@ -36,61 +59,60 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public void OnProvidersExecuting(PageRouteModelProviderContext context)
{
EnsureCache();
for (var i = 0; i < _cachedModels.Count; i++)
if (context == null)
{
var pageModel = _cachedModels[i];
context.RouteModels.Add(new PageRouteModel(pageModel));
throw new ArgumentNullException(nameof(context));
}
CreateModels(context.RouteModels);
}
public void OnProvidersExecuted(PageRouteModelProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
}
private void EnsureCache()
private void CreateModels(IList<PageRouteModel> results)
{
lock (_cacheLock)
var rootDirectory = _pagesOptions.RootDirectory;
if (!rootDirectory.EndsWith("/", StringComparison.Ordinal))
{
if (_cachedModels != null)
rootDirectory = rootDirectory + "/";
}
var areaRootDirectory = _pagesOptions.AreaRootDirectory;
if (!areaRootDirectory.EndsWith("/", StringComparison.Ordinal))
{
areaRootDirectory = areaRootDirectory + "/";
}
foreach (var viewDescriptor in GetViewDescriptors(_applicationManager))
{
if (viewDescriptor.Item != null && !ChecksumValidator.IsItemValid(_templateEngine.Project, viewDescriptor.Item))
{
return;
// If we get here, this compiled Page has different local content, so ignore it.
continue;
}
var rootDirectory = _pagesOptions.RootDirectory;
if (!rootDirectory.EndsWith("/", StringComparison.Ordinal))
PageRouteModel model = null;
// When RootDirectory and AreaRootDirectory overlap (e.g. RootDirectory = '/', AreaRootDirectory = '/Areas'), we
// only want to allow a page to be associated with the area route.
if (_pagesOptions.AllowAreas && viewDescriptor.RelativePath.StartsWith(areaRootDirectory, StringComparison.OrdinalIgnoreCase))
{
rootDirectory = rootDirectory + "/";
model = GetAreaPageRouteModel(areaRootDirectory, viewDescriptor);
}
else if (viewDescriptor.RelativePath.StartsWith(rootDirectory, StringComparison.OrdinalIgnoreCase))
{
model = GetPageRouteModel(rootDirectory, viewDescriptor);
}
var areaRootDirectory = _pagesOptions.AreaRootDirectory;
if (!areaRootDirectory.EndsWith("/", StringComparison.Ordinal))
if (model != null)
{
areaRootDirectory = areaRootDirectory + "/";
results.Add(model);
}
var cachedApplicationModels = new List<PageRouteModel>();
foreach (var viewDescriptor in GetViewDescriptors(_applicationManager))
{
PageRouteModel model = null;
// When RootDirectory and AreaRootDirectory overlap (e.g. RootDirectory = '/', AreaRootDirectory = '/Areas'), we
// only want to allow a page to be associated with the area route.
if (_pagesOptions.AllowAreas && viewDescriptor.RelativePath.StartsWith(areaRootDirectory, StringComparison.OrdinalIgnoreCase))
{
model = GetAreaPageRouteModel(areaRootDirectory, viewDescriptor);
}
else if (viewDescriptor.RelativePath.StartsWith(rootDirectory, StringComparison.OrdinalIgnoreCase))
{
model = GetPageRouteModel(rootDirectory, viewDescriptor);
}
if (model != null)
{
cachedApplicationModels.Add(model);
}
}
_cachedModels = cachedApplicationModels;
}
}

View File

@ -30,6 +30,7 @@
<ProjectReference Include="..\WebSites\HtmlGenerationWebSite\HtmlGenerationWebSite.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.Mvc.TestCommon\Microsoft.AspNetCore.Mvc.TestCommon.csproj" />
<ProjectReference Include="..\..\samples\MvcSandbox\MvcSandbox.csproj" />
<ProjectReference Include="..\WebSites\RazorBuildWebSite\RazorBuildWebSite.csproj" />
<ProjectReference Include="..\WebSites\RazorPageExecutionInstrumentationWebSite\RazorPageExecutionInstrumentationWebSite.csproj" />
<ProjectReference Include="..\WebSites\RazorPagesWebSite\RazorPagesWebSite.csproj" />
<ProjectReference Include="..\WebSites\RazorWebSite\RazorWebSite.csproj" />

View File

@ -0,0 +1,68 @@
// 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.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
public class RazorBuildTest : IClassFixture<MvcTestFixture<RazorBuildWebSite.Startup>>
{
public RazorBuildTest(MvcTestFixture<RazorBuildWebSite.Startup> fixture)
{
Client = fixture.Client;
}
public HttpClient Client { get; }
[Fact]
public async Task PrecompiledPage_LocalPageWithDifferentContent_NotUsed()
{
// Act
var response = await Client.GetAsync("http://localhost/Precompilation/Page");
var responseBody = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Hello from buildtime-compiled precompilation page!", responseBody.Trim());
}
[Fact]
public async Task PrecompiledView_LocalViewWithDifferentContent_NotUsed()
{
// Act
var response = await Client.GetAsync("http://localhost/Precompilation/View");
var responseBody = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Hello from buildtime-compiled precompilation view!", responseBody.Trim());
}
[Fact(Skip = "Not yet implemented")]
public async Task Rzc_LocalPageWithDifferentContent_IsUsed()
{
// Act
var response = await Client.GetAsync("http://localhost/Rzc/Page");
var responseBody = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Hello from runtime-compiled rzc page!", responseBody.Trim());
}
[Fact(Skip = "Not yet implemented")]
public async Task Rzc_LocalViewWithDifferentContent_IsUsed()
{
// Act
var response = await Client.GetAsync("http://localhost/Rzc/View");
var responseBody = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Hello from runtime-compiled rzc view!", responseBody.Trim());
}
}
}

View File

@ -6,7 +6,9 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Razor.Hosting;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
@ -17,14 +19,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
public void PopulateFeature_ReturnsEmptySequenceIfNoAssemblyPartHasViewAssembly()
{
// Arrange
var applicationPartManager = new ApplicationPartManager();
applicationPartManager.ApplicationParts.Add(
new AssemblyPart(typeof(ViewsFeatureProviderTest).GetTypeInfo().Assembly));
applicationPartManager.FeatureProviders.Add(new ViewsFeatureProvider());
var partManager = new ApplicationPartManager();
partManager.ApplicationParts.Add(new AssemblyPart(typeof(ViewsFeatureProviderTest).GetTypeInfo().Assembly));
partManager.FeatureProviders.Add(new ViewsFeatureProvider());
var feature = new ViewsFeature();
// Act
applicationPartManager.PopulateFeature(feature);
partManager.PopulateFeature(feature);
// Assert
Assert.Empty(feature.ViewDescriptors);
@ -36,7 +37,29 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
// Arrange
var part1 = new AssemblyPart(typeof(object).GetTypeInfo().Assembly);
var part2 = new AssemblyPart(GetType().GetTypeInfo().Assembly);
var featureProvider = new TestableViewsFeatureProvider(new Dictionary<AssemblyPart, IEnumerable<RazorViewAttribute>>
var items = new Dictionary<AssemblyPart, IReadOnlyList<RazorCompiledItem>>
{
{
part1,
new[]
{
new TestRazorCompiledItem(typeof(object), "mvc.1.0.view", "/Views/test/Index.cshtml", new object[]{ }),
// This one doesn't have a RazorViewAttribute
new TestRazorCompiledItem(typeof(StringBuilder), "mvc.1.0.view", "/Views/test/About.cshtml", new object[]{ }),
}
},
{
part2,
new[]
{
new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Areas/Admin/Views/Index.cshtml", new object[]{ }),
}
},
};
var attributes = new Dictionary<AssemblyPart, IEnumerable<RazorViewAttribute>>
{
{
part1,
@ -50,35 +73,70 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
new[]
{
new RazorViewAttribute("/Areas/Admin/Views/Index.cshtml", typeof(string)),
// This one doesn't have a RazorCompiledItem
new RazorViewAttribute("/Areas/Admin/Views/About.cshtml", typeof(int)),
}
},
});
};
var applicationPartManager = new ApplicationPartManager();
applicationPartManager.ApplicationParts.Add(part1);
applicationPartManager.ApplicationParts.Add(part2);
applicationPartManager.FeatureProviders.Add(featureProvider);
var featureProvider = new TestableViewsFeatureProvider(items, attributes);
var partManager = new ApplicationPartManager();
partManager.ApplicationParts.Add(part1);
partManager.ApplicationParts.Add(part2);
partManager.FeatureProviders.Add(featureProvider);
var feature = new ViewsFeature();
// Act
applicationPartManager.PopulateFeature(feature);
partManager.PopulateFeature(feature);
// Assert
Assert.Collection(feature.ViewDescriptors.OrderBy(f => f.RelativePath, StringComparer.Ordinal),
view =>
{
Assert.Empty(view.ExpirationTokens);
Assert.True(view.IsPrecompiled);
Assert.Null(view.Item);
Assert.Equal("/Areas/Admin/Views/About.cshtml", view.RelativePath);
Assert.Equal(typeof(int), view.Type);
Assert.Equal("/Areas/Admin/Views/About.cshtml", view.ViewAttribute.Path);
Assert.Equal(typeof(int), view.ViewAttribute.ViewType);
},
view =>
{
// This one doesn't have a RazorCompiledItem
Assert.Empty(view.ExpirationTokens);
Assert.True(view.IsPrecompiled);
Assert.Equal("/Areas/Admin/Views/Index.cshtml", view.Item.Identifier);
Assert.Equal("mvc.1.0.view", view.Item.Kind);
Assert.Equal(typeof(string), view.Item.Type);
Assert.Equal("/Areas/Admin/Views/Index.cshtml", view.RelativePath);
Assert.Equal(typeof(string), view.Type);
Assert.Equal("/Areas/Admin/Views/Index.cshtml", view.ViewAttribute.Path);
Assert.Equal(typeof(string), view.ViewAttribute.ViewType);
},
view =>
{
// This one doesn't have a RazorViewAttribute
Assert.Empty(view.ExpirationTokens);
Assert.True(view.IsPrecompiled);
Assert.Equal("/Views/test/About.cshtml", view.Item.Identifier);
Assert.Equal("mvc.1.0.view", view.Item.Kind);
Assert.Equal(typeof(StringBuilder), view.Item.Type);
Assert.Equal("/Views/test/About.cshtml", view.RelativePath);
Assert.Equal(typeof(StringBuilder), view.Type);
Assert.Null(view.ViewAttribute);
},
view =>
{
Assert.Empty(view.ExpirationTokens);
Assert.True(view.IsPrecompiled);
Assert.Equal("/Views/test/Index.cshtml", view.Item.Identifier);
Assert.Equal("mvc.1.0.view", view.Item.Kind);
Assert.Equal(typeof(object), view.Item.Type);
Assert.Equal("/Views/test/Index.cshtml", view.RelativePath);
Assert.Equal(typeof(object), view.Type);
Assert.Equal("/Views/test/Index.cshtml", view.ViewAttribute.Path);
Assert.Equal(typeof(object), view.ViewAttribute.ViewType);
});
}
@ -88,50 +146,78 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
{
// Arrange
var name = new AssemblyName($"DynamicAssembly-{Guid.NewGuid()}");
var assembly = AssemblyBuilder.DefineDynamicAssembly(name,
AssemblyBuilderAccess.RunAndCollect);
var assembly = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.RunAndCollect);
var applicationPartManager = new ApplicationPartManager();
applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly));
applicationPartManager.FeatureProviders.Add(new ViewsFeatureProvider());
var partManager = new ApplicationPartManager();
partManager.ApplicationParts.Add(new AssemblyPart(assembly));
partManager.FeatureProviders.Add(new ViewsFeatureProvider());
var feature = new ViewsFeature();
// Act
applicationPartManager.PopulateFeature(feature);
partManager.PopulateFeature(feature);
// Assert
Assert.Empty(feature.ViewDescriptors);
}
[Fact]
public void PopulateFeature_DoesNotFail_IfAssemblyHasEmptyLocation()
{
// Arrange
var assembly = new AssemblyWithEmptyLocation();
var applicationPartManager = new ApplicationPartManager();
applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly));
applicationPartManager.FeatureProviders.Add(new ViewsFeatureProvider());
var partManager = new ApplicationPartManager();
partManager.ApplicationParts.Add(new AssemblyPart(assembly));
partManager.FeatureProviders.Add(new ViewsFeatureProvider());
var feature = new ViewsFeature();
// Act
applicationPartManager.PopulateFeature(feature);
partManager.PopulateFeature(feature);
// Assert
Assert.Empty(feature.ViewDescriptors);
}
private class TestRazorCompiledItem : RazorCompiledItem
{
public TestRazorCompiledItem(Type type, string kind, string identifier, object[] metadata)
{
Type = type;
Kind = kind;
Identifier = identifier;
Metadata = metadata;
}
public override string Identifier { get; }
public override string Kind { get; }
public override IReadOnlyList<object> Metadata { get; }
public override Type Type { get; }
}
private class TestableViewsFeatureProvider : ViewsFeatureProvider
{
private readonly Dictionary<AssemblyPart, IEnumerable<RazorViewAttribute>> _attributeLookup;
private readonly Dictionary<AssemblyPart, IEnumerable<RazorViewAttribute>> _attributes;
private readonly Dictionary<AssemblyPart, IReadOnlyList<RazorCompiledItem>> _items;
public TestableViewsFeatureProvider(Dictionary<AssemblyPart, IEnumerable<RazorViewAttribute>> attributeLookup)
public TestableViewsFeatureProvider(
Dictionary<AssemblyPart, IReadOnlyList<RazorCompiledItem>> items,
Dictionary<AssemblyPart, IEnumerable<RazorViewAttribute>> attributes)
{
_attributeLookup = attributeLookup;
_items = items;
_attributes = attributes;
}
protected override IEnumerable<RazorViewAttribute> GetViewAttributes(AssemblyPart assemblyPart)
{
return _attributeLookup[assemblyPart];
return _attributes[assemblyPart];
}
protected override IReadOnlyList<RazorCompiledItem> LoadItems(AssemblyPart assemblyPart)
{
return _items[assemblyPart];
}
}

View File

@ -0,0 +1,199 @@
// 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 Moq;
using Xunit;
using static Microsoft.AspNetCore.Razor.Hosting.TestRazorCompiledItem;
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{
public class ChecksumValidatorTest
{
public ChecksumValidatorTest()
{
FileProvider = new TestFileProvider();
Project = new FileProviderRazorProject(Mock.Of<IRazorViewEngineFileProviderAccessor>(a => a.FileProvider == FileProvider));
}
public RazorProject Project { get; }
public TestFileProvider FileProvider { get; }
[Fact]
public void IsRecompilationSupported_NoChecksums_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_NoPrimaryChecksum_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_HasPrimaryChecksum_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_NoChecksums_ReturnsTrue()
{
// Arrange
var item = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Views/Home/Index.cstml", new object[] { });
// Act
var result = ChecksumValidator.IsItemValid(Project, item);
// Assert
Assert.True(result);
}
[Fact]
public void IsItemValid_NoPrimaryChecksum_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(Project, 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"),
});
FileProvider.AddFile("/Views/Home/_ViewImports.cstml", "dkdkfkdf"); // This will be ignored
// Act
var result = ChecksumValidator.IsItemValid(Project, 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"),
});
FileProvider.AddFile("/Views/Home/Index.cstml", "other content");
// Act
var result = ChecksumValidator.IsItemValid(Project, 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"),
});
FileProvider.AddFile("/Views/Home/Index.cstml", "some content");
// Act
var result = ChecksumValidator.IsItemValid(Project, 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"),
});
FileProvider.AddFile("/Views/Home/Index.cstml", "some content");
FileProvider.AddFile("/Views/Home/_ViewImports.cstml", "some other import");
// Act
var result = ChecksumValidator.IsItemValid(Project, 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"),
});
FileProvider.AddFile("/Views/Home/Index.cstml", "some content");
FileProvider.AddFile("/Views/Home/_ViewImports.cstml", "some import");
FileProvider.AddFile("/Views/_ViewImports.cstml", "some other import");
// Act
var result = ChecksumValidator.IsItemValid(Project, item);
// Assert
Assert.True(result);
}
}
}

View File

@ -8,12 +8,14 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Razor.Hosting;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
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.Internal
{
@ -55,7 +57,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
// Assert
Assert.Same(result1, result2);
Assert.Null(result1.ViewAttribute);
Assert.Collection(result1.ExpirationTokens,
Assert.Collection(
result1.ExpirationTokens,
token => Assert.Equal(fileProvider.GetChangeToken(path), token));
}
@ -73,7 +76,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
// Assert
Assert.NotNull(result.ViewAttribute);
Assert.Collection(result.ExpirationTokens,
Assert.Collection(
result.ExpirationTokens,
token => Assert.Same(fileProvider.GetChangeToken(path), token),
token => Assert.Same(fileProvider.GetChangeToken("/file/exists/_ViewImports.cshtml"), token),
token => Assert.Same(fileProvider.GetChangeToken("/file/_ViewImports.cshtml"), token),
@ -246,27 +250,280 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
}
[Fact]
public async Task CompileAsync_DoesNotRecompile_IfFileTriggerWasSetForPrecompiledView()
public async Task CompileAsync_PrecompiledViewWithoutChecksumForMainSource_DoesNotSupportRecompilation()
{
// Arrange
var path = "/Views/Home/Index.cshtml";
var fileProvider = new TestFileProvider();
var fileInfo = 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, result);
// Act - 2
fileProvider.Watch(path);
fileProvider.GetChangeToken(path).HasChanged = true;
result = await viewCompiler.CompileAsync(path);
// Assert - 2
Assert.Same(precompiledView, result);
}
[Fact]
public async Task CompileAsync_PrecompiledViewWithoutAnyChecksum_DoesNotSupportRecompilation()
{
// Arrange
var path = "/Views/Home/Index.cshtml";
var fileProvider = new TestFileProvider();
var fileInfo = 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);
}
[Fact]
public async Task CompileAsync_PrecompiledViewWithChecksum_UsesPrecompiledViewWhenChecksumIsMatch()
{
// Arrange
var path = "/Views/Home/Index.cshtml";
var fileProvider = new TestFileProvider();
var fileInfo = 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
fileProvider.Watch(path);
fileProvider.GetChangeToken(path).HasChanged = true;
var result = await viewCompiler.CompileAsync(path);
// Assert
Assert.Same(precompiledView, result);
}
[Fact]
public async Task CompileAsync_PrecompiledViewWithChecksum_CanRejectWhenChecksumFails()
{
// Arrange
var path = "/Views/Home/Index.cshtml";
var fileProvider = new TestFileProvider();
var fileInfo = 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_CanRecompile()
{
// Arrange
var path = "/Views/Home/Index.cshtml";
var fileProvider = new TestFileProvider();
var fileInfo = 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, result);
// Act - 2
fileInfo.Content = "some other content";
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();
var fileInfo = 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, result);
// Act - 2
fileProvider.GetChangeToken(path).HasChanged = true;
result = await viewCompiler.CompileAsync(path);
// Assert - 2
Assert.Same(precompiledView, result);
}
[Fact]
public async Task CompileAsync_PrecompiledViewWithChecksum_CanReusePrecompiledViewIfContentChangesToMatch()
{
// Arrange
var path = "/Views/Home/Index.cshtml";
var fileProvider = new TestFileProvider();
var fileInfo = fileProvider.AddFile(path, "some other 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
fileInfo.Content = "some content";
fileProvider.GetChangeToken(path).HasChanged = true;
result = await viewCompiler.CompileAsync(path);
// Assert - 2
Assert.Same(precompiledView, result);
}
[Fact]
public async Task CompileAsync_PrecompiledViewWithChecksum_CanRecompileWhenViewImportChanges()
{
// Arrange
var path = "/Views/Home/Index.cshtml";
var importPath = "/Views/_ViewImports.cshtml";
var fileProvider = new TestFileProvider();
var fileInfo = fileProvider.AddFile(path, "some content");
var importFileInfo = 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, result);
// Act - 2
importFileInfo.Content = "some import changed";
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()
{
@ -274,18 +531,23 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var path1 = "/Views/Home/Index.cshtml";
var path2 = "/Views/Home/About.cshtml";
var waitDuration = TimeSpan.FromSeconds(20);
var fileProvider = new TestFileProvider();
fileProvider.AddFile(path1, "Index content");
fileProvider.AddFile(path2, "About content");
var resetEvent1 = new AutoResetEvent(initialState: false);
var resetEvent2 = new ManualResetEvent(initialState: false);
var cache = GetViewCompiler(fileProvider);
var compilingOne = false;
var compilingTwo = false;
var result1 = new CompiledViewDescriptor();
var result2 = new CompiledViewDescriptor();
cache.Compile = path =>
var compiler = GetViewCompiler(fileProvider);
compiler.Compile = path =>
{
if (path == path1)
{
@ -325,8 +587,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
};
// Act
var task1 = Task.Run(() => cache.CompileAsync(path1));
var task2 = Task.Run(() => cache.CompileAsync(path2));
var task1 = Task.Run(() => compiler.CompileAsync(path1));
var task2 = Task.Run(() => compiler.CompileAsync(path2));
// Event 1
resetEvent1.Set();

View File

@ -6,102 +6,261 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Razor.Hosting;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
using static Microsoft.AspNetCore.Razor.Hosting.TestRazorCompiledItem;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class CompiledPageRouteModelProviderTest
{
public CompiledPageRouteModelProviderTest()
{
FileProvider = new TestFileProvider();
Project = new FileProviderRazorProject(Mock.Of<IRazorViewEngineFileProviderAccessor>(a => a.FileProvider == FileProvider));
TemplateEngine = new RazorTemplateEngine(RazorEngine.Create(), Project);
PagesOptions = new RazorPagesOptions();
Provider = new TestCompiledPageRouteModelProvider(new ApplicationPartManager(), Options.Create(PagesOptions), TemplateEngine, NullLoggerFactory.Instance);
}
public TestFileProvider FileProvider { get; }
public RazorProject Project { get; }
public RazorTemplateEngine TemplateEngine { get; }
public RazorPagesOptions PagesOptions { get; }
public TestCompiledPageRouteModelProvider Provider { get; }
[Fact]
public void OnProvidersExecuting_AddsModelsForCompiledViews()
{
// Arrange
var descriptors = new[]
Provider.Descriptors.AddRange(new[]
{
GetDescriptor("/Pages/About.cshtml"),
GetDescriptor("/Pages/Home.cshtml", "some-prefix"),
};
var provider = new TestCompiledPageRouteModelProvider(descriptors, new RazorPagesOptions());
CreateVersion_2_0_Descriptor("/Pages/About.cshtml"),
CreateVersion_2_0_Descriptor("/Pages/Home.cshtml", "some-prefix"),
});
var context = new PageRouteModelProviderContext();
// Act
provider.OnProvidersExecuting(context);
Provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(context.RouteModels,
Assert.Collection(
context.RouteModels,
result =>
{
Assert.Equal("/Pages/About.cshtml", result.RelativePath);
Assert.Equal("/About", result.ViewEnginePath);
Assert.Collection(result.Selectors,
Assert.Collection(
result.Selectors,
selector => Assert.Equal("About", selector.AttributeRouteModel.Template));
Assert.Collection(result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/About", kvp.Value);
});
Assert.Collection(
result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/About", kvp.Value);
});
},
result =>
{
Assert.Equal("/Pages/Home.cshtml", result.RelativePath);
Assert.Equal("/Home", result.ViewEnginePath);
Assert.Collection(result.Selectors,
Assert.Collection(
result.Selectors,
selector => Assert.Equal("Home/some-prefix", selector.AttributeRouteModel.Template));
Assert.Collection(result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Home", kvp.Value);
});
Assert.Collection(
result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Home", kvp.Value);
});
});
}
[Fact] // 2.1 adds some additional metadata to the view descriptors. We want to make sure both versions work.
public void OnProvidersExecuting_AddsModelsForCompiledViews_Version_2_1()
{
// Arrange
Provider.Descriptors.AddRange(new[]
{
CreateVersion_2_1_Descriptor("/Pages/About.cshtml"),
CreateVersion_2_1_Descriptor("/Pages/Home.cshtml", "some-prefix"),
});
var context = new PageRouteModelProviderContext();
// Act
Provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(
context.RouteModels,
result =>
{
Assert.Equal("/Pages/About.cshtml", result.RelativePath);
Assert.Equal("/About", result.ViewEnginePath);
Assert.Collection(
result.Selectors,
selector => Assert.Equal("About", selector.AttributeRouteModel.Template));
Assert.Collection(
result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/About", kvp.Value);
});
},
result =>
{
Assert.Equal("/Pages/Home.cshtml", result.RelativePath);
Assert.Equal("/Home", result.ViewEnginePath);
Assert.Collection(
result.Selectors,
selector => Assert.Equal("Home/some-prefix", selector.AttributeRouteModel.Template));
Assert.Collection(
result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Home", kvp.Value);
});
});
}
[Fact]
public void OnProvidersExecuting_ValidatesChecksum_RejectsPageWhenContentDoesntMatch()
{
// Arrange
Provider.Descriptors.AddRange(new[]
{
CreateVersion_2_1_Descriptor("/Pages/About.cshtml", metadata: new object[]
{
new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), "/Pages/About.cshtml"),
}),
});
FileProvider.AddFile("/Pages/About.cshtml", "some other content");
var context = new PageRouteModelProviderContext();
// Act
Provider.OnProvidersExecuting(context);
// Assert
Assert.Empty(context.RouteModels);
}
[Fact]
public void OnProvidersExecuting_ValidatesChecksum_AcceptsPageWhenContentMatches()
{
// Arrange
Provider.Descriptors.AddRange(new[]
{
CreateVersion_2_1_Descriptor("/Pages/About.cshtml", metadata: new object[]
{
new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), "/Pages/About.cshtml"),
new RazorSourceChecksumAttribute("SHA1", GetChecksum("some import"), "/Pages/_ViewImports.cshtml"),
}),
});
FileProvider.AddFile("/Pages/About.cshtml", "some content");
FileProvider.AddFile("/Pages/_ViewImports.cshtml", "some import");
var context = new PageRouteModelProviderContext();
// Act
Provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(
context.RouteModels,
result => Assert.Equal("/Pages/About.cshtml", result.RelativePath));
}
[Fact]
public void OnProvidersExecuting_ValidatesChecksum_SkipsValidationWhenMainSourceMissing()
{
// Arrange
Provider.Descriptors.AddRange(new[]
{
CreateVersion_2_1_Descriptor("/Pages/About.cshtml", metadata: new object[]
{
new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), "/Pages/About.cshtml"),
new RazorSourceChecksumAttribute("SHA1", GetChecksum("some import"), "/Pages/_ViewImports.cshtml"),
}),
});
FileProvider.AddFile("/Pages/_ViewImports.cshtml", "some other import");
var context = new PageRouteModelProviderContext();
// Act
Provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(
context.RouteModels,
result => Assert.Equal("/Pages/About.cshtml", result.RelativePath));
}
[Fact]
public void OnProvidersExecuting_AddsModelsForCompiledAreaPages()
{
// Arrange
var descriptors = new[]
Provider.Descriptors.AddRange(new[]
{
GetDescriptor("/Features/Products/Files/About.cshtml"),
GetDescriptor("/Features/Products/Files/Manage/Index.cshtml"),
GetDescriptor("/Features/Products/Files/Manage/Edit.cshtml", "{id}"),
};
var options = new RazorPagesOptions
{
AllowAreas = true,
AreaRootDirectory = "/Features",
RootDirectory = "/Files",
};
var provider = new TestCompiledPageRouteModelProvider(descriptors, options);
CreateVersion_2_0_Descriptor("/Features/Products/Files/About.cshtml"),
CreateVersion_2_0_Descriptor("/Features/Products/Files/Manage/Index.cshtml"),
CreateVersion_2_0_Descriptor("/Features/Products/Files/Manage/Edit.cshtml", "{id}"),
});
PagesOptions.AllowAreas = true;
PagesOptions.AreaRootDirectory = "/Features";
PagesOptions.RootDirectory = "/Files";
var context = new PageRouteModelProviderContext();
// Act
provider.OnProvidersExecuting(context);
Provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(context.RouteModels,
Assert.Collection(
context.RouteModels,
result =>
{
Assert.Equal("/Features/Products/Files/About.cshtml", result.RelativePath);
Assert.Equal("/About", result.ViewEnginePath);
Assert.Collection(result.Selectors,
Assert.Collection(
result.Selectors,
selector => Assert.Equal("Products/About", selector.AttributeRouteModel.Template));
Assert.Collection(result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal("Products", kvp.Value);
},
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/About", kvp.Value);
});
Assert.Collection(
result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal("Products", kvp.Value);
},
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/About", kvp.Value);
});
},
result =>
{
@ -110,35 +269,38 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
Assert.Collection(result.Selectors,
selector => Assert.Equal("Products/Manage/Index", selector.AttributeRouteModel.Template),
selector => Assert.Equal("Products/Manage", selector.AttributeRouteModel.Template));
Assert.Collection(result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal("Products", kvp.Value);
},
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Manage/Index", kvp.Value);
});
Assert.Collection(
result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal("Products", kvp.Value);
},
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Manage/Index", kvp.Value);
});
},
result =>
{
Assert.Equal("/Features/Products/Files/Manage/Edit.cshtml", result.RelativePath);
Assert.Equal("/Manage/Edit", result.ViewEnginePath);
Assert.Collection(result.Selectors,
Assert.Collection(
result.Selectors,
selector => Assert.Equal("Products/Manage/Edit/{id}", selector.AttributeRouteModel.Template));
Assert.Collection(result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal("Products", kvp.Value);
},
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Manage/Edit", kvp.Value);
});
Assert.Collection(
result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal("Products", kvp.Value);
},
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Manage/Edit", kvp.Value);
});
});
}
@ -146,32 +308,36 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public void OnProvidersExecuting_DoesNotAddsModelsForAreaPages_IfFeatureIsDisabled()
{
// Arrange
var descriptors = new[]
Provider.Descriptors.AddRange(new[]
{
GetDescriptor("/Pages/About.cshtml"),
GetDescriptor("/Areas/Accounts/Pages/Home.cshtml"),
};
var options = new RazorPagesOptions { AllowAreas = false };
var provider = new TestCompiledPageRouteModelProvider(descriptors, options);
CreateVersion_2_0_Descriptor("/Pages/About.cshtml"),
CreateVersion_2_0_Descriptor("/Areas/Accounts/Pages/Home.cshtml"),
});
PagesOptions.AllowAreas = false;
var context = new PageRouteModelProviderContext();
// Act
provider.OnProvidersExecuting(context);
Provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(context.RouteModels,
Assert.Collection(
context.RouteModels,
result =>
{
Assert.Equal("/Pages/About.cshtml", result.RelativePath);
Assert.Equal("/About", result.ViewEnginePath);
Assert.Collection(result.Selectors,
Assert.Collection(
result.Selectors,
selector => Assert.Equal("About", selector.AttributeRouteModel.Template));
Assert.Collection(result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/About", kvp.Value);
});
Assert.Collection(
result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/About", kvp.Value);
});
});
}
@ -179,56 +345,59 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public void OnProvidersExecuting_DoesNotAddAreaAndNonAreaRoutesForAPage()
{
// Arrange
var descriptors = new[]
Provider.Descriptors.AddRange(new[]
{
GetDescriptor("/Areas/Accounts/Manage/Home.cshtml"),
GetDescriptor("/Areas/About.cshtml"),
GetDescriptor("/Contact.cshtml"),
};
var options = new RazorPagesOptions
{
AllowAreas = true,
AreaRootDirectory = "/Areas",
RootDirectory = "/",
};
var provider = new TestCompiledPageRouteModelProvider(descriptors, options);
CreateVersion_2_0_Descriptor("/Areas/Accounts/Manage/Home.cshtml"),
CreateVersion_2_0_Descriptor("/Areas/About.cshtml"),
CreateVersion_2_0_Descriptor("/Contact.cshtml"),
});
PagesOptions.AllowAreas = true;
PagesOptions.AreaRootDirectory = "/Areas";
PagesOptions.RootDirectory = "/";
var context = new PageRouteModelProviderContext();
// Act
provider.OnProvidersExecuting(context);
Provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(context.RouteModels,
Assert.Collection(
context.RouteModels,
result =>
{
Assert.Equal("/Areas/Accounts/Manage/Home.cshtml", result.RelativePath);
Assert.Equal("/Manage/Home", result.ViewEnginePath);
Assert.Collection(result.Selectors,
Assert.Collection(
result.Selectors,
selector => Assert.Equal("Accounts/Manage/Home", selector.AttributeRouteModel.Template));
Assert.Collection(result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal("Accounts", kvp.Value);
},
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Manage/Home", kvp.Value);
});
Assert.Collection(
result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal("Accounts", kvp.Value);
},
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Manage/Home", kvp.Value);
});
},
result =>
{
Assert.Equal("/Contact.cshtml", result.RelativePath);
Assert.Equal("/Contact", result.ViewEnginePath);
Assert.Collection(result.Selectors,
Assert.Collection(
result.Selectors,
selector => Assert.Equal("Contact", selector.AttributeRouteModel.Template));
Assert.Collection(result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Contact", kvp.Value);
});
Assert.Collection(
result.RouteValues.OrderBy(k => k.Key),
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Contact", kvp.Value);
});
});
}
@ -236,24 +405,28 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPage_WithIndexAtRoot()
{
// Arrange
var descriptors = new[]
Provider.Descriptors.AddRange(new[]
{
GetDescriptor("/Pages/Index.cshtml"),
GetDescriptor("/Pages/Admin/Index.cshtml", "some-template"),
};
var provider = new TestCompiledPageRouteModelProvider(descriptors, new RazorPagesOptions { RootDirectory = "/" });
CreateVersion_2_0_Descriptor("/Pages/Index.cshtml"),
CreateVersion_2_0_Descriptor("/Pages/Admin/Index.cshtml", "some-template"),
});
PagesOptions.RootDirectory = "/";
var context = new PageRouteModelProviderContext();
// Act
provider.OnProvidersExecuting(context);
Provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(context.RouteModels,
Assert.Collection(
context.RouteModels,
result =>
{
Assert.Equal("/Pages/Index.cshtml", result.RelativePath);
Assert.Equal("/Pages/Index", result.ViewEnginePath);
Assert.Collection(result.Selectors,
Assert.Collection(
result.Selectors,
selector => Assert.Equal("Pages/Index", selector.AttributeRouteModel.Template),
selector => Assert.Equal("Pages", selector.AttributeRouteModel.Template));
},
@ -261,7 +434,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
Assert.Equal("/Pages/Admin/Index.cshtml", result.RelativePath);
Assert.Equal("/Pages/Admin/Index", result.ViewEnginePath);
Assert.Collection(result.Selectors,
Assert.Collection(
result.Selectors,
selector => Assert.Equal("Pages/Admin/Index/some-template", selector.AttributeRouteModel.Template),
selector => Assert.Equal("Pages/Admin/some-template", selector.AttributeRouteModel.Template));
});
@ -271,24 +445,26 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPage()
{
// Arrange
var descriptors = new[]
Provider.Descriptors.AddRange(new[]
{
GetDescriptor("/Pages/Index.cshtml"),
GetDescriptor("/Pages/Admin/Index.cshtml", "some-template"),
};
var provider = new TestCompiledPageRouteModelProvider(descriptors, new RazorPagesOptions());
CreateVersion_2_0_Descriptor("/Pages/Index.cshtml"),
CreateVersion_2_0_Descriptor("/Pages/Admin/Index.cshtml", "some-template"),
});
var context = new PageRouteModelProviderContext();
// Act
provider.OnProvidersExecuting(context);
Provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(context.RouteModels,
Assert.Collection(
context.RouteModels,
result =>
{
Assert.Equal("/Pages/Index.cshtml", result.RelativePath);
Assert.Equal("/Index", result.ViewEnginePath);
Assert.Collection(result.Selectors,
Assert.Collection(
result.Selectors,
selector => Assert.Equal("Index", selector.AttributeRouteModel.Template),
selector => Assert.Equal("", selector.AttributeRouteModel.Template));
},
@ -296,7 +472,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
Assert.Equal("/Pages/Admin/Index.cshtml", result.RelativePath);
Assert.Equal("/Admin/Index", result.ViewEnginePath);
Assert.Collection(result.Selectors,
Assert.Collection(
result.Selectors,
selector => Assert.Equal("Admin/Index/some-template", selector.AttributeRouteModel.Template),
selector => Assert.Equal("Admin/some-template", selector.AttributeRouteModel.Template));
});
@ -306,40 +483,57 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public void OnProvidersExecuting_ThrowsIfRouteTemplateHasOverridePattern()
{
// Arrange
var descriptors = new[]
Provider.Descriptors.AddRange(new[]
{
GetDescriptor("/Pages/Index.cshtml"),
GetDescriptor("/Pages/Home.cshtml", "/some-prefix"),
};
var provider = new TestCompiledPageRouteModelProvider(descriptors, new RazorPagesOptions());
CreateVersion_2_0_Descriptor("/Pages/Index.cshtml"),
CreateVersion_2_0_Descriptor("/Pages/Home.cshtml", "/some-prefix"),
});
var context = new PageRouteModelProviderContext();
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => provider.OnProvidersExecuting(context));
Assert.Equal("The route for the page at '/Pages/Home.cshtml' cannot start with / or ~/. Pages do not support overriding the file path of the page.",
ex.Message);
var exception = Assert.Throws<InvalidOperationException>(() => Provider.OnProvidersExecuting(context));
Assert.Equal(
"The route for the page at '/Pages/Home.cshtml' cannot start with / or ~/. Pages do not support overriding the file path of the page.",
exception.Message);
}
private static CompiledViewDescriptor GetDescriptor(string path, string prefix = "")
private static CompiledViewDescriptor CreateVersion_2_0_Descriptor(string path, string routeTemplate = "")
{
return new CompiledViewDescriptor
{
RelativePath = path,
ViewAttribute = new RazorPageAttribute(path, typeof(object), prefix),
ViewAttribute = new RazorPageAttribute(path, typeof(object), routeTemplate),
};
}
private static CompiledViewDescriptor CreateVersion_2_1_Descriptor(
string path,
string routeTemplate = "",
object[] metadata = null)
{
return new CompiledViewDescriptor
{
RelativePath = path,
ViewAttribute = new RazorPageAttribute(path, typeof(object), routeTemplate),
Item = new TestRazorCompiledItem(typeof(object), "mvc.1.0.razor-page", path, metadata ?? Array.Empty<object>()),
};
}
public class TestCompiledPageRouteModelProvider : CompiledPageRouteModelProvider
{
private readonly IEnumerable<CompiledViewDescriptor> _descriptors;
public TestCompiledPageRouteModelProvider(IEnumerable<CompiledViewDescriptor> descriptors, RazorPagesOptions options)
: base(new ApplicationPartManager(), Options.Create(options), NullLoggerFactory.Instance)
public TestCompiledPageRouteModelProvider(
ApplicationPartManager partManager,
IOptions<RazorPagesOptions> options,
RazorTemplateEngine templateEngine,
ILoggerFactory loggerFactory)
: base(partManager, options, templateEngine, loggerFactory)
{
_descriptors = descriptors;
}
protected override IEnumerable<CompiledViewDescriptor> GetViewDescriptors(ApplicationPartManager applicationManager) => _descriptors;
public List<CompiledViewDescriptor> Descriptors { get; } = new List<CompiledViewDescriptor>();
protected override IEnumerable<CompiledViewDescriptor> GetViewDescriptors(ApplicationPartManager applicationManager) => Descriptors;
}
}
}

View File

@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Html.Abstractions" Version="$(MicrosoftAspNetCoreHtmlAbstractionsPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Runtime" Version="$(MicrosoftAspNetCoreRazorRuntimePackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Abstractions" Version="$(MicrosoftExtensionsFileProvidersAbstractionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />

View File

@ -0,0 +1,47 @@
// 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.Security.Cryptography;
using System.Text;
namespace Microsoft.AspNetCore.Razor.Hosting
{
public class TestRazorCompiledItem : RazorCompiledItem
{
public TestRazorCompiledItem(Type type, string kind, string identifier, object[] metadata)
{
Type = type;
Kind = kind;
Identifier = identifier;
Metadata = metadata;
}
public override string Identifier { get; }
public override string Kind { get; }
public override IReadOnlyList<object> Metadata { get; }
public override Type Type { get; }
public static string GetChecksum(string content)
{
byte[] bytes;
using (var sha = SHA1.Create())
{
bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(content));
}
var result = new StringBuilder(bytes.Length);
for (var i = 0; i < bytes.Length; i++)
{
// The x2 format means lowercase hex, where each byte is a 2-character string.
result.Append(bytes[i].ToString("x2"));
}
return result.ToString();
}
}
}

View File

@ -0,0 +1,39 @@
#pragma checksum "D:\k\Mvc\test\WebSites\RazorBuildWebSite\Pages\Precompilation\Page.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "f3a9fa49018f90b3470f6c0e4f475d1d8c9cd456"
// <auto-generated/>
#pragma warning disable 1591
[assembly: global::Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.RazorPageAttribute(@"/Pages/Precompilation/Page.cshtml", typeof(RazorBuildWebSite.Pages.Precompilation._Pages_Precompilation_Page), null)]
namespace RazorBuildWebSite.Pages.Precompilation
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
public class _Pages_Precompilation_Page : global::Microsoft.AspNetCore.Mvc.RazorPages.Page
{
#pragma warning disable 1998
public async override global::System.Threading.Tasks.Task ExecuteAsync()
{
BeginContext(26, 52, true);
WriteLiteral("\r\nHello from buildtime-compiled precompilation page!\r\n");
EndContext();
}
#pragma warning restore 1998
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; }
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; }
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; }
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; }
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<Page_Model> Html { get; private set; }
public global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary<Page_Model> ViewData => (global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary<Page_Model>)PageContext?.ViewData;
public Page_Model Model => ViewData.Model;
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,9 @@

using Microsoft.AspNetCore.Mvc.RazorPages;
namespace RazorBuildWebSite.Pages.Precompilation
{
public class Page_Model : PageModel
{
}
}

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(StandardTestWebsiteTfms)</TargetFrameworks>
<DefineConstants>$(DefineConstants)</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.Mvc\Microsoft.AspNetCore.Mvc.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,37 @@
#pragma checksum "D:\k\Mvc\test\WebSites\RazorBuildWebSite\Views\Precompilation\View.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "a09a0106df2e63aecf6fc6ddf30df39b489d9783"
// <auto-generated/>
#pragma warning disable 1591
[assembly: global::Microsoft.AspNetCore.Mvc.Razor.Compilation.RazorViewAttribute(@"/Views/Precompilation/View.cshtml", typeof(RazorBuildWebSite.Views.Precompilation._Views_Precompilation_View))]
namespace RazorBuildWebSite.Views.Precompilation
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
public class _Views_Precompilation_View : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<dynamic>
{
#pragma warning disable 1998
public async override global::System.Threading.Tasks.Task ExecuteAsync()
{
BeginContext(0, 48, true);
WriteLiteral("Hello from buildtime-compiled precompilation view!");
EndContext();
}
#pragma warning restore 1998
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; }
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; }
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; }
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; }
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<dynamic> Html { get; private set; }
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace RazorBuildWebSite.Views.Pages.Rzc
{
class Page
{
}
}

View File

@ -0,0 +1,9 @@

using Microsoft.AspNetCore.Mvc.RazorPages;
namespace RazorBuildWebSite.Views.Pages.Rzc
{
public class Page_Model : PageModel
{
}
}

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(StandardTestWebsiteTfms)</TargetFrameworks>
<DefineConstants>$(DefineConstants)</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.Mvc\Microsoft.AspNetCore.Mvc.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace RazorBuildWebSite.Views.Views.Rzc
{
class Index
{
}
}

View File

@ -0,0 +1,13 @@

using Microsoft.AspNetCore.Mvc;
namespace RazorBuildWebSite.Controllers
{
public class PrecompilationController : Controller
{
public new ActionResult View()
{
return base.View();
}
}
}

View File

@ -0,0 +1,13 @@

using Microsoft.AspNetCore.Mvc;
namespace RazorBuildWebSite.Controllers
{
public class RzcController : Controller
{
public new ActionResult View()
{
return base.View();
}
}
}

View File

@ -0,0 +1,4 @@
@page
@model Page_Model
Hello from runtime-compiled precompilation page!

View File

@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace RazorBuildWebSite.Pages.Precompilation
{
public class Page_Model : PageModel
{
}
}

View File

@ -0,0 +1,4 @@
@page
@model Page_Model
Hello from runtime-compiled rzc page!

View File

@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace RazorBuildWebSite.Pages.Rzc
{
public class Page_Model : PageModel
{
}
}

View File

@ -0,0 +1 @@
@namespace RazorBuildWebSite.Pages

View File

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>$(StandardTestWebsiteTfms)</TargetFrameworks>
<DefineConstants>$(DefineConstants)</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.Mvc\Microsoft.AspNetCore.Mvc.csproj" />
<!-- Faking like we had Razor-on-Build (Rzc) and MvcPrecompilation -->
<ProjectReference Include="..\RazorBuildWebSite.PrecompiledViews\RazorBuildWebSite.PrecompiledViews.csproj" />
<ProjectReference Include="..\RazorBuildWebSite.Views\RazorBuildWebSite.Views.csproj" />
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(MicrosoftAspNetCoreStaticFilesPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
<!--
Referencing here so you can easily regenerate the C# from Razor.
Just do `dotnet build /t:RazorGenerate /p:TargetFramework=netcoreapp2.0` and look in obj/Debug/netcoreapp2.0/Razor
-->
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="$(MicrosoftAspNetCoreRazorDesignPackageVersion)" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,35 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace RazorBuildWebSite
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
app.UseMvcWithDefaultRoute();
}
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.UseKestrel()
.UseIISIntegration()
.Build();
host.Run();
}
}
}

View File

@ -0,0 +1 @@
Hello from runtime-compiled precompilation view!

View File

@ -0,0 +1 @@
Hello from runtime-compiled rzc view!

View File

@ -0,0 +1 @@
@namespace RazorBuildWebSite.Views

View File

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