diff --git a/Mvc.sln b/Mvc.sln
index b2bf771f05..107d31ef52 100644
--- a/Mvc.sln
+++ b/Mvc.sln
@@ -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}
diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewDescriptor.cs
index 660f38f6de..e1ccf3c3b6 100644
--- a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewDescriptor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewDescriptor.cs
@@ -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
{
+ ///
+ /// Represents a compiled Razor View or Page.
+ ///
public class CompiledViewDescriptor
{
+ ///
+ /// Creates a new .
+ ///
+ public CompiledViewDescriptor()
+ {
+
+ }
+
+ ///
+ /// Creates a new . At least one of or
+ /// must be non-null.
+ ///
+ /// The .
+ /// The .
+ 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();
+ RelativePath = ViewPath.NormalizePath(item?.Identifier ?? attribute.Path);
+ IsPrecompiled = true;
+ }
+
///
/// The normalized application relative path of the view.
///
@@ -27,5 +74,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
/// Gets a value that determines if the view is precompiled.
///
public bool IsPrecompiled { get; set; }
+
+ ///
+ /// Gets the descriptor for this view.
+ ///
+ public RazorCompiledItem Item { get; set; }
+
+ ///
+ /// Gets the type of the compiled item.
+ ///
+ public Type Type => Item?.Type ?? ViewAttribute?.ViewType;
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs
index a9e3b15ed2..ade3da33e3 100644
--- a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs
@@ -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 ViewAssemblySuffixes = new string[]
+ {
+ PrecompiledViewsAssemblySuffix,
+ ".Views",
+ };
+
///
public void PopulateFeature(IEnumerable parts, ViewsFeature feature)
{
foreach (var assemblyPart in parts.OfType())
{
- var viewAttributes = GetViewAttributes(assemblyPart);
- foreach (var attribute in viewAttributes)
- {
- var relativePath = ViewPath.NormalizePath(attribute.Path);
- var viewDescriptor = new CompiledViewDescriptor
- {
- ExpirationTokens = Array.Empty(),
- 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 items,
+ IEnumerable 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(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 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();
+ }
+
///
/// Gets the sequence of instances associated with the specified .
///
@@ -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();
@@ -62,29 +114,28 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
return Enumerable.Empty();
}
- 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.
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ChecksumValidator.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ChecksumValidator.cs
new file mode 100644
index 0000000000..704ae1151f
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ChecksumValidator.cs
@@ -0,0 +1,122 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Linq;
+using Microsoft.AspNetCore.Razor.Hosting;
+using Microsoft.AspNetCore.Razor.Language;
+
+namespace Microsoft.AspNetCore.Mvc.Razor.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;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorPageFactoryProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorPageFactoryProvider.cs
index 0ffd390d52..b5c7341830 100644
--- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorPageFactoryProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorPageFactoryProvider.cs
@@ -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.
diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs
index 0a031b8cba..a9119951ef 100644
--- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs
@@ -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> _precompiledViewLookup;
- private readonly ConcurrentDictionary _normalizedPathLookup;
+ private readonly Dictionary _precompiledViews;
+ private readonly ConcurrentDictionary _normalizedPathCache;
private readonly IFileProvider _fileProvider;
private readonly RazorTemplateEngine _templateEngine;
private readonly Action _compilationCallback;
@@ -78,26 +81,33 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
_compilationCallback = compilationCallback;
_logger = logger;
- _normalizedPathLookup = new ConcurrentDictionary(StringComparer.Ordinal);
+ _normalizedPathCache = new ConcurrentDictionary(StringComparer.Ordinal);
+
+ // This is our L0 cache, and is a durable store. Views migrate into the cache as they are requested
+ // from either the set of known precompiled views, or by being compiled.
_cache = new MemoryCache(new MemoryCacheOptions());
- _precompiledViewLookup = new Dictionary>(
+ // 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(
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 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 CreateCacheEntry(string normalizedPath)
+ private Task OnCacheMiss(string normalizedPath)
{
- TaskCompletionSource compilationTaskSource = null;
+ ViewCompilerWorkItem item;
+ TaskCompletionSource taskSource;
MemoryCacheEntryOptions cacheEntryOptions;
- Task 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 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();
- 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();
+ 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(), // 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(),
+ };
+
+ 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()
+ {
+ _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();
- 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();
+
+ 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 ExpirationTokens { get; set; }
+
+ public CompiledViewDescriptor Descriptor { get; set; }
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs
index f82255ab10..3b6a518dd3 100644
--- a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs
@@ -378,6 +378,20 @@ namespace Microsoft.AspNetCore.Mvc.Razor
internal static string FormatUnsupportedDebugInformationFormat(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("UnsupportedDebugInformationFormat"), p0);
+ ///
+ /// At least one of the '{0}' or '{1}' values must be non-null.
+ ///
+ internal static string CompiledViewDescriptor_NoData
+ {
+ get => GetString("CompiledViewDescriptor_NoData");
+ }
+
+ ///
+ /// At least one of the '{0}' or '{1}' values must be non-null.
+ ///
+ 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);
diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx
index 63deeebd9b..19b0e8f053 100644
--- a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx
+++ b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx
@@ -197,4 +197,7 @@
The debug type specified in the dependency context could be parsed. The debug type value '{0}' is not supported.
-
+
+ At least one of the '{0}' or '{1}' values must be non-null.
+
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageRouteModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageRouteModelProvider.cs
index 4a5f9aa72f..032f868abd 100644
--- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageRouteModelProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageRouteModelProvider.cs
@@ -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 _logger;
- private List _cachedModels;
public CompiledPageRouteModelProvider(
ApplicationPartManager applicationManager,
IOptions 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();
}
@@ -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 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();
- 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;
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj
index b360623583..e8db9d460a 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj
@@ -30,6 +30,7 @@
+
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorBuildTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorBuildTest.cs
new file mode 100644
index 0000000000..e9ecfacbdf
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorBuildTest.cs
@@ -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>
+ {
+ public RazorBuildTest(MvcTestFixture 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());
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs
index c61ac53b2b..7493f608d1 100644
--- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs
@@ -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>
+
+ var items = new Dictionary>
+ {
+ {
+ 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>
{
{
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